最近写了几篇关于GC的文章,主要是由于线上有一些关于GC的问题,因此想顺便总结一波,梳理一下GC的一些知识点和排查思路。html
以前有读者留言说能不能写一篇实战经验方面的,这不就来了吗~java
咱们项目上用到的主要仍是CMS + ParNew的组合。因此重点看的资料也是这方面的。缓存
在学习的过程当中,也拜读了美团技术团队的这篇文章《Java中9种常见的CMS GC问题分析与解决》。这篇文章质量很是高,从理论知识,源码分析,到常见的GC问题案例,囊括了分析路径、根因、调优策略等等内容,很是详尽且全面,尤为是最后部分的处理流程SOP和根因鱼骨图,很是nice。墙裂推荐,值得一读!!!微信
知道你们喜欢现成的,因此我手动copy了这两张图过来,有须要的自取:并发
我遇到的案例可能没有上面文章做者那么丰富,但也是真实遇到的几个案例,因此借这篇文章分享出来,你们能够参考参考,避免踩相似的坑。app
以前有个任务会频繁地重复调用一个接口。因此用guava cache作了一个简单的内存缓存。结果上线后发现常常收到Young GC频繁的告警,时间跟这个任务的启动时间也比较吻合。框架
经过监控看到的GC图大概是这样:高并发
能够看到,Young GC的次数会在某一个时间点飙升。同时伴随着Old区域内存快速升高,最后会触发一次Full GC。oop
根据这个状况,能够确定的是因为本次代码改动引发的。经过Heap Dump分析后发现,占用内存较大的是一个guava cache的Map对象。源码分析
查找代码发现,使用guava cache的时候,没有设置最大缓存数量和弱引用,而是设置了一个几分钟的过时时间。而这个任务的量又比较大,到线上后很快就缓存了大量的对象,致使频繁触发Young GC,但又因为有引用GC不掉(这个从Survivor区的内存大小图像能够推测),因此慢慢代数比较多的对象就晋升到了老年代,后面老年代内存到达必定阈值引起Full GC。
后面经过设置最大缓存数量解决了这个问题。又积累了一个宝贵的经验,完美!
在线上灰度环境中发现收到Young GC和Old GC频繁的告警。监控看到的GC图大概长这样:
根据GC图大概能够看出来,Young GC和Old GC都很是频繁,且每次都能回收走大量的对象。那能够简单地推测:确实是产生了大量的对象,且极有可能有一部分大对象。小对象引起的Young GC频繁,而大对象引起了Old GC频繁。
排查下来也是一段代码引发的。对于一个查询和排序分页的SQL,同时这个SQL须要join多张表,在分库分表下,直接调用SQL性能不好,甚至超时。因而想了个比较low的办法:查单表,把全部数据查出来,在内存排序分页。用了一个List来保存数据,而有些数据量大,形成了这个现象。用Heap Dump分析,也印证了这个猜想,List类型的对象占用了大量的空间。
这是一个报接口线程池满的问题。但每次都会在同一时间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。
这个是较早的一个案例了,GC图已经找不到了。
但引起Full GC频繁的大概就这几种可能性:
根据代码和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,一个坚持技术原创的博主,个人微信公众号是:编了个程
都看到这儿了,若是以为个人文章写得还行,不妨支持一下。
文章会首发到公众号,阅读体验最佳,欢迎你们关注。
你的每个转发、关注、点赞、评论都是对我最大的支持!
还有学习资源、和一线互联网公司内推哦