最近收到一个游戏服务器因为GC导致大量玩家掉线的问题,让我看看,并发给我一个JMC的飞行记录和堆内存的hprof 堆转储文件。我分别用JDK中的jmc和jvisualvm打开进行分(瞎J)析(8看)。先看看基本信息。
基本信息:
生成的日期: Tue Jul 28 19:51:09 CST 2020
文件: E:\Downloads\java_pid11875\java_pid11875.hprof.4
文件大小: 13,721.7 MB字节总数: 12,583,316,263
类总数: 5,124
实例总数: 209,093,920
类加载器: 60
垃圾回收根节点: 4,351
等待结束的暂挂对象数: 0环境:
操作系统: Linux (3.16.0-6-amd64)
体系结构: amd64 64bit
Java 主目录: /usr/lib/jvm/jdk1.8.0_144/jre
Java 版本: 1.8.0_144
JVM: Java HotSpot(TM) 64-Bit Server VM (25.144-b01, mixed mode)
Java 供应商: Oracle Corporation
在JMC仪表盘看到,最大GC暂停时间居然接近14秒,恐怖如斯!
看了一下,JVM参数-XX:+UnlockCommercialFeatures -Xmx40g -Xms40g ,堆内存安排了40G。既然是GC的问题,先看看堆中有哪些对象。在jvisualvm中看到,数量最多的对象都是和网络收发有关的,比如java nio、netty和nukkitx包中的类。我判断,如此大的堆内存还导致频繁Full GC,说明老年代对象肯定数量很大,应该就是这些网络有关的对象一直无法回收导致的。
在JMC中看看,热点代码(指执行频率很高的代码)也都在网络/IO相关的类中。
再看看JVM中的线程,这时候都在干啥?果然,各个线程这时候不是忙着释放内存、就是忙着申请分配内存,而且都是netty库中的方法。说明这时候用户连接非常繁忙。
看到这些信息,再想到这是一台高并发、长连接的游戏服务器,我想问题可能在netty上。netty是一个很优秀的网络编程框架,但是netty这个库的缺点就是对象比较多,netty中一个链接是一个channel,每一个channel会有一个DefaultPipeline,然后会有一个HeadContext和TailContext以及Unsafe对象。长连接就会导致相关的这些对象一直不释放,能经历一次又一次的GC,最后进入老年代,老年代里面越来越满,Full GC一次就很久。但是,就算知道可能是这个原因,也没办法把netty去掉,代价太大,项目代码都要改写。怎么办呢,只能从调整堆内存各区大小、调整垃圾回收策略(回收器类型)方面下手优化。
先来安利一下,截止Java 8 Oracle官方虚拟机提供的垃圾回收器有哪些。
由上图可知,到Java 8为止,官方JVM提供7种垃圾回收器。Serial,ParNew,Parallel Scavenge主要负责新生代的垃圾回收,CMS,Serial Odl, Parallel Old主要负责老年代的垃圾回收,G1在新生代和老年代都可以使用。他们的特点对比:
- 串行收集器:Serial + Serial Old;
- 并行收集器: Parallel Scavenge + Parallel Old,专注于应用吞吐量;
- 并发收集器:CMS,G1,专注于响应时间。
什么是吞吐量呢?在JVM中是用单独的线程执行GC的,垃圾回收的线程会和应用程序的线程争抢CPU的执行时间,应用程序的线程执行时间越长,就是吞吐量越高。
什么是响应时间呢?就是GC不要导致应用程序执行停顿,让程序能尽快执行并响应用户。
用脚指头可以想出来,好的GC就是吞吐量又高、响应时间又短(快)。但是很不幸,这两者是矛盾的 —— 为了提高吞吐量,就要减少GC的运行时间,但是GC运行少,就会积累大量的垃圾对象和内存碎片,导致后来GC的时候,需要花更多时间来暂停程序的运行,反而导致了响应时间和吞吐量都下降。
回到我们这个诊断案例。先看看这台服务器的JVM用的什么垃圾回收器!用的是Oracle 1.8 HotSpot JVM server模式下默认的Parallel系列回收器。其中Parallel Scavenge使用复制算法对年轻代进行回收,Parallel Old使用标记整理算法对老年代进行回收,这两个是吞吐量优先的回收器。在这台游戏服务器大量网络连接的情况下,如果GC的策略是吞吐量优先,会导致很多本该及时回收的垃圾对象被延迟回收,最后进入老年代。而老年代的容量达到26.66GB,等到老年代积累的对象很多时,每一次的GC暂停时间就会很恐怖。当内存压力很大(比如我看到出现很多分配内存失败Allocation Failure失败)时,会导致Full GC,这时的每次Full GC就会耗时很长,产生非常明显的服务器暂停的效果,就算JVM不会OutOfMemoryError,用户连接超时、掉线也就不足为奇了。
按照这个思路,最后开发团队调整了JVM启动参数,使用G1回收器,改善了这个长时间Full GC导致用户掉线的问题。长期效果,后续待观察。
微信扫码关注我的视频号: