关于java:你们要的线上GC问题案例来啦

43次阅读

共计 2824 个字符,预计需要花费 8 分钟才能阅读完成。

先举荐一篇好文

最近写了几篇对于 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,一个保持技术原创的博主,我的微信公众号是:编了个程

都看到这儿了,如果感觉我的文章写得还行,无妨反对一下。

文章会首发到公众号,浏览体验最佳,欢送大家关注。

你的每一个转发、关注、点赞、评论都是对我最大的反对!

还有学习资源、和一线互联网公司内推哦

正文完
 0