今天在开发时,有个场景用到了大对象(Map常量)用于本地缓存,突然想看看应用的内存与垃圾回收情况。便决定从开发过程入手,详细分析一下应用的JVM情况。在查看过程中果然发现默认配置在启动过程中就出现过几次FullGC,所以开始着手调优。
本文环境
- Java8
- SpringBoot应用
- IDEA
使用默认配置情况启动应用
查看本次启动的默认配置
应用启动成功后,切换到terminals,使用jps -l
列出当前运行的java应用,SpringBoot应用会很直观的显示执行的入口类,如下所示:
146916 org.jetbrains.jps.cmdline.Launcher
148532 完整应用名
133880 org.jetbrains.jps.cmdline.Launcher
149708 sun.tools.jps.Jps
找对对应应用的pid后,可以使用jmap工具或者jconsole工具
查看应用的具体情况,前者是命令行工具,方便快捷;后者为可视化工具,比较立体直观。
使用jdk自带工具分析
jmap -heap pid
: 查看java进程的堆栈信息,结果如下
Client compiler detected. #使用client模式运行
JVM version is 25.144-b01
using thread-local object allocation. #使用本地线程分配缓冲TLAB分配对象
Mark Sweep Compact GC #使用标记-整理算法(-XX:UseSerialGC使用老年代垃圾收集算法,也就是说默认使用SerialNew+SerialOld的单线程收集器组合)
Heap Configuration:
MinHeapFreeRatio = 40 #堆可用空间小于40%开始堆动态扩容
MaxHeapFreeRatio = 70 #堆可用空间大于70%开始缩容
MaxHeapSize = 268435456 (256.0MB) #最大堆内存
NewSize = 5570560 (5.3125MB) #最小新生代内存
MaxNewSize = 89456640 (85.3125MB) #最大新生代内存
OldSize = 11206656 (10.6875MB) #老年代内存
NewRatio = 2 #新生代与老年代内存比例1:2
SurvivorRatio = 8 #Survivor与EdenSpace空间比例为1:1:8(因为有两个survivor区)
MetaspaceSize = 12582912 (12.0MB) #堆外内存元空间大小,java去永久带后引入,使用计算机直接内存
CompressedClassSpaceSize = 1073741824 (1024.0MB) #类指针压缩空间大小
MaxMetaspaceSize = 4294901760 (4095.9375MB)#最大堆外内存使用量
G1HeapRegionSize = 0 (0.0MB) #G1区块大小,因为没有使用G1收集器,为0
Heap Usage:
New Generation (Eden + 1 Survivor Space):#新生代空间=Eden+1个Survivor空间,因为新生代使用复制算法,且90%对象朝生夕死的特性,只保留一个备用的复制区域,所以新生代可用空间实际上比配置要小一个Survivor空间大小
capacity = 19333120 (18.4375MB)
used = 17121152 (16.3280029296875MB)
free = 2211968 (2.1094970703125MB)
88.55865995762711% used
Eden Space:
capacity = 17235968 (16.4375MB)
used = 17121152 (16.3280029296875MB)
free = 114816 (0.1094970703125MB)
99.33385812737643% used
From Space:
capacity = 2097152 (2.0MB)
used = 0 (0.0MB)
free = 2097152 (2.0MB)
0.0% used
To Space:
capacity = 2097152 (2.0MB)
used = 0 (0.0MB)
free = 2097152 (2.0MB)
0.0% used
tenured generation: #老年代
capacity = 42754048 (40.7734375MB)
used = 20843832 (19.87822723388672MB)
free = 21910216 (20.89521026611328MB)
48.752885340822% used
18522 interned Strings occupying 1603384 bytes.
因为在java8,jvm已经不存在永久带的概念,取而代之的是使用机器直接内存的Metaspace
。
jstat -gc 148532
: 统计应用的gc情况
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
2240.0 2240.0 2240.0 0.0 18304.0 6399.6 45472.0 27798.7 27776.0 27280.7 0.0 0.0 160 0.424 4 0.178 0.601
- S0C:Survivor0大小
- S0U:Survivor0使用量
- S1C:Survivor1大小
- S1U:Survivor0使用量
- EC:Eden区大小
- EU:Eden区使用量
- OC:老年代区域大小
- OU:老年代使用量
- MC:Metaspace大小
- MU:Metaspace使用量
- CCSC:压缩区域大小
- CCSU:压缩区域使用量
- YGC:新生代区域GC次数
- YGCT:YoungGC总耗时
- FGC:FullGC次数
- FGCT:FullGC总耗时
- GCT:GC总耗时
从上面统计信息可以看到整个应用从启动过程就有4次FullGC,Metaspace使用量已经非常接近阙值,下面我们试着来优化一下当前应用。
自定义启动参数
-server #调整为使用server模式运行
-Xmx256M #最大堆
-Xms256M #最小堆 = Xmx表示使用固定堆
-XX:NewRatio=2 #新生代与老年代空间比=1:2
-XX:SurvivorRatio=8 #单个Survivor与Eden空间比=1:8
-XX:MetaspaceSize=128M #元空间初始化大小=128M
-Xloggc:./gc.log #gc过程记录日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+HeapDumpOnOutOfMemoryError #如果出现OOM异常打印dump日志
-XX:HeapDumpPath=./dump.dump
使用上述参数启动应用后,应用堆信息:
Server compiler detected. #server模式运行
JVM version is 25.144-b01
using thread-local object allocation. #TLAB分配方案
Parallel GC with 4 thread(s) #Parallel Old并发收集器,使用4条线程进行gc
Heap Configuration:
MinHeapFreeRatio = 0 #固定堆不进行动态扩容
MaxHeapFreeRatio = 100
MaxHeapSize = 268435456 (256.0MB)
NewSize = 89391104 (85.25MB)
MaxNewSize = 89391104 (85.25MB)
OldSize = 179044352 (170.75MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 134217728 (128.0MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 4294901760 (4095.9375MB)
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 71565312 (68.25MB)
used = 51179296 (48.808380126953125MB)
free = 20386016 (19.441619873046875MB)
71.5141100761218% used
From Space:
capacity = 8912896 (8.5MB)
used = 2656816 (2.5337371826171875MB)
free = 6256080 (5.9662628173828125MB)
29.808672736672793% used
To Space:
capacity = 8912896 (8.5MB)
used = 0 (0.0MB)
free = 8912896 (8.5MB)
0.0% used
PS Old Generation
capacity = 179044352 (170.75MB)
used = 35128952 (33.50157928466797MB)
free = 143915400 (137.24842071533203MB)
19.620251411225752% used
21588 interned Strings occupying 1818848 bytes.
再看看gc情况:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
8704.0 8704.0 2555.0 0.0 69888.0 50619.8 174848.0 34201.8 26240.0 25782.0 0.0 0.0 18 0.118 0 0.000 0.118
经过调整后,FullGC消失,但是我们同时修改了堆大小配置与Metaspace配置,无法判断之前的FullGC主要是由什么原因导致的。
可以按上面的配置,一次只修改堆空间或Metaspace测试,得到的结果是修改单个配置之后还是会有FullGC发生,表明本次的Full GC是由于两者共同引起的,老年代空间不足导致的FullGC在gc日志上没有体现,Metaspace空间不足导致的FullGC在日志中体现如下:
2018-06-07T16:55:35.503+0800: 9.713: [Full GC (Metadata GC Threshold) [PSYoungGen: 1942K->0K(78592K)] [ParOldGen: 31564K->22486K(174848K)] 33507K->22486K(253440K), [Metaspace: 26848K->26848K(28032K)], 0.0870488 secs] [Times: user=0.20 sys=0.00, real=0.09 secs]
结论
- JVM默认是以client模式运行,client模式的默认收集器组合为:
SerialNew+SerialOld
的单线程收集器组合,老年代使用标记整理算法 - server模式默认使用的是ps收集器组合:
Parallel Scaverge+Paralle Old
的收集器组合,在打印出来的gc日志也可以印证:-XX:+UseParallelGC
- JVM调优需要结合应用的实际情况,没有最优配置,最好经过多次、多种情况下的充分测试,还需要在应用实际运行过程中找到应用的最佳JVM内存配置
- java8已经不存在永久带,取而代之的是使用直接内存的Metaspace,存在默认值=20M左右