JVM专题 | 我用GC指标定位生产故障,学习垃圾回收机制真的有用

前言

每次说起Java的进阶学习,总是绕不过jvm这个话题。在jvm学习的开篇中,首先学到的就是jvm内存结构,然后就是gc垃圾回收机制。但对于许多日常开发来说,学习jvm内存结构之后,还能知道使用Xms、Xmx来调整heap大小,而学习GC可能对开发的帮助不太明显。

灵感一闪

但是通过gc,可以更好的定位程序运行中的问题。为什么这么说呢,前两天遇到了一个问题,消费kafka解析数据,因为数据量突增,导致部分主机上消费kafka一直积压,一共积压了80亿条数据,为了将这部分数据消费掉,增加主机的同时,每台主机也增加了一个进程。

同时对于kafka中topic的分区也做了调整,从160增加到200。上面的所有操作目的都是提高并发,最后效果也是显而易见,整体消费积压在逐步减少,但是有些分区积压并未减少。

我登录主机查看进程还在,查看日志发现部分线程数据解析量1200w/min,有的2w/min,但是一台主机上两个进程的日志都记录在了同一个log文件中,无法区分到底是哪个进程出现了问题。

这时候学习gc的优势就体现出来了,我先使用jps找到两个进程对应的PID,然后 jstat -histo 分别查看两个进程的GC情况。其中一个进程FGC的次数为0,一个FGC次数已经200多了。

所以我断定第二个进程有问题,通过ps查看启动时间,这个进程是最早启动的进程,因为那时候还没有提高并发,大量的原始数据在被读进了jvm的heap之后,程序没有足够的解析能力,导致数据一直存放在内部queue中,最后触发Full GC导致STW,从而暂停所有应用程序线程,所以最终现象就是程序解析能力下降。

GC

上面说了那么多,其实就是为了讲明白一件事:学习GC有用。那么该如何学习GC呢?GC的学习主要从以下几个方面开始。

什么是GC

GC 通常指的是“垃圾回收”(Garbage Collection),这是一种自动管理内存的机制,主要用于编程语言中。它的主要作用是自动检测和释放不再使用的内存,从而避免内存泄漏和提高程序的稳定性。

在Java中,不需要开发者手动释放内存,GC 会定期检查内存中不再被引用的对象,并将其占用的内存空间回收,以便存放新增的对象。

上面提到的内存,指得就是jvm的内存区域。很多人都知道jvm的区域划分为堆(heap)、方法区(java8中移除方法区、并修改为使用内存的metaspace)、虚拟机栈等。而 GC 针对的区域主要是heap。

gc分类

在 Java 中,我们听到最多的就是Young GC(Minor GC) 和 Full GC。直接从字面意思理解,YGC就是对年轻代(Young)进行垃圾回收,FGC就是对新生代和老生代全部(Full)进行垃圾回收。

兜兜转转,还是离不开jvm的内存结构,已知上面说GC的区域是heap,这里又分为年轻代和老年代,故年轻代和老年代都包含于heap。

年轻代和老年代

年轻代是堆的一部分,专门用于存放新创建的对象。它通常包括三个区域:Eden 区和两个 Survivor 区(S0 和 S1)。

我启动一个java程序,将内存设置为10m,通过 jstat 查看每个区域所占的大小。

如图,以C结尾的表示capacity(容量),U表示used(已使用),单位为KB。从而可以看出Eden与S0、S1在年轻代的区域占比为4:1:1,可以通过 -XX:SurvivorRatio 来控制。

OC就是老年代的容量,7168KB也就是7MB,新生代与老年代的比例为3:7。

Young GC

Young GC就是回收Eden区域的。新对象被创建时,它们首先被分配到 Eden 区。只有当 Eden 区满时,才会触发 Minor GC,清理不再使用的对象,并将存活的对象移动到 Survivor 区。

这里写一段代码来演示一下:

 

java

代码解读

复制代码

while (true) { byte[] b = new byte[1024]; Thread.sleep(10); }

代码中一直在创建1KB的字节数组,我们使用jstat查看GC状态。

如图,共触发了25次YGC,因为字节数组b在创建之后,在后面没有被引用,所以当Eden满的时候,就会触发Young GC清理掉,从而不会进入S0、S1和老年代。

换句话说,当字节数组有引用了之后,就会进入S0、S1和老年代。修改代码,将字节数组添加到que。

 

java

代码解读

复制代码

ConcurrentLinkedQueue<byte[]> queue = new ConcurrentLinkedQueue<>(); while (true) { byte[] b = new byte[1024]; queue.add(b); Thread.sleep(10); }

这时候b被queue引用,在Young GC垃圾回收Eden时,无法回收这些被引用的字节数组。此时,查看GC状态。

可以看到S0使用率100%,S1未被使用。这样的设计是为了避免内存的碎片化,在每次 Young GC 时,活着的对象能够从一个 Survivor 区复制到另一个,这种复制方式可以高效地管理内存。

在上图中,我们可以看到还触发了一次FGC。

Full GC

为什么会触发Full GC,这就要从GC过程中对象的流转过程说起:

  1. 当Eden满的时候,触发第一次Young GC,存活对象移动到S0,Eden清空
  2. 当Eden再满的时候,触发YoungGC,Eden和S0中的对象通过复制送入S1,S0和Eden清空
  3. 在多个 GC 循环后,如果某个对象在 Survivor 区中存活超过一定次数(通常是15次),它会被放到到老年代

而如果S0、S1被填满,而老年代也没有足够的空间来容纳存活的对象,就会触发 Full GC。此时,JVM 会尝试回收老年代的对象,以释放空间。

因为此queue中的数据一直在add添加,而没有poll取走,这样b就会一直被queue引用,无法达到被GC清理的条件。

如图,在老年代使用率达到99%之后,触发第三次Full GC,但是很遗憾没有什么对象是能被清理的。这时候程序只能无奈的抛出OOM内存溢出的异常,然后退出程序。

优化

所以我们在程序开发时,要尽量避免触发Full GC。Full GC涉及整个堆(年轻代和老年代),需要检查和回收存活时间较长的对象,同时在垃圾回收期间会STW(Stop-The-World),所有应用线程都被暂停,直到垃圾回收完成。

这种机制确保了在回收过程中,不会有新的对象被创建或修改,从而保证内存一致性,但是造成明显的延迟,可能影响我最初讲的程序效率问题。

而Young GC主要针对年轻代(Eden 区和 Survivor 区),通常只处理新创建的对象,回收效率较高,且大部分对象是都是被最近使用的,因此执行时间相对较短。

减少 Full GC STW 的方法:

  1. 优化堆内存配置:确保年轻代和老年代的大小合适,以减少 GC 发生频率。
  2. 选择合适的 GC 算法:使用适合的垃圾回收器(如 G1、ZGC 等),它们在处理 Full GC 时通常表现更好。
  3. 监控和调整:定期监控内存使用情况,及时调整 JVM 参数,减少 Full GC 的发生。

结语

本篇文章主要从我最近遇到的问题入手,偶发灵感使用GC定位问题的一次实践。对于开发者来说,学习GC的时间成本很低,搞清楚Young GC和Full GC,同时学会使用jstat查看gc的指标就基本上ok了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值