Java开发者们最为头疼的问题就是线上常常会抛出OOM异常,有人说,这肯定是内存不够用了啊,扩大内存容量不就可以了嘛,但是内存是需要代价的,不能无限的扩展,虽然JVM为我们管理内存,保证了内存的合理回收,但是如果我们对它的底层了解的够透彻,编写出更加健壮的程序,就可以降低成本,充分利用资源,减少线上故障带来的损失。
在学习如何防止OOM之前,我们首先要知道什么情况下会发生OOM?
1.堆溢出
堆是JVM中很重要的一块内存,绝大部分对象会被分配在堆上,因此堆内存的管理也成为一件很棘手的事情,一般来说,大多数内存溢出都会发生该区域,大量的创建对象,而这些对象又各自持有强引用不能被回收,当堆内存的使用达到Xmx所指定的堆空间大小的时候,溢出错误就自然而然的产生。
当堆内存溢出的时候我们如何解决呢?最为简单明了的方法就是设置参数-Xmx指定一个更大的堆空间。但是我们也不可能一直调大堆内存,因此,我们可以使用一些内存分析工具,比如MAT,Visual VM工具来查看对象的创建情况,查看是否存在内存泄漏,优化代码,回收无用的对象。
2.直接内存溢出
在Java中NIO,我们也可以直接使用堆外内存,也就是直接内存的使用,这部分空间的内存是直接向操作系统申请的,直接内存的申请速度一般要比堆内存慢,但是其访问速度要比堆内存快,因此,对于那些可以复用的,并且会被经常访问的空间,使用直接内存是可以提高系统性能的。但是由于直接内存不受JVM完全管理,若使用不当,也容易触发直接内存溢出,导致宕机。
如果系统的堆内存少有GC发生,而直接内存申请频繁,会比较容易导致直接内存溢出。因为直接内存不一定能够触发GC,所以保证直接内存不溢出的方法是合理地进行Full GC的执行,或者设定一个系统实际可达的-XX:MaxDirectMemorySize
的值,这样,当直接内存使用超过该值得时候就会触发GC,从而避免内存溢出问题,不过这样也会因此频繁的GC导致系统执行变慢,应当斟酌设置。
3.过多线程导致OOM
由于每一个线程的开启都要占用系统内存,因此当线程数量太多的时候,也有可能会导致OOM,由于线程的栈空间也是堆外分配的,因此和直接内存非常相似,如果想让系统支持更多的线程,那么就需要使用一个较小的堆空间。另外,我们还可以减少每一个线程所占的内存空间,使用参数-Xss
指定线程的栈空间。
4.永久(元数据)区溢出
在JDK1.8之后,永久区被替换为元数据区域,它们的功能都是相似的,保存类的元数据信息。如果一个系统不断地产生新的类,而没有回收,那么最终很有可能会导致永久区溢出。如果要解决永久区溢出的问题,可以从下面几个方面考虑:
- 增加MaxPermSize的值。
- 减少系统需要的类的数量。
- 使用ClassLoader合理地装载各个类,并定期进行回收。
5.GC效率低引起的OOM
如果堆内存空间太小,那么GC所占的时间就会较多,并且回收所释放的内存就会较少,根据GC所占用的系统时间,以及释放内存的大小,虚拟机会评估GC的效率,一旦虚拟机认为GC的效率过低,就有可能直接抛出OOM异常,不过虚拟机不会随意的判定,以下是虚拟机会检查的几种情况:
- 花在GC上的时间是否超过了98%。
- 老年代释放的内存是否小于2%。
- eden区释放的内存是否小于2%。
- 是否连续最近5次GC都出现上述几种情况。
虽然虚拟机限制了这么多的条件,但是在绝大部分的场合中,还是会抛出堆溢出的错误。不过这个错误只是为了提示系统的堆内存可能太小,虚拟机并不强制一定要开启这个错误提示。
参考资料:《实战Java虚拟机》