堆内存居高不下,JDK8自适应作怪
背景
收到线上系统报警,堆内存超过98%,长时间居高不下。
内存泄漏检查
dump文件下载下来,使用visualVM分析了一波,发现有int[]和char[],占用了堆内存50%以上的空间,但是Retained Size 很小,意思就是执行一波FULL GC就会把大部分的占用空间清掉。
而且根据观察也确实是这样。这就说明这些对象实际都已经不可达了,早就应该被回收掉啊,为什么会跑到老年代呢。
Young GC
众所周知,JVM年轻代分为eden区和survivor区,对象被创建后首先在Eden区,如果一次young gc 没有将其回收的话,会到survivor区。从survivor区到old generation需要了解下动态年龄判断。
动态年龄判断:
- 对象超过15次没有被回收,可以通过MaxTenuringThreshold设置
- 相同年龄的对象超过survivor区的50%,可以通过TargetSurvivorRatio设置。
对象超过15次没被回收不太可能,那就是survivor区太小了?
Young Generation空间分配
看了一下进程的参数配置,只设置了初始堆内存和最大堆内存 2G,其他都是默认参数。
/export/servers/jdk1.8.0_20/bin/java -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.library.path=/usr/local/lib -server -Xms2048m -Xmx2048m -XX:MaxPermSize=256m -XX:+UnlockExperimentalVMOptions -Djava.awt.headless=true -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000 -Djmagick.systemclassloader=no -Dnetworkaddress.cache.ttl=300 -Dsun.net.inetaddr.ttl=300 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/Instances/xxxxx/server1/logs -XX:ErrorFile=/export/Instances/xxxxx/server1/logs/java_error_%p.log -Djava.endorsed.dirs=/export/servers/tomcat8.0.30/endorsed -classpath /export/servers/tomcat8.0.30/bin/bootstrap.jar:/export/servers/tomcat8.0.30/bin/tomcat-juli.jar -Dcatalina.base=/export/Instances/xxxxx/server1 -Dcatalina.home=/export/servers/tomcat8.0.30 -Djava.io.tmpdir=/export/Instances/xxxxx/server1/temp org.apache.catalina.startup.Bootstrap -config /export/Instances/xxxxx/server1/conf/server.xml start
那因为新生代和老年代的默认比例是1:2,eden区和两个survivor区默认比例是8:1:1,空间分配至应该是这样的:
名称 | 空间大小 |
---|---|
年轻代 | 2048*1/3= 682 |
Eden | 682*8/10= 546 |
Survivor | 682*1/10= 68 |
使用jmap看一下线上的情况
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 715653120 (682.5MB)
MaxNewSize = 715653120 (682.5MB)
OldSize = 1431830528 (1365.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 705167360 (672.5MB)
used = 251792376 (240.12792205810547MB)
free = 453374984 (432.37207794189453MB)
35.70675420938371% used
From Space:
capacity = 5242880 (5.0MB)
used = 3047568 (2.9063873291015625MB)
free = 2195312 (2.0936126708984375MB)
58.12774658203125% used
To Space:
capacity = 5242880 (5.0MB)
used = 0 (0.0MB)
free = 5242880 (5.0MB)
0.0% used
PS Old Generation
capacity = 1431830528 (1365.5MB)
used = 442316256 (421.8256530761719MB)
free = 989514272 (943.6743469238281MB)
30.891662620005263% used
672.5M和5M,纳尼?哪是8:1啊,都100多倍了,而且经过几次观察发现实际比例竟然在变化!
发现UseAdaptiveSizePolicy
经过查询发现 JDK 1.8 的默认垃圾回收器是 UseParallelGC,默认启动了 AdaptiveSizePolicy。这个参数会让垃圾回收根据每次垃圾回收的GC时间和吞吐量来动态调整eden区和survivor区的比例。
引用阿菜的博客的文章:
AdaptiveSizePolicy 有三个目标:
Pause goal:应用达到预期的 GC 暂停时间。
Throughput goal:应用达到预期的吞吐量,即应用正常运行时间 / (正常运行时间 + GC 耗时)。
Minimum footprint:尽可能小的内存占用量。
AdaptiveSizePolicy 为了达到三个预期目标,涉及以下操作:
如果 GC 停顿时间超过了预期值,会减小内存大小。理论上,减小内存,可以减少垃圾标记等操作的耗时,以此达到预期停顿时间。
如果应用吞吐量小于预期,会增加内存大小。理论上,增大内存,可以降低 GC 的频率,以此达到预期吞吐量。
如果应用达到了前两个目标,则尝试减小内存,以减少内存消耗。
临时处理
关闭UseAdaptiveSizePolicy策略,JVM增加参数:
-XX:-UseAdaptiveSizePolicy -XX:SurvivorRatio=8
同时显式申明survivor区的比例。
结果:
其他处理
团队netty大神贡献其他优化,主要是关闭了netty内存管理,每个对象使用完成之后立即释放。
ChannelInboundHandlerAdapter
SimpleChannelInboundHandler.java
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}