11

内存溢出的几种原因和解决办法

 3 years ago
source link: http://javakk.com/1014.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JVM创建者在设计它时考虑了自动内存管理,这意味着程序员不需要担心内存分配和内存。未使用的对象可以以透明的方式自动释放,这非常方便,尤其是当您刚接触JVM时。但是,即使是一般情况下,要编写的代码也比传统方法少,而且不容易出错,因为传统方法要求您手动执行所有操作。

然而,实际情况并不像听起来那么理想,尤其是当你在开发具有巨大流量的长寿命应用程序时。虽然在JVM中引起内存泄漏比在C中更难,但仍然有可能。选择GC算法并将其参数化对性能也有很大的影响。而且,与任何抽象或自动化一样,如果您希望有意识地编写代码(这是专业的方法),您需要了解在幕后进行了哪些工作,以便能够预防或诊断问题。让我们来看看一些有用的工具和技术,这些工具和技术将帮助您找到应用程序崩溃或减速的原因,而不是快速工作并能够完成创建它的目的。

OutOfMemoryError

我们需要的第一件事是一段导致 OutOfMemoryError 的可靠代码。OutOfMemoryError是JVM引发的异常,它通知我们内存不足。可能有许多可能的原因导致抛出此异常,您可以查看异常的原因以了解发生了什么。现在,让我们编写一个应用程序,它会不断地分配内存,直到超过内存限制为止;

// file Application.scala
object Application {
  def main(args: Array[String]): Unit = {
    LazyList.from(0).toList
  }
}

编译一下:

scalac Application.scala

设置堆大小:

scala Application -J-Xmx10m -J-Xms10m

XmsXmx 是JVM标志,指定应用程序的堆大小(或者简单地说,应用程序将拥有多少内存),其中 Xms 代表堆的初始大小, Xmx 代表最大大小。

在我们的例子中,10MB是一个足够小的值,可以很快体验到内存不足。我们可以看到应用程序崩溃,错误如下:

Exception in thread "main" java.lang.<a href="http://javakk.com/tag/outofmemoryerror" title="查看更多关于 OutOfMemoryError 的文章" target="_blank">OutOfMemoryError</a>: Java heap space

内存泄露的原因

在这种情况下,问题很明显。我们只有一个线程和一行代码。当然,实际应用程序要比这复杂得多。当我们在生产中看到 OutOfMemoryError 时,查看堆栈跟踪对我们没有帮助,因为导致问题的行是相当随机的,我们正在寻找一个分配内存且不释放内存的代码。我们可以在JVM内部查找问题的根源。

让我们添加一个 -XX:+HeapDumpOnOutOfMemoryError 标志,它将导致在OutOfMemoryError时生成堆转储。

scala Application -J-Xmx10m -J-Xms10m -J-XX:+HeapDumpOnOutOfMemoryError

我们可以看到,当我们的应用程序崩溃时,生成了一个扩展名为 .hprof 和PID的文件。这个文件是二进制的,所以我们需要一些工具来查看里面的内容。有很多工具可以完成这项工作,即使是在线的——比如HeapHero(https://heaphero.io/),如果数据不敏感,则可以使用。一开始,我推荐VisualVM。

导入文件后,您可以检查的最有用的内容是所有已分配对象的列表及其使用的内存百分比。我们可以从 scala.collection.immutable 不可变以及几乎构成整个堆的整数。这与我们在项目中所做的是一致的。列表和整数是显而易见的,因为我们创建 List[int] 作为结果。缺点. 这是因为Scala的 LazyList 使用了 memorization ,所以它保留了对所有元素的引用。

在磁盘空间明显受限的环境中(例如在云中),使用 -XX:+HeapDumpOnOutOfMemoryError 选项时要小心。这是一个完整的堆转储,这意味着在OutOfMemoryError的情况下,它至少与最大堆大小一样大。对于大的堆,这可能比分配给映像的磁盘空间大得多(因为您可能不需要太多空间,因为最好不要从服务直接写入磁盘)。

测试living应用程序如何使用VisualVM分配对象

在许多情况下,了解一个活动的应用程序如何分配对象是很有用的。您也可以使用VisualVM实现此目的。这个工具允许您直接连接到任何本地运行的JVM。如果需要检查已部署的应用程序,可以通过JMX连接连接到它。您只需设置一个标志,在应用程序启动时启用JMX连接。

scala Application -J-Xmx10m -J-Xms10m -J-Dcom.sun.management.jmxremote

之后,您就可以与VisualVM连接了。如果在云中部署应用程序,请确保正在使用的端口已打开。

用这种方式研究应用程序比简单地查看堆转储提供了更多的选择。您可以查看许多有用的统计信息,例如GC活动和已用堆大小。您可以随时请求JVM为您创建一个堆转储,这样您就不需要等到应用程序崩溃时才进行。一个真正有用的工具是一个采样器,它允许您创建一个已用内存的转储。这实际上类似于堆转储,但是在本例中,您可以看到每个线程的分配率,这样您就可以跟踪比应该更贪婪地分配内存的线程。在你的调查过程中,这可能是一个非常有用的信息。

垃圾收集

我们已经了解了如何在应用程序运行或崩溃后查看内存分配情况。现在让我们来看看垃圾回收。有时 OutOfMemoryError 不是由内存泄漏引起的,而是因为我们没有给应用程序足够的内存来处理。当应用程序开始使用比通常更多的内存时,也会发生这种情况,因为流量增加。但是不管发生了什么,为了了解整个情况,能够查看GC日志是非常有用的,因为它们可能包含我们故事中缺失的部分。分析GC日志比简单地查看堆转储要复杂得多,因为它需要了解垃圾收集算法是如何工作的。下面是一个很好的关于这个主题的介绍:

分析垃圾收集日志

GC作为JVM中的一个进程透明地工作。但是,我们可以告诉JVM,我们需要它来生成GC日志。可以使用以下附加参数执行此操作:

scala Application -J-Xlog:gc:file=gc-log

请记住,与线程转储不同,这是一个操作,顾名思义,它容纳日志,而不是创建一次性转储。这意味着每个重要的GC操作都会被登录,并且您不能将JVM配置为只在应用程序崩溃时创建日志。确保有足够的磁盘空间来存储日志是很重要的。您也可以使用参数 NumberOfGCLogFilesGCLogFileSize 来确保日志的大小是可控的。

scala Application -J-Xlog:gc:file=gc-log -J-XX:NumberOfGCLogFiles=10 -J-XX:GCLogFileSize=5M

GC日志是常规的文本文件,因此您可以在任何文本编辑器中读取它们,但是为了能够快速分析它们,您需要一个特殊的工具。你可以使用许多免费的在线工具(例如。https://gceasy.io/). 值得注意的是,GC日志不包含任何应用程序数据,只包含与GC工作相关的数据,因此您可以安全地将日志上载到外部服务,而不必担心暴露敏感数据。

有许多有用的统计数据你可能想看看。其中之一是应用程序在执行GC而不是直接为您工作的时间百分比。这一指标的每一次显著增长都应该令人震惊。您还应该查看堆使用率图,并检查与次要GC相比,完整GC的运行频率。经过一段时间的练习,这个工具提供的所有数据应该能让您对应用程序的内存利用率有一个很好的整体了解。

预防胜于治疗

到目前为止,我们已经讨论了当应用程序崩溃或不稳定时该怎么做。尽管这很有用,但我们需要能够更早地作出反应。我们绝对不想在压力下工作,在应用程序无法工作的情况下拼命解决与内存管理相关的问题。作为专业人士,我们应该做得更好——我们需要监控我们的应用程序,以便及时发现问题,在应用程序崩溃之前。如果我们事先检查了正确的指标,您就可以解决大多数与内存管理相关的问题,而不会对用户产生重大影响。

小结

处理与内存相关的问题是很困难的,本文只是对这个主题的一个介绍。我们决不应认为使用JVM可以免除内存管理的所有责任。此外,值得注意的是,每个GC周期都使用CPU,因此优化代码不仅有助于避免崩溃,还可以降低云计算成本。

r6n63uZ.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK