JVM应该可以算Java中最为核心的部分了,其中开箱即用的内存管理又是JVM中的核心组成部分。我们都知道JVM的内存管理具有垃圾回收功能(Java Garbage Collector),编码时只需要new而无需主动的释放(类似于C++中的delete操作),所以Java中比较少出现内存泄露的情况。比较少出现,并不一定就不会出现,那么Java程序在什么时候会出现内存泄露呢?出现内存泄露该如何排查呢?
Java程序为什么会有内存泄露
什么是内存泄露呢?内存泄露可以定义为:当一个对象已经不再被应用程序使用以后,它所占用的内存没有得到及时的释放,导致内存使用量随着时间的推移不断的增加,最终导致应用程序崩溃的现象(Java中会在new新对象的时候抛出OutOfMemoryError)。
对于Java程序而言,当一个Object已经不会被程序所使用,但是它还被其它对象所引用,从而导致GC的时候无法被回收,从而导致内存泄露。
下图比较直观的展示了Java内存泄露发生的情形:
从上图我们可以把对象分为两大类:被引用的和不被引用的。垃圾回收的时候不释放那些不被引用的对象,被引用的对象则不会被释放,即便是这些对象后续一直都没有被用过。
定位Java内存泄露是比较麻烦的事情,需要使用到JVM提供的多种工具来进行内存分析,并且往往还需要结合代码进行分析。
Java堆内存泄露
首先我们来看看Java程序中最为常见的堆内存泄露。
如果想要直观的模拟出堆内存泄露,我们需要设置一一个较小的堆大小(内存泄露是独立存在的,和对内存大小无关,不过较小的堆内存大小可以更直观的观察到内存泄露)。
我们可以通过如下两个启动参数设置对内存大小:
通过静态变量来演示内存泄露
首先我们通过如下的代码来看看看看正常Java代码运行时的内存变化情况:
启动参数:-Xms20m -Xmx20m
运行是的内存变化图:
在我们的代码中我们每秒调用一次test方法,这个方法中会往list中添加数据,但是在方法返回之后这些数据就处于无引用状态了(并不一定会立马进行GC),我们每10秒调用一次System.gc()(full GC)。从内存监控图上能够直观的观察到堆内存的使用变化曲线。
接下来我们对上面的代码做一下修改:
静态变量保存了过多不在使用的对象引用导致的内存泄露是我们编码过程中最为常见的一种内存泄露方式。下面的代码就演示了这个情况:
为了更加直观的观察内存变化,我稍微调整了一下每次插入的数据个数,启动参数仍旧和上面一下。这个时候我们能够得到如下图所示的内存变化曲线:
从曲线中我们可以发现,堆内存使用量一直在增加,并没有和前一个示例一样在full GC后释放没有使用的堆内存,每次调用test方法后添加到list中的对象都会被list所引用,所以GC是不会收集并释放这些内存的。
如何定位和解决内存泄漏
Java的内存泄漏定位一般是比较困难的,需要使用到很多的实践经验和调试技巧。下面是一些比较通用的方法:
- 可以添加-verbose:gc启动参数来输出Java程序的GC日志。通过分析这些日志,可以知道每次GC后内存是否有增加,如果在缓慢的增加的那,那就有可能是内存泄漏了(当然也需要结合当前的负载)。如果无法添加这个启动参数,也可以使用jstat来查看实时的gc日志。如果条件允许的话可以考虑使用jvisualvm图形化的观察,不过线上的话一般没这个条件。
- 当通过dump出堆内存,然后使用jvisualvm查看分析,一般能够分析出内存中大量存在的对象以及它的类型等。我们可以通过添加-XX:+HeapDumpOnOutOfMemoryError启动参数来自动保存发生OOM时的内存dump。
- 当确定出大对象,或者大量存在的实例类型以后,我们就需要去review代码,从实际的代码入手来定位到真正发生泄漏的代码。