问题产生
下午对系统进行例行检查,发现项目的CPU的曲线不正常,经常会出现一个小高峰,但是在当时的流量并没有明显的变化,由此展开了今日的排查
JVM堆
查看堆的使用情况,在最近的15分钟之内,JVM内存的变化不规则,没有呈现出锯齿状。
接着查看GC的情况
其实问题已经非常的明显了,我们解析一下上面的这张图。
做不的1分钟内垃圾回收次数
, 可以看到一分钟内YGC的次数最高达到了70
次左右, 同时还有伴有1.3次Full GC (Ergonomics),
Ergonomics翻译成中文,一般都是“人体工程学”。在JVM中的垃圾收集器中的Ergonomics就是负责自动的调解gc暂停时间和吞吐量之间的平衡,然后你的虚拟机性能更好的一种做法。
再看暂停时长,已经达到了恐怖的 1.7s
, 这个已经严重影响了系统的正常运行,然后从右边的那个小图上看, 内存分配的情况, 年轻代的要求分配内存达到了500M
, 从第一张图JVM的内存结构上看,整个年轻代的内存在623M
, 在这种情况下,每次YGC之后都分配不了这么多内存,所以只能一部分大对象往老年代塞,其他的继续YGC,然后看内存是否可以够用,如此往复,整个系统的CPU和负载一瞬间全部上去了。
通过查看接口监控,发现这这个时间段有一个导出Excel 的功能在执行
通过查看代码分析,该接口会在循环里面构建大量的对象,这些对象都是朝生夕死的。 除非一些大的对象,很少一部分才会进入老年代。
基于这种场景,作出如下调整, 由于该项目目前是一个非常大的单体应用,2个G的内存够呛,年轻代600多M在出现一些稍微耗费内存的情况就支撑不了。
因此堆内存提高到4G
, 4G的堆内存,垃圾回收器采取G1回收器,G1回收器网上有很多介绍,
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
他废除了年轻代和老年代的空间区分,取而代之的是一个一个的Region
, 默认一个Region
的大小是2M, 如果我设置了4G的内存之后,那么默认就是 (1024*4)/2 = 2048个regions , 鉴于本服务存在很多文件读取解析,会有大对象的产生,减少进入H区的对象, 所以Region的大小设置为4M。
G1回收器有个非常好的特性就是会不断的帮助JVM调整策略, 会根据实际的GC情况调整年轻代和老年代的比例大小,默认情况下,年轻代最多可以占用60%的堆内存。这其实就是GC的灵活性。
G1的另一个显著特点他能够让用户设置应用的暂停时间,通过参数:-XX:MaxGCPauseMillis
来指定,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。
(这段话网上摘录,据说是阿里的面试题
)
通过上面的分析,最把启动命令改成下面这样
nohup java -jar -Denv=pro -Dserver.port=8080 -Xms4g -Xmx4g -XX:+UseG1GC -XX:G1HeapRegionSize=4m -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError ./x x x.jar &
使用命令查看堆内存设置,发现一切生效。
[root@localhost ~]# jmap -heap 10181
Attaching to process ID 10181, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.231-b11
using thread-local object allocation.
Garbage-First (G1) GC with 2 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 4294967296 (4096.0MB) # 最大堆内存
NewSize = 1363144 (1.2999954223632812MB)
MaxNewSize = 2575302656 (2456.0MB)
OldSize = 5452592 (5.1999969482421875MB)
NewRatio = 2 # 新生代和老年代的比例, 就是说老年代和新生代的比例是2:1
SurvivorRatio = 8 # 新生代比例
MetaspaceSize = 21807104 (20.796875MB) # 元空间
CompressedClassSpaceSize = 1073741824 (1024.0MB) # 对象压缩指针空间
MaxMetaspaceSize = 17592186044415 MB # 最大元空间,受限于物理机的内存
G1HeapRegionSize = 4194304 (4.0MB) # 每个Region的大小
Heap Usage:
G1 Heap:
regions = 1024 # 总的Region数量
capacity = 4294967296 (4096.0MB) # 总内存
used = 1047525872 (998.9985198974609MB) # 已使用
free = 3247441424 (3097.001480102539MB) #剩余
24.389612302184105% used
G1 Young Generation:
Eden Space:
regions = 221 # Eden区使用的region的数量
capacity = 2185232384 (2084.0MB) # Eden总内存
used = 926941184 (884.0MB) # Eden已使用
free = 1258291200 (1200.0MB) #Eden剩余
42.41842610364683% used
Survivor Space:
regions = 8 # Survivor所占的Region的数量
capacity = 33554432 (32.0MB)
used = 33554432 (32.0MB)
free = 0 (0.0MB)
100.0% used
G1 Old Generation:
regions = 21 # 老年代使用的Region
capacity = 2076180480 (1980.0MB) # 老年代大小
used = 87030256 (82.99851989746094MB)
free = 1989150224 (1897.001480102539MB)
4.1918444392657035% used
项目上线后,在同样执行文件导出的接口(该接口执行了一分钟),整体的GC情况正常,CPU正常。
后续继续观察系统的运行情况,有问题再做调整吧!
sharedCode源码交流群,欢迎喜欢阅读源码的朋友加群,添加下面的微信, 备注”加群“ 。