27、JVM 调优实战 - 实战:线上系统卡死无法访问问题排查

1. 基于JVM运行的系统最怕的是什么?

JVM运行的时候,最核心的内存区域,就是堆内存区域,这里会放各种我们系统中创建出来的对象。

堆内存里通常都会划分为新生代和老年代两个内存区域,对象一般来说都是优先放在新生代的。

 

随着系统不停的运行,会有越来越多的对象放入年轻代中,然后年轻代会塞满,而放不下更多的对象。

这时候就需要清理一下年轻代的垃圾对象,也就是那些没有GC Roots引用的对象。

所谓的GC Roots就是类的静态变量,方法的局部变量。平时创建对象的地方,就是在方法里,但是一旦一个方法运行完毕后,方法的局部变量就没了,此前在方法里创建出来的对象就是垃圾了,没有人引用了。

因此,在年轻代里,99%都是这种没人引用的垃圾对象。

所以,当年轻代快塞满的时候,会触发Minor GC将无引用的对象给回收掉。

当对新生代进行垃圾回收,就一定会停止系统程序的运行,不让系统程序执行任何代码逻辑,这个叫做 STW

此时,只允许后台的垃圾回收器的多个垃圾回收线程去工作,执行垃圾回收。

 

所谓的复制算法,就是对所有的GC Roots进行追踪,去标记出来所有被GC Roots直接或间接引用的对象,他们就是存活对象。

系统卡顿问题

每次一旦年轻代塞满之后,在进行垃圾回收的时候,这个期间都必须停止系统程序的运行。而这就是JVM运行的系统最害怕的问题: 系统卡顿问题

2. 年轻代GC到底多久一次对系统影响不大?

内存足够的情况下,通常来说系统可能在低峰时期在几个小时才有一次新生代GC,高峰期最多也就几分钟一次新生代GC。

而且一般的业务系统都是部署在2核4G或者4核8G的机器上,此时分配给堆的内存不会超过3G,给新生代中的Eden区的内存也就1G左右。

而且新生代采用的复制算法效率极高,因为新生代里存活的对象很少,只要迅速标记出这少量存活对象,移动到 Survivor 区,然后回收掉其他全部垃圾对象即可,速度很快。

很多时候,一次新生代GC可能也就耗费几毫秒,几十毫秒。而这对于用户来说几乎是无感知的,所以新生代GC一般基本对系统性能影响不大。

3. 什么时候新生代GC对系统影响很大?

当你的系统部署在大内存机器上的时候,比如说机器是32核64G的机器,此时分配给系统的内存有几十个G,新生代的Eden区可能30G~40G的内存。

比如类似Kafka、Elasticsearch之类的大数据相关的系统,都是部署在大内存的机器上的,此时如果你的系统负载非常的高,对于大数据系统是很有可能的,比如每秒几万的访问请求到Kafka、Elasticsearch上去。

那么可能导致你Eden区的几十G内存频繁塞满要触发垃圾回收,假设1分钟会塞满一次。

然后每次垃圾回收要停顿掉Kafka、Elasticsearch的运行,然后执行垃圾回收大概需要几秒钟,此时会发现,可能每过一分钟,你的系统就要卡顿几秒钟,有的请求一旦卡死几秒钟就会超时报错,此时可能会导致你的系统频繁出错。

4.如何解决大内存机器的新生代GC过慢的问题?

答:用G1垃圾回收器。

因为G1垃圾回收器,可以设置一个期望的每次GC的停顿时间,比如可以设置一个20ms。

那么G1基于它的Region内存划分原理,就可以在运行一段时间之后,比如就针对2G内存的Region进行垃圾回收,此时就仅仅停顿20ms,然后回收掉2G的内存空间,腾出来了部分内存,接着还可以继续让系统运行。

5.要命的频繁老年代GC问题

新生代GC一般问题不大,真正的问题在于频繁触发老年代的GC。

对象进入老年代的几个条件有:年龄太大了、动态年龄判定规则、新生代GC后存活对象太多无法放入Survivor中

深入分析这几个条件:

1、 第一个,对象年龄太大了,这种对象一般很少,都是系统中确实需要长期存在的核心组件,他们一般不需要被回收掉,所以在新生代熬过默认15次垃圾回收之后就会进入老年代;

2、 第二个,动态年龄判定规则,如果一次新生代GC过后,发现Survivor区域中的几个年龄的对象加起来超过了Survivor区域的 50%,比如说年龄1+年龄2+年龄3的对象大小总和,超过了Survivor区域的50%,此时就会把年龄3以上的对象都放入老年代。

3、 第三,新生代垃圾回收过后,存活对象太多了,无法放入Survivor中,此时直接进入老年代。

上述条件中,第二个和第三都是很关键的,如果新生代中的Survivor区域内存过小,就会导致上述第二个和第三个条件频繁发生,然后导致大量对象快速进入老年代,进而频繁触发老年代的GC。

老年代GC通常来说都很耗费时间,无论是CMS垃圾回收器还是G1垃圾回收器,因为比如说CMS就要经历初始标记、并发标记、重新标记、并发清理、碎片整理几个环节,过程非常的复杂,G1同样也是如此。

通常来说,老年代GC至少比新生代GC慢10倍以上,比如新生代GC每次耗费200ms,其实对用户影响不大,但是老年代每次GC耗费2s,那可能就会导致老年代GC的时候用户发现页面上卡顿2s,影响就很大了。