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