结合案例介绍JVM内存管理机制
问题回放
对某Linux服务器上的某应用进行压力测试,在一周的测试过程中,发现active内存(类似于AIX的计算内存)以每天150MB左右的速度增长。采用性能测试工具调用socket方式进行压测,TPS为3000左右,针对服务器的两个功能模块的测试均有此现象。
怀疑存在内存泄漏。
初步分析
一般内存泄漏与交易量或系统资源未释放有关。
首先分析内存增长量与交易量的关系:每交易内存增长字节数=增长字节数/总交易量=150*1024*1024/(24*60*60*3000)=0.6字节。通过初步分析,每个交易内存增长数量不足1字节。
第二通过执行lsof检查应用的打开文件数,并未发现不断增长的资源使用。
根据以上两点判断,不一定是内存泄漏问题,并将怀疑方向指向Java的GC(Garbage Collection)机制造成内存回收不完全造成。
内存分代管理
JVM对内存进行分代管理,它首先将内存分为新生代,老年代,持久代;在新生代中又将内存分为Eden,Survivor1,Survivor2。
新创建的对象,会由JVM分配在Eden空间中,Survivor1和Survivor2空间交替使用,老年代保存在两个Survivor空间交替几次后仍然存活的对象,持久代保存常量、代码等极少需要被回收的对象。
在Java中,新生代、老年代、eden代的大小都是可配置的。
GC
GC(Garbage Collection)是Java的内存回收模块,主要分为新生代GC和老年代GC两个子模块。
注意:Java有自动的内存回收,并不等于Java程序没有内存泄露。
新生代 GC
新生代GC在Eden空间已满时触发。对新生代内存进行清理,速度较快,发生较频繁。
新生代 GC的基本步骤:
1. Eden空间存活的对象被移动到其中一个幸存者空间。
2. 此后,在Eden空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。
3. 当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。
4. 在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。
老年代 GC
老年代GC事件基本上是在老年代空间已满时发生,也可通过在代码中主动调用GC命令触发。
其执行的基本操作如下:
1. 标记老年代中依然存活对象。(标记)
2. 从头开始检查堆内存空间,并且只留下依然幸存的对象。(清理)
3. 从头开始,顺序地填占堆内存空间。(压缩)
在高版本jdk中,也有其他多种为提高执行效率而作的优化处理。
服务器内存使用进一步分析
该服务器中为Java分配的堆内存总数为2000MB,新生代与老年代比为1:2,所以新生代约为667MB,老生代约为1333MB。其中eden和Survivor的比例是8:1,所以eden的大小约为533MB。
从交易处理机制上看,服务器在处理交易时,不会产生大量需要保存到老年代的对象,并且老年代内存空间相对比较大,不容易被填满。结合Java执行GC的原理分析,我们怀疑压力测试时,老年代内存的增长会非常缓慢,几天内都不会触发老年代GC,以致出现内存使用量不断增长不回收的现象。
猜测:如果定时做GC,内存应不会一直增长。
实验验证
通过对比不做GC和做GC,验证以上猜测:
1、不做GC的内存使用情况:
2、在程序中增加定时触发老年代GC回收,内存使用情况:
经8天的连续压力测试,在定时触发GC的情况下,系统内存未出现增长情况。
结论
该场景性能测试中看到内存不断增长的现象与Java的GC机制有关,不属于内存泄漏。如果需要及时回收内存资源,可在程序中主动调研全量GC,或调整Java内存参数,平衡GC、性能和合理的内存占用率。
垃圾回收:
https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/
JAVA在有GC的情况下也会内存泄露:
http://www.ibm.com/developerworks/cn/java/l-JavaMemoryLeak/