JVM-Java GC分析
如何获取JavaGC日志
用动态命令查看:
jstat -gc 1262 2000 20 每隔20秒输入一次日志,总共输入20次
设置GC参数打印出日志
-
-XX:+PrintGC
输出GC日志 -
-XX:+PrintGCDetails
输出GC的详细日志 -
-XX:+PrintGCTimeStamps
输出GC的时间戳(以基准时间的形式) -
-XX:+PrintGCDateStamps
输出GC的时间戳(以日期的形式,如 2017-09-04T21:53:59.234+0800) -
-XX:+PrintHeapAtGC
在进行GC的前后打印出堆的信息 -
-Xloggc:../logs/gc.log
日志文件的输出路径
Tomcat设置示例:
JAVA_OPTS
=
"-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4
-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log
-Djava.awt.headless=true
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails
-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000
-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15"
根据上面的参数我们来做一下解析:
-Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m
Xms,即为jvm启动时得JVM初始堆大小,Xmx为jvm的最大堆大小,xmn为新生代的大小,permsize为永久代的初始大小,MaxPermSize为永久代的最大空间。
-XX:SurvivorRatio=4
SurvivorRatio为新生代空间中的Eden区和Survivor区的大小比值,默认是32,也就是说Eden区是 Survivor区的32倍大小,要注意Survivo是有两个区的,因此Surivivor其实占整个young genertation的1/34。
调小这个参数将增大survivor区,让对象尽量在survitor区呆长一点,减少进入年
老代的对象。去掉救助空间的想法是让大部分不能马上回收的数据尽快进入年老代,加快年老代的回收频率,减少年老代暴涨的可能性,这个是通过将-XX:SurvivorRatio 设置成比较大的值(比如65536)来做到。
-verbose:gc-Xloggc:$CATALINA_HOME/logs/gc.log
将虚拟机每次垃圾回收的信息写到日志文件中,文件名由file指定,文件格式是*文件,内容和-verbose:gc输出内容相同。
-Djava.awt.headless=true
Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标。
-XX:+PrintGCTimeStamps-XX:+PrintGCDetails
设置gc日志的格式
-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000
指定rmi调用时gc的时间间隔
-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15
采用并发gc方式,经过15次minor gc 后进入年老代
如何分析GC日志
Young GC回收日志:
2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]
Full GC回收日志:
2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]
通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中PSYoungGen表示gc回收前后年轻代的内存变化;ParOldGen表示gc回收前后老年代的内存变化;PSPermGen表示gc回收前后永久区的内存变化。
young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少full gc的次数。
通过两张图非常明显看出gc日志构成:
Young GC日志:
Full GC日志:
工具:
jconsole
VisualVM
GChisto
gcviewer
GC Easy
案例分析:
JDK版本:jdk_1.8.0_66
JVM参数:
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=4 -XX:+UseAdaptiveSizePolicy -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=1073741824 -XX:NewSize=1073741824 -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:+PrintGCTimeStamps
症状:
※ gc throughput percent逐步下降,从一般的99.96%逐步下降,跌破99%,进入98%,最低点能到94%
※ young gc time逐步增加,从一般的十几毫秒逐步上升,突破50,再突破100,150,200,250
※ 在8.5天的时间内,发生了9000多次gc,其中full gc为4次,*均将*8秒,大部分是young gc(allocation failure为主),*均270多毫秒,最大值将*7秒*均对象创建速率为10.63 mb/sec,
*均的晋升速率为2 kb/sec,cpu使用率正常,没有明显的飙升
Full GC
27.066: [Full GC (Metadata GC Threshold) [PSYoungGen: 19211K->0K(917504K)] [ParOldGen: 80K->18440K(1048576K)] 19291K->18440K(1966080K), [Metaspace: 20943K->20943K(1069056K)], 0.5005658 secs] [Times: user=0.24 sys=0.01, real=0.50 secs]
100.675: [Full GC (Metadata GC Threshold) [PSYoungGen: 14699K->0K(917504K)] [ParOldGen: 18464K->23826K(1048576K)] 33164K->23826K(1966080K), [Metaspace: 34777K->34777K(1081344K)], 0.7937738 secs] [Times: user=0.37 sys=0.01, real=0.79 secs]
195.073: [Full GC (Metadata GC Threshold) [PSYoungGen: 24843K->0K(1022464K)] [ParOldGen: 30048K->44782K(1048576K)] 54892K->44782K(2071040K), [Metaspace: 58220K->58220K(1101824K)], 3.7936515 secs] [Times: user=1.86 sys=0.02, real=3.79 secs]
242605.669: [Full GC (Ergonomics) [PSYoungGen: 67276K->0K(882688K)] [ParOldGen: 1042358K->117634K(1048576K)] 1109635K->117634K(1931264K), [Metaspace: 91365K->90958K(1132544K)], 22.1573804 secs] [Times: user=2.50 sys=3.51, real=22.16 secs]
从GC日志中看到,前三个Full GC是Metadata GC Threshold引起的,所以我们关注一下这个区域的参数。
jstat -gcmetacapacity 7
MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC FGCT GCT
0.0 1136640.0 99456.0 0.0 1048576.0 12160.0 38009 16 275.801 14361.992
可以看到MCMX(Maximum metaspace capacity(kb))有一个多G,而MC(Metaspace capacity(kb))才97m,为啥会引起Full GC? 我们的虚拟机参数中没有设置metaspace相关的参数,所以虚拟的默认值为
java -XX:+PrintFlagsFinal | grep Meta
uintx InitialBootClassLoaderMetaspaceSize = 4194304 {product}
uintx MaxMetaspaceExpansion = 5451776 {product}
uintx MaxMetaspaceFreeRatio = 70 {product}
uintx MaxMetaspaceSize = 18446744073709547520 {product}
uintx MetaspaceSize = 21807104 {pd product}
uintx MinMetaspaceExpansion = 339968 {product}
uintx MinMetaspaceFreeRatio = 40 {product}
bool TraceMetadataHumongousAllocation = false {product}
bool UseLargePagesInMetaspace = false {product}
可以看到MinMetaspaceFreeRatio为40,MaxMetaspaceFreeRatio为70,MetaspaceSize为20M,Full GC (Metadata GC Threshold)主要分为了三次
第一次,[Metaspace: 20943K->20943K(1069056K)]
第二次,[Metaspace: 34777K->34777K(1081344K)]
第三次,[Metaspace: 58220K->58220K(1101824K)]
可以看到metaspace的阈值不断动态调整,至于具体调整的逻辑,官方文档貌似没讲,这里暂时不深究。只要没有超过Max值就没有致命影响,但是对于低延时的应用来讲,是要尽量避免动态调整引起的gc耗时,
可以根据调优计算并设置初始阈值来解决。
Full GC(Metadata GC Threshold)
从GC日志中看到,老年代离最大的空间还很远,Metaspace并没有真正的释放空间,所以怀疑Metaspace空间不够用了。所以在JVM的启动参数加上Metaspace相关的参数 -XX:MetaspaceSize=128M
Full GC (Ergonomics)
这里可以看到full gc的reason是Ergonomics,是因为开启了UseAdaptiveSizePolicy,jvm自己进行自适应调整引发的full gc
GC (Allocation Failure)
分析完full gc之后我们看下young gc,看log里头99%都是GC (Allocation Failure)造成的young gc。Allocation Failure表示向young generation(eden)给新对象申请空间,但是young generation(eden)剩余的合适
空间不够所需的大小导致的minor gc。
参考:
【1】个人博客,纯洁的微笑,http://www.ityouknow.com/jvm/2017/09/18/GC-Analysis.html
【2】博客,https://my.oschina.net/sunnywu/blog/332870
【3】个人博客,纯洁的微笑,http://www.ityouknow.com/java/2017/02/22/jvm-tool.html
【4】博客,https://blog.csdn.net/liubenlong007/article/details/78143285