前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM专题 | 我用GC指标定位生产故障,学习垃圾回收机制真的有用

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

原创
作者头像
叫我阿柒啊
发布2024-09-27 22:38:06
1530
发布2024-09-27 22:38:06
举报
文章被收录于专栏:Java放弃之路入门到放弃之路

前言

每次说起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了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 灵感一闪
  • GC
    • 什么是GC
      • gc分类
      • 年轻代和老年代
        • Young GC
          • Full GC
          • 优化
          • 结语
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档