在这篇博文中,我想详细介绍一下 java.lang.OutOfMemoryError 错误这个错误是如何在Java应用程序中发生的。
在前面的条目中,我们看到 **OutOfMemoryError **有完全不同的类型。然而,最常见的错误是
Exception in thread "main": java.lang.OutOfMemoryError: Java heap space
此错误意味着堆上不再有足够的可用内存来填充新对象的内存请求,即不能在堆上生成新对象。由于根据JVM规范,每个堆都必须有一个垃圾收集器,这也意味着它不能再清空任何内存,堆被“活动”对象完全占用。
为了更好地理解这种情况是如何产生的,我首先要描述什么是Java中的“活动”对象。
在Java中,对象是在堆上创建的,只要它们仍然被引用,就一直存在。垃圾收集器在GC阶段检查对象是否仍然被引用,如果没有,垃圾收集器会将其标记为“垃圾”,并在稍后进行清理(还有其他GC算法,例如复制收集器或垃圾优先方法,但这些方法与理解无关)。然而,并不是每个引用都对对象的生存起决定性作用,只有所谓的GC根引用才起决定性作用。特别是在与Java内存泄漏相关的情况下, GC ROOT 是一个中心概念,您必须理解它才能识别对对象的关键引用。垃圾收集器根是未详细引用的对象,负责将引用的对象保留在内存中。如果一个对象没有被GC根直接或间接引用,它将被标记为“不可访问”并被释放到垃圾收集。垃圾收集根有三种类型:
这个具体的例子是最好的方式来说明这一点:
public class MyFrame extends javax.swing.JFrame {
// reachable via Classloader as soon class is loaded
public static final ArrayList STATIC = new ArrayList();
// as long as the JFrame is not dispose()'d,
// it is reachable via a native window
private final ArrayList jni = new ArrayList()
// while this method is executing parameter is kept reachable,
// even if it is not used
private void myMethod(ArrayList parameter) {
// while this method is running, this list is reachable from the stack
ArrayList local = new ArrayList();
}
}
基本上,您可以在堆中看到与 Java OutOfMemoryError 问题相关的三个不同问题区域:
当对象仍然具有GC根引用,但在应用程序中不再使用时,就会产生Java内存泄漏。这些“游荡对象”证明了JVM内存的完整持续时间。如果在应用程序逻辑中连续创建这样的“对象体”,典型的问题子对象是静态集合,它们被用作一种缓存。 add() 和 remove() 方法在这里使用的频率是多少。添加的对象被静态集合项引用,并且由于GC根引用(static)而不能再释放。
在内存泄漏的上下文中,也经常提到所谓的支配者或支配树。
image.png
支配者的概念来源于图论,当一个节点只能到达另一个节点时,它就被定义为另一个节点的支配者。因此,当没有其他对象C引用B时,对象A是另一个对象B的支配者。支配者树则是一个子树,其中来自根节点的条件应用于所有子节点。如果根引用被释放,整个支配树将被释放。因此,在内存泄漏搜索中,非常大的控制树是非常好的候选。
根据不再需要的对象的生成频率和大小,以及Java堆的配置大小,OutOfMemoryError迟早会发生。正是后一种变体,即所谓的“爬行内存泄漏”,在许多应用程序中都会发现,而且这些问题通常会被“忽略”,并且会遇到以下措施:
更大的堆来争取时间,直到错误发生。不幸的是,在64位jvm时代,这种方法正变得越来越流行。
晚上重启应用服务器。这将导致内存重置。如果内存在24小时内没有完全填满,可以通过重新启动来避免错误。
这两个版本都是危险的,因为它们对性能有负面影响,并且有可能由于用户行为的改变或更多的通信量而导致错误比预期更快地发生。性能也受到垃圾收集器的负面影响,因为越来越满的“终身生成”意味着GC必须经历更多的对象,“标记”阶段需要越来越多的时间,随着大量堆,要分析的对象的数量变得更大。因此,本系列文章将详细分析这些内存泄漏,以避免出现这种情况。
还有一些情况下,堆中的OutOfMemoryError不是由实际意义上的内存泄漏引起的,而是应用程序消耗了太多内存。在这种情况下,要么选择的堆太小,必须将其放大,要么必须减少应用程序的内存消耗,例如选择较小的缓存大小。
然而,临时存储的高消耗也特别重要,因为它会导致某些并行访问应用程序中发生OutOfMemoryError,因此这些应用程序是不确定的,因此会造成更大的不适,因为你不能在晚上重新开始。以下示例显示了可能的问题代码:
byte[] image = getTheByteImage();
response.setContentType("image/jpeg");
ServletOutputStream out = response.getOutputStream();
out.write(image);
out.flush();
out.close();
内存消耗在这里并不明显,但是,图像在每次调用时都作为字节数组放在堆上,然后再发送到浏览器。更好的选择是直接简化图像:
InputStream image = getTheImageAsStream();
response.setContentType("image/jpeg");
ServletOutputStream out = response.getOutputStream();
IOUtils.copy(image, out);
out.flush();
out.close();
( BufferedStreams 和 ioutil 在内部使用 byte ,但它们通常要小得多)
在这方面,我们第一次只有 java.lang.OutOfMemoryError 错误说明堆中的问题。
在本系列的下一部分“Java虚拟机的配置和监视”中,我将向您展示如何在sun jvm上配置和优化堆设置,以及如何使用JVM资源监视内存。
因此,接下来的两个部分将更实际,而不是理论性的,我计划整合一些小屏幕截图来给出说明性的例子。
有一些有趣的场景,引用不再可访问,但内存无法释放,例如:
public void someMethod() {
try {
String xml = getSomeBigXML();
// do something
} catch (AnException) {
// handle exception
}
// After this point xml is not reachable,
// but cannot be freed until the method is finished...
// do something long running
}
在这种情况下,GC无法释放“大”xml对象,因为它仍然有一个GC根,但在 try-catch 块之外无法访问它。如果该方法长时间运行,可能会导致内存问题。
“控制”应用程序内存的一个好选择是 java.lang.ref 文件告诉JVM如何处理对象的引用—例如,如果您使用 WeakReference ,如果您的应用程序中不再使用该对象,则该引用不会阻止GC完成该对象(就像“正常”引用一样)。 java.util.WeakHashMap 文件对所有条目使用 weakreference ,因此这是一种可能的缓存实现。这些类型的引用还允许您更好地“拦截”对象的生命周期。