1. 前言
当系统异常产生了dump文件需要我们对其进行排查时,其本质上考验的是我们对于Java运行时内存结构的知识掌握是否牢固以及对业务代码的熟悉程度。其次不要觉得这是一件很有技术含量高大上的事情,只要工具会用,对一般的异常有基本的判断,大部分有点经验的开发都能胜任这个事情。
分析dump文件使用最多的便是Eclipse的MAT
工具或Java自带的Visual VM
,更加推荐使用Visual VM
来进行排查,毕竟是官方的工具,只要安装了JDK就会有,并且基本能满足正常的排查要求。
很多开发平时很少有机会根据dump文件排查生产问题,因此总觉得这个事情比较虚,且没有好的工具去实现,其实Java官方提供的Visual VM
就能满足大部分场景,基本足以解决日常需要。但前提是要学会使用导出Dump文件命令,以及在程序的启动参数上增加异常自动生成dump文件的配置。
使用Visual VM排查时,使用类实例数、大小属性和查看堆线程统计三个功能基本就能确定是哪些类或对象过多导致内存溢出,最后再去代码中看这些类在哪些地方会频繁使用创建基本就能解决大部分问题。当我们在使用第三方框架时,一些框架的标识最好能够在对象中体现,这样能够帮助我们更快的更为是哪些功能有问题。
Visual VM的使用比较简单,在这里不做过多阐述。
2. 堆内存溢出
异常日志:
Java heap space
堆内存异常是比较常见的,毕竟Java的所有数组和对象分配都是在堆上进行的。
一般堆内存溢出常见于大量的创建对象却没有释放,这种现象是比较容易排查的,只需要判断某类对象是否过多即可。常见于重复代码块,或因某个事件频繁触发导致某个方法被频繁调用,且方法里的对象一直没被释放导致内存溢出。
3. GC执行异常
异常日志:
GC overhead limit exceeded
当多次执行垃圾收集的时间占用了CPU的98%,且GC回收的内存少于2%,JVM就会抛出这个异常。但这个异常在部分情况下都是会被Java heap space
代替的,所以仅凭这个无法准确判断出具体是什么问题,唯一能确定的就是当前堆内存已经被占满,且在频繁的执行GC,和堆内存溢出有相似之处。
处理方式一般和堆内存异常类似,需要排查哪类对象占用了过多的内存,在哪个代码块中存在频繁的创建对象且不释放的情况。
4. 元空间内存溢出
异常日志:
Metaspace
从JDK8起,元空间代替了永久代,元空间一般存储的数据为Class对象和常量对象等。
当出现这个问题需要着重排查新增的代理对象和字符串intern方法的使用。
5. 创建线程异常
异常日志:
Unable to create new native thread
每个机器内核创建的线程数量是有限制的,当创建的线程数量过多时抛出该异常。
这类问题只要指定了线程名称都很好排查,可以利用Visual VM查看堆转储上的线程就能看出来是哪些线程异常数量过多。
6. 内存交换问题
异常日志:
Out of swap space?
操作系统一般都有虚拟内存,当物理运行内存不够时会使用虚拟内存,但当虚拟内存都无法满足JVM的要求时就会抛出该异常。
一般碰到这个问题需要检查JVM的大小配置是否超过了机器本身配置,并检查有没有哪些对象创建数量异常导致内存飙升。如果配置或代码都没问题,那最终只能升级机器、转微服务开发或对程序业务进行拆分以满足程序的性能要求。
7. 数组长度过大
异常日志:
Requested array size exceeds VM limit
出现的频率较低,当数组的长度超出JVM的限制则抛该异常。
如果实际不需要这么长的数组则设置合适即可,如果需要这么长的数组则需要对数组进行分段处理。
8. 系统误杀异常
异常日志:
Kill process or sacrifice child
当操作系统可用内存极低的情况下会触发killer操作杀掉部分线程,如果Java程序因为这个情况被误杀则会抛出该异常。
一般原因是服务器运行了其它的程序,导致其它的程序占用过多内存,触发了killer机制,Java程序单独部署可以避免这个问题。
生产问题千奇百怪,但只要把握住Java的运行时原理,大致判断出不同的对象数据存储在哪些地方,并根据堆对象统计大致判断出问题所在,大部分生产问题都是可以解决的。
使用dump排查问题不是洪水猛兽。