总述
基本知识
JDK8内存模型
参数
GC基本要点
内存数据收集工具
JVM参数调整过程
一阶段(设置堆区总内存)
二阶段(调整New区内存)
三阶段(调整Eden与Survivor)
生产系统稳定性很重要,JVM内存越大,内存溢出的风险越低。只要主机内存允许,多浪费点内存在JVM上也无可厚非。不过,用更少的资源发挥更大的作用是条正道,所以应合理设置JVM内存。
不同java应用对内存要求不同,合适的JVM参数只能通过对每个应用实际运行过程中内存占用数据收集后才能确定。即先设置初值,然后在应用运行过程中收集内存占用数据,最后计算各内存区域的大小。
本文主要讲解如何初步确定一个新应用的内存参数,至于更细致的优化方案没做描述。
JDK8对内存区域的划分中去掉了之前版本的持久区(PermGen),-XX:PermSize和-XX:MaxPermSize参数失效,也不会发生java.lang.OutOfMemoryError: PermGen异常。取而代之的是元空间(Metaspace),元空间GC与堆区GC独立。
堆区内存模型:
JVM默认比例是Eden:S0:S1:Tenured=8:1:1:20。JVM“浪费”掉S1,即40份内存只有39份内存实际储存数据。
Old:New=2:1,对应参数–XX:NewRatio=2;Eden:S0=8:1,对应参数–XX:SurvivorRatio=8。
堆区内存回收分为New区的YGC和Old区的FGC。
-Xms128M :设置堆内存最小值
-Xmx128M :设置堆内存最大值
-XX:NewSize=64M :设置New区最小值
-XX:MaxNewSize=64 :设置New区最大值
-XX:NewRatio=2 :设置Old区与New区的比例
-Xmn64M :设置New区大小,等价于-XX:NewSize=64M 与-XX:MaxNewSize=64M两个参数,此值一旦设置则–XX:NewRatio无效。
-XX:SurvivorRatio=8 :设置Eden区与两个S区之和的比例
-verbose:gc :输出每次GC的相关情况
-Xloggc :…/logs/gc.log 日志文件的输出路径
-XX:+PrintGC :输出GC日志
-XX:+PrintGCDetails :输出GC的详细日志
-XX:+PrintGCTimeStamps :输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps :输出GC的时间戳(以日期的形式,如 2019-11-20T21:53:59.234+0800)
-XX:+PrintHeapAtGC :在进行GC的前后打印出堆的信息
新对象在Eden区创建,Eden区空间不足触发YGC。Eden区空间还是不足将对象创建在Old区。
新对象大小超过-XX:PretenureSizeThreshold参数(默认值是0)大小直接创建在Old区。
进行YGC时存活的对象在S1(To Space)区中存不下,那么把存活的对象转移到Old区。
进行YGC时存活的对象gc次数超过-XX:MaxTenuringThreshold(默认值15)的值,那么把存活的对象转移到Old区。
Old区内存不足时触发FGC,如果空间还不够,则JVM抛出OutOfMemoryError。
借助JDK自带命令行jstat -gc pid,显示的大小为KB。
字段含义为C:总容量,U:已使用容量,S0,S1:survivor区,E:Eden区,O:Old区,M:元空间,CC:CompressedClassSpaceSize,YGC:young区回收次数,YGCT:YGC的总时间,FGC:Old区回收次数,FGCT:FGC的总时间,GTC:YGCT+FGCT。
如 jstat -gc11925
也可使用jmap -heap pid 命令显示堆内存状况。
打印GC日志的参数:-verbose:gc -Xloggc:gc.log -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
其中gc.log可设置成实际路径。
目标:只设置堆区的总内存,JVM各区使用默认比例
参数:-Xms128M -Xmx128M
运行应用程序一段时间后,多次采样数据:
gstat -gc 11925
jmap -heap 11925
可以看到 2次FGC,470次YGC,GC频率还过的去。分析数据后发现两个问题:一、Old区利用率非常低只占用17%,128M内存还是有点浪费;二、Eden区利用率很高,自然的YGC的次数也确实高了点。
先缩小总内存,基于以上数据计算应用所需总内存=28M+14M,即42M。但To Space会浪费2.5%(1/40)的内存,极限堆区内存=42/(1-2.5%)=44.21M,取个整数用64M应该能满足。即参数:-Xms64M -Xmx64M。64M约是1.5倍的内存。不放心可以用2倍内存89M,取个整数用:-Xms96M -Xmx96M。
使用极限内存大小可满足应用的基本的运行,不过,可预知的结果是FGC的频率会非常高。
备注:以上的算法将Eden+S0+Old作为堆内存的可用空间,考虑一个极端情况,某次YGC后恰巧把所有Eden区的对象转移到了Old区。所以保险点极限内存应以Old区为基准,则上面的例子中应保证Old区大小不低于42M,极限堆区内存=42/(2/3)=63M。
目标:减少YGC的频率
参数:-Xms128M -Xmx128M -Xmn64M
以前面实例,统计数据中应用程序产生的对象占用内存42M,而New区的容量是38M,堆总内存128M,理论闲置80M空间,所以将New区内存超过42M也不为过。
修改之后,实测jstat -gc 14854,感觉差异不大。
目标:减少YGC时因Survior空间不足直接转移到Old区
参数:-Xms128M -Xmx128M -Xmn64M-XX:SurvivorRatio=4
Eden与Survior是个矛盾,Eden与Survior的比例较大时导致YGC时因S1(To)区空间不足直接把数据转移到Old区;比例较小时S1空间浪费较大。
常说二八定律,但-XX:SurvivorRatio=8 中的8是Eden区与两个S区之和的比例,不防将Eden与S0区的比例设置成8,也即由8:1:1改成8:2:2,对应-XX:SurvivorRatio=4。推荐值在4到8之间。