【0020】全方位剖析Java内存溢出

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

在实际开发中,我们总会遇到过内存溢出异常OutOfMemoryError(OOM),也很难找出问题所在,漫无目的去查看程序代码也无补于事,本文就是提供一些技巧或经验,当遇到内存溢出时应该如何查找问题所在。


本文大纲:

1OOM分析技巧

2、运行时数据区内存溢出

3、直接内存溢出

4、总结



一、OOM分析技巧

当遇到内存溢出OutOfMemoryError异常的时候。


第一步:先判断内存溢出发生在哪个区域,这里的区域指的是Java虚拟机运行时数据区,堆、栈、方法区都会发生内存溢出,如果都不是,则就是本机直接内存溢出。

第二步:使用内存分析工具对内存进行分析,找出有问题的核心代码。


第三步:修改程序代码,重新部署测试了。第二步和第三步一般要重复进行直到解决问题。


二、运行时数据区内存溢出

内存溢出发生在运行时数据区时,可能是堆、栈或方法区这三个区域,不可能是程序计数器,因为程序计数器工作需要的内存很小很小。


1、内存溢出发生在堆

内存溢出发生在堆,多数情况是因为创建的对象数量太多,而垃圾收集器还没有来得及回收。具体原因有两种。


第一种是内存泄露,即程序中创建了很多对象,使用完之后,却保留了引用,从而导致GC没有回收。举个例子,比如说我往一个Stack里面存对象,但是每次取出对象之后却不清空Stack里面的对象引用,从而让GC觉得那些对象是有用的,最终没有被回收。


第二种是内存溢出,即程序运行就是需要很大的内存,这种还比较好办,把虚拟机的内存参数配置大一点可以解决,但是程序方面最好也查找出占用内存如此之大的原因是那些对象导致的,从而改善优化程序,毕竟调大虚拟机运行内存参数只是临时之策。


看一下程序,运行下面的程序,使用的虚拟机参数是:-verbose:gc-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 

-XX:+HeapDumpOnOutOfMemoryError


解释一下使用到的虚拟机参数:

-verbose:gc 打印垃圾收集器信息

-Xms20M虚拟机运行最小内存为20M

-Xmx20M 虚拟机运行最大内存为20M

-Xmn10M 堆中新生代的内存为10M

-XX:+PrintGCDetails 打印垃圾收集器的详细信息

-XX:SurvivorRatio=8 堆中Eden区与Survivor区的比值为8,Survivor区有两个,每个Survivor区占新生代的1/10

-XX:+HeapDumpOnOutOfMemoryError将内存溢出的错误生成文件


【0020】全方位剖析Java内存溢出


运行结果如下:

【0020】全方位剖析Java内存溢出


分析:报错的信息中显示Java heap space,意思就是堆空间已满。


生成的错误文件是java_pid3964.hprof,使用MAT内存分析工具进行分析,MAT是一个Eclipse的插件,查看histogram视图发现主要原因是由于Fish这个类实例过多导致。


既然如此,那就查看具体代码找出创建此对象的地方,然后进行程序修改,问题得以解决。

【0020】全方位剖析Java内存溢出

2、内存溢出发生在栈

说到栈,很多人会想到StackOverflowError这个异常,开发中偶尔会遇到,是由于递归太深导致,看一下下面的程序,运行时虚拟机参数为:-Xss128k,把栈的容量设置得很小,栈的深度就会大大降低,也就是栈帧数量减少,一个方法一个栈帧。

【0020】全方位剖析Java内存溢出


运行结果如下,显示stackLeak方法不断被递归调用,直到栈溢出,直接点击提示的错误行,即可找出程序的根源代码。


【0020】全方位剖析Java内存溢出


接下来说一下重点,栈也会遇到内存溢出的问题,要想栈的内存溢出就得设置栈的容量大一点,从而栈的数量就减少,也即是程序支持的线程数减少。

设置虚拟机运行参数为-Xss10M,每个栈的容量为10M,下面的程序时通过不断的创建线程,一个线程一个栈,不断的增大消耗内存,直到内存溢出。

【0020】全方位剖析Java内存溢出


运行结果截图就不提供了,由于运行该程序会导致机器假死,我的电脑也假死了一次,不想重启,我直接说一下结果。程序运行一段时间,电脑慢慢动不了,然后报内存溢出异常,并提示不能创建新的本地方法。unable to create new native method。


因为Java中的线程是和计算机内核映射的,创建新的线程需要本地资源,而本地资源是通过Java本地方法调用的,遇到这种情况,需要把虚拟机参数中的栈容量调小一点以容纳更多的线程,程序方面检查为什么有那么多线程在跑。


3、内存溢出发生在方法区

方法区主要存储类的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。如果方法区抛出内存溢出异常,就是应用系统中类的信息太多,一般是由于动态技术造成,比如动态生成类、动态代理。


下面案例使用CGLIB来做一个测试。虚拟机参数为:-XX:PermSize=10M  -XX:MaxPermSize=10M,设置持久代初始化内存为10M,最大内存也是10M。

【0020】全方位剖析Java内存溢出


运行结果如下:【0020】全方位剖析Java内存溢出


使用MAT工具查看内存分析,发现创建了大量的代理对象类,这无疑对方法区造成很大的内存消耗。

【0020】全方位剖析Java内存溢出


三、直接内存溢出

直接内存不属于虚拟机运行时数据区的一部分,但是Java中的NIO可以使用直接内存,我们通过反射机制来使用Unsafe类进行分配直接内存,直到内存溢出。


虚拟机参数设置:

-XX:MaxDirectMemorySize=10M,最大的直接内存为10M

【0020】全方位剖析Java内存溢出


运行结果如下:

【0020】全方位剖析Java内存溢出

分析:从错误信息中可以看到堆空间和持久代空间都没有满,但是内存溢出了,这种情况原因比较难找,一般是NIO的问题,可以先排查程序中使用到NIO的部分。


四、总结

简单总结一下,本文主要介绍了如何排查内存溢出异常,先锁定内存溢出所在的区域,然后直接找不出问题所在,就使用内存分析工具进行分析,最后调整修改代码了。步骤虽然很简单,但是实际开发中要找出内存溢出的核心代码,还是比较困难的,在这只是指明一个大致方向。感谢您的阅读,谢谢。

始发于微信公众号: Java框架源码分析