问题反馈
最近生产一台专门用于部署下载文件的服务运维反馈报垃圾回收频繁
情况描述
这种告警其实没有引起服务宕机,但是如果频繁的GC会影响服务接收请求的效率,减少服务可用的时间,进而影响整个服务的吞吐率。如果放任不管,也可能引起OOM。
排查思路
-
从告警信息"PSMarkSweep"得知垃圾清理使用的是"标记-清除"算法,之前学习JVM知道标记清除一般是用在老年代的,而老年代的垃圾回收一般都是和新生代一块回收的,这种回收会导致服务暂时无法使用也就是通常所说的STW。所以应该排查为什么内存这么容易就满了。
-
先看下在垃圾回收前,都有哪些请求会占用过多的资源,进行复现。
-
排查记录我看到,在这之前有用户频繁的在调用下载接口,所以可以得知肯定是用户调用才会引发GC,而且用户下载的都是EXCEL文件。那么就可以得知我们该如何复现场景了。
-
复现场景前,我们需要将垃圾回收日志打印出来。我在本地IDEA设置一下GC日志参数,看看垃圾回收的情况。
-
前期猜测是因为我们设置的堆内存大小和堆内存的分配策略有问题,于是我在本次进行项目启动,并打印垃圾回收的日志,打印出来出乎意料,几乎每秒都在执行垃圾回收。从日志可以看出来前期是因为初始化堆内存容量过小一直在扩容和垃圾回收。
-
打开jconsole观察垃圾回收的时间,确实报出来回收比较频繁,我的项目刚启动不到一个小时,光垃圾回收就用了19分钟。我决定先将堆内存的初始化大小设置大一点看看。
-
那么我的初始化容量大概是多少呢,查阅java官方(The Parallel Collector)得知
-
物理内存<=192MB的话JVM最大内存是物理内存的一半,否则占用物理内存的四分之一。
-
最大值、最小值、比例:
a) 在32位JVM上,如果有4 GB或更多的物理内存,则默认的最大堆大小最多可以为1 GB。
b) 在64位JVM上,如果有128GB或更多的物理内存,则默认的最大堆大小最大为32 GB。
c) 在JVM初始化期间分配了一个较小的值,称为初始堆大小。此数量至少为8 MB,否则为物理内存的1/64,最大为1 GB。
-
-
分配给年轻代的最大空间量是堆总大小的三分之一,即年轻代和老年代默认的比例是1:2
-
观察垃圾回收日志,可知初始化和最大堆内存的大小(我的物理机器内存是32G,计算后初始化堆大小是32G/64=512M,最大堆内存是32G/4=8G)
-
上述可知,我初始化新生代是153088大概是150M,文件生成服务本身会将文件流读取到内存中,所以初始化内存不能过小,不然会导致频繁的JVM内存扩容。从而增加FULLGC的时间和扩容时间
-
故而我尝试将堆内存容量增大
-
继续重启后复现下载场景,重新观察GC日志
-
显然刚开始堆内存分配不足导致GC的日志没有了,继续观察,都是因为元空间内存不足导致的GC,先尝试把Metadata的空间设置大点看看。
-
设置完元空间的初始化内存后,继续重启观察GC日志,发现启动后只有一次新生代的GC,后面又频繁调用GC,我仔细观察了下GC的原因,这不是函数调用引起的么。谁没事干会手动调用垃圾回收啊,于是我在idea对System.gc()这个函数进行断点,发现了端倪。
-
观察调用链,发现jxl包在生成excel的时候后,每次关闭流的时候竟然会手动调用gc,我靠瞬间悟了。
-
继续观察发现有一个配置项可以设置disabled的状态,从而控制每次生成完excel关流时是否需要进行垃圾回收。
-
这个值可以通过JVM启动参数或者环境变量设置,这不找到原因了么,我赶紧加个配置试试
-
再次复现频繁下载场景,观察GC日志,已经只有少数新生代的GC信息了。
-
最后再观察下,垃圾回收时间明显减少了。到这基本上可以收工了。以后可以再探讨下,新生代回收频繁的问题,还有元空间之前设置的1024m是否合适,因为元空间毕竟只会初始化一些项目中的类信息和代码缓存等一些辅助信息。
小总结下
-
频繁发生GC要先看下GC之前用户的行为,然后想办法进行复现
-
排查问题时要善用GC日志
-
如果项目初始化后内存不断扩大,可以考虑提升初始化的堆容量和元空间的容量
-
观察GC日志中是否有手动调用GC,利用断点进行分析源头对其进行处理