先举荐一篇好文
最近写了几篇对于GC的文章,次要是因为线上有一些对于GC的问题,所以想顺便总结一波,梳理一下GC的一些知识点和排查思路。
之前有读者留言说能不能写一篇实战经验方面的,这不就来了吗~
咱们我的项目上用到的次要还是CMS + ParNew的组合。所以重点看的材料也是这方面的。
在学习的过程中,也拜读了美团技术团队的这篇文章《Java中9种常见的CMS GC问题剖析与解决》。这篇文章品质十分高,从理论知识,源码剖析,到常见的GC问题案例,囊括了剖析门路、根因、调优策略等等内容,十分详尽且全面,尤其是最初局部的解决流程SOP和根因鱼骨图,十分nice。墙裂举荐,值得一读!!!
晓得大家喜爱现成的,所以我手动copy了这两张图过去,有须要的自取:
GC问题案例
我遇到的案例可能没有下面文章作者那么丰盛,但也是实在遇到的几个案例,所以借这篇文章分享进去,大家能够参考参考,防止踩相似的坑。
案例1 Young GC频繁
之前有个工作会频繁地反复调用一个接口。所以用guava cache做了一个简略的内存缓存。后果上线后发现常常收到Young GC频繁的告警,工夫跟这个工作的启动工夫也比拟吻合。
通过监控看到的GC图大略是这样:
能够看到,Young GC的次数会在某一个工夫点飙升。同时随同着Old区域内存疾速升高,最初会触发一次Full GC。
依据这个状况,能够必定的是因为本次代码改变引起的。通过Heap Dump剖析后发现,占用内存较大的是一个guava cache的Map对象。
查找代码发现,应用guava cache的时候,没有设置最大缓存数量和弱援用,而是设置了一个几分钟的过期工夫。而这个工作的量又比拟大,到线上后很快就缓存了大量的对象,导致频繁触发Young GC,但又因为有援用GC不掉(这个从Survivor区的内存大小图像能够揣测),所以缓缓代数比拟多的对象就降职到了老年代,前面老年代内存达到肯定阈值引发Full GC。
前面通过设置最大缓存数量解决了这个问题。又积攒了一个贵重的教训,完满!
案例2 Young GC和Old GC都频繁
在线上灰度环境中发现收到Young GC和Old GC频繁的告警。监控看到的GC图大略长这样:
依据GC图大略能够看进去,Young GC和Old GC都十分频繁,且每次都能回收走大量的对象。那能够简略地揣测:的确是产生了大量的对象,且极有可能有一部分大对象。小对象引发的Young GC频繁,而大对象引发了Old GC频繁。
排查下来也是一段代码引起的。对于一个查问和排序分页的SQL,同时这个SQL须要join多张表,在分库分表下,间接调用SQL性能很差,甚至超时。于是想了个比拟low的方法:查单表,把所有数据查出来,在内存排序分页。用了一个List来保留数据,而有些数据量大,造成了这个景象。用Heap Dump剖析,也印证了这个猜想,List类型的对象占用了大量的空间。
案例3 接口线程池满和Full GC
这是一个报接口线程池满的问题。但每次都会在同一时间Full GC。监控图大略长这样:
从工夫上来看,先是Java线程数量飙升,而后触发Full GC。前面重启后,Java线程数量恢复正常水位。
这里波及到一个冷常识:一个Java线程默认会占用多少内存?
这个参数能够管制: -XX:ThreadStackSize。在64 位的Linux下, 默认是1 MB(这个说法也不全对,还是取决于栈深度)。Java 11对这个有一个优化,能够缩小内存占用。详情能够参考这篇文章:https://dzone.com/articles/ho...
排查下来根因是这个利用还是应用的Log4j 1,而Log4j 1有性能问题,在高并发下,上面这段代码的同步块可能会引起大量线程阻塞:
void callAppenders(LoggingEvent event) { int writes = 0; for(Category c = this; c != null; c=c.parent) { // Protected against simultaneous call to addAppender, removeAppender,... synchronized(c) { if(c.aai != null) { writes += c.aai.appendLoopOnAppenders(event); } if(!c.additive) { break; } } } if(writes == 0) { repository.emitNoAppenderWarning(this); }}
解决办法就是缩小日志打印,降级日志框架到Log4j 2或者Logback。
案例4 利用启动时Full GC频繁
这个是较早的一个案例了,GC图曾经找不到了。
但引发Full GC频繁的大略就这几种可能性:
- 调用System.gc()
- Old区空间有余
- 永恒代/元空间满
依据代码和GC图排除了后面两种可能性,那就是元空间满了。在Java 8中,XX:MaxMetaspaceSize
是没有下限的,最大容量与机器的内存无关;然而XX:MetaspaceSize
是有一个默认值的:21M。而如果利用须要进元空间的对象较多(比方有大量代码),就会频繁触发Full GC。解决办法是能够通过JVM参数指定元空间大小:-XX:MetaspaceSize=128M
。
总结
能够看到下面的大多数案例都是代码改变或者利用框架引起的。个别公司内都会有默认的一套JVM参数,真正须要JVM参数调优解决问题的case其实是比拟少的。
而且有时候GC问题可能并不是问题的本源,有可能是其它问题引发的GC问题,在理论排查的时候要依据日志及工夫线去判断。
至于怎么去判断是否“频繁”和“耗时长”?尽管有一些公式计算,但我感觉只有不影响现有的业务,那就是能够承受的。而如果某个利用的GC体现显著不同于以往的平均水平,或者其它类似利用的程度,那就有理由狐疑是不是存在GC问题了。
很多时候GC问题不充沛的压测是测试不进去的,而压测老本又比拟大。常常会在线上灰度公布中察看到,所以须要在灰度公布的时候亲密察看零碎的监控和告警。如果有条件,能够上红蓝部署,升高危险。
GC问题还是蛮简单的,须要大量的教训和理论知识。遇到之后能够总结剖析一下根因,平时也能够看看其他人的博客,吸取经验,这样就能不断完善本人的常识体系。
求个反对
我是Yasin,一个保持技术原创的博主,我的微信公众号是:编了个程
都看到这儿了,如果感觉我的文章写得还行,无妨反对一下。
文章会首发到公众号,浏览体验最佳,欢送大家关注。
你的每一个转发、关注、点赞、评论都是对我最大的反对!
还有学习资源、和一线互联网公司内推哦
求个反对
我是Yasin,一个保持技术原创的博主,我的微信公众号是:编了个程
都看到这儿了,如果感觉我的文章写得还行,无妨反对一下。
文章会首发到公众号,浏览体验最佳,欢送大家关注。
你的每一个转发、关注、点赞、评论都是对我最大的反对!
还有学习资源、和一线互联网公司内推哦