在下列情况下,JVM会抛出内存溢出错误(OutofMemoryError):
后面两种情况涉及Java堆本身,更为常见,但是不要看到OutOfMemoryError
就自动下结论认为堆是问题所在。必须看一下为什么会发生这种错误(原因会是异常输出的一部分)。
列表中的第一种情况——JVM没有原生内存可用,其原因与堆无关。在32位的JVM中,一个进程的最大内存是4GB(在某些版本的Windows上是3GB,在某些比较老的Linux版本上是3.5 GB)。指定一个非常大的堆大小,比如说3.8 GB,使应用的大小很接近4GB的限制,这很危险。即便在64位的JVM中,操作系统的虚拟内存也不是JVM请求多少就有多少。
必须意识到,如果OutofMemoryError
消息中提到了原生内存的分配,那对堆进行调优解决不了问题:你、需要看一下错误中提到的是何种原生内存问题。例如,下面的消息说明线程栈的原生内存耗尽了:
Exception in thread "main" java.lang.OutofNemoryError:unable to create new native thread
这种内存错误与堆无关,其发生原因是永久代(在Java 7中)或元空间原生内存(在Java8中)满了。 根源可能有两种情况:第一种情况是应用使用的类太多,超出了永久代的默认容纳范围;解决方案是增加永久代的大小。(在Java 8中,如果设置了类的元空间的最大大小,也会出现同样的问题。)
第二种情况相对来说有点棘手:它涉及类加载器的内存泄漏。这种情况经常出现于Java EE应用服务器中。部署到应用服务器中的每个应用都运行在自己的类加载器中(这提供了隔离,使一个应用中的类不会和另一个应用中的类共享,也不会有干扰)。在开发中,每次修改了应用都必须重新部署,这时就会创建一个新的类加载器来加载新的类,而老的类加载器就可以退出作用域了。一旦类加载器退出了作用域,该类的元数据就可以回收了。
如果老的类加载器没有退出作用域,那么该类的元数据也就无法释放,最后永久代就会被填满,进而抛出OutOfMemoryError
。在这种情况下,增加永久代的大小会有所帮助,但最终只是推迟了错误抛出的时机而已。
如果在某个应用服务器环境中出现这种情况,除了联系应用服务器厂商,让他们修复内存泄漏问题外,也别无他法。如果正在编写的应用会创建并丢弃大量类加载器,一定要非常谨慎,确保类加载器本身能正确丢弃(尤其是,确保没有线程将其上下文加载器设置成一个临时的类加载器)。
识别这种情况的关键仍然是OutofMemoryError
的输出全文。在Java 8中,如果元空间满了,错误消息将会是下面这样的:
Exception in thread "main" java.lang.OutofMemoryError:Metaspace
在Java7中类似
Exception in thread “main” java.lang.OutOfMemoryError: PermGen space
3.堆内存不足
当确实是堆本身内存不足时,错误消息会是这样的:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
``
自动堆转储
OutOfMemoryError是不可预料的,很难确定应该何时获得堆转储。有几个JVM标志可以起到帮助,
-XX:+HeapDumpOnOutOfMemoryError,该标志默认为false,打开该标志,JVM会在抛出OutofNemoryError时创建堆转储。-XX:HeapDumpPath=<path>
该标志指定了堆转储将被写入的位置;默认会在应用的当前工作目录下生成java_
pid<pid>.hprof文件。这里的路径可以指定目录(这种情况下会使用默认的文件名),也可以指定要生成的实际文件的名字。
-XX:+HeapDumpAfterFullGC
这会在运行一次Full GC后生成一个堆转储文件。
-XX:+HeapDumpBeforeFullGC
这会在运行一次FullGC之前生成一个堆转储文件。
有的情况下(比如,因为执行了多次Full GC)会生成多个堆转储文件,这时JVM会在堆转储文件的名字上附一个序号。
如果应用会因为堆空间的原因不可预测地抛出`OutOfMemoryError`,而且需要那一刻的堆转储来分析错误原因,请尝试打开这些标志。
下图演示了由集合类(这里是一个HashMap)引发的Java内存泄漏这一经典案例。(集合类是导致内存泄漏的最常见原因:应用向集合中插入条目,但从不释放它们。)这是一个直方图对比视图:它显示了两个不同的堆转储中对象数目的差别。例如,与基线堆转储相比,目标堆转储中的Integer对象要多出19744个。
要克服这种情况,最好的办法是修改应用的逻辑,主动将不再需要的条目从集合中删除。 作为一种选择,可以使用弱引用或软引用的集合,当在应用中已经不存在对某些条目的任何引用时,该集合会自动丢弃它们,不过这样的集合是有代价的。
达到GC的开销限制
JVM抛出OutofNemoryErro
r的最后一种情况是JVM认为在执行GC上花费了太多时间:
Exception in thread “main” java.lang.OutofMemoryError: GC overhead limit exceeded当满足下列所有条件时就会抛出该错误。
-XX:GCTimeLimit=N
标志指定的值。其默认值是98(也就是,如果98%的时间花在了GC上,则该条件满足)。-XX:GCHeapFreeLimit=
N标志指定的值。其默认值是2,这意味着如果FullGC期间释放的内存不足堆的2%,则该条件满足。请注意,所有四个条件必须都满足。一般来说,应用中连续执行了5次以上的Full GC,不一定会抛出OutOfMemoryError
。其原因是,即便应用将98%的时间花费在执行Full GC上,但是每次GC期间释放的堆空间可能会超过2%。这种情况下可以考虑增加GCHeapFreeLinit的值。
还请注意,如果前两个条件连续4次Full GC周期都成立,作为释放内存的最后一搏,JVM中所有的软引用都会在第五次Full GC之前被释放。这往往会防止该错误,因为第五次Full GC很可能会释放超过2%的堆内存(假设该应用使用了软引用)。
快速小结
在Java中,第一种更高效使用内存的方式是减少堆内存的使用。这句话不难理解:堆内存用的越少,堆被填满的几率就越低,需要的GC周期也越少。而且有倍乘效应:新生代回收的次数更少,对象的晋升年龄也就不会很频繁地增加,这意味着对象被提升到老年代的可能性也降低了。因此,Full GC周期(或者是并发GC周期)也会减少。而且,如果这些Full GC周期能够清理更多内存,它们发生的频率也会降低。