垃圾回收的停顿

垃圾回收的停顿

垃圾回收停顿是什么

JVM 垃圾回收停顿,全称 Stop-The-World(STW),是垃圾回收器(GC)执行内存管理任务时触发的关键线程暂停状态。

垃圾回收器的核心职责是判定堆内存中对象的可达性、回收不可达的垃圾对象、整理内存碎片以优化内存分配效率。由于堆内存是 JVM 所有应用线程的共享资源,为保障 GC 操作的准确性与安全性,JVM 会在 GC 关键阶段暂停所有应用线程的执行,仅允许 GC 线程运行。此阶段应用程序对外呈现完全无响应状态,也是制约 Java 应用吞吐量与延迟稳定性的核心因素之一。

STW 触发的核心目的

STW 的核心目标是冻结堆内存的引用关系与内存布局,消除应用线程并发操作对 GC 流程的干扰,具体可拆解为两个维度:

  • 保障 GC 标记结果的一致准确
    阻断应用线程对堆内存对象的创建、引用关联更新、引用解除等写操作,使堆内所有对象的可达性状态固定于某一瞬时时间切片。消除 "一边标记存活对象、一边修改引用关系" 带来的不确定性,避免出现存活对象漏标、垃圾对象错标等问题。
  • 保障 GC 内存操作的安全可控
    在执行内存复制、内存压缩等核心操作时,规避应用线程与 GC 线程对同一内存区域的并发访问冲突。当 GC 线程移动存活对象地址或更新对象引用指针时,STW 可确保应用线程不会读取或写入处于 "迁移过程中" 的无效内存地址,防止内存访问错乱与程序崩溃。

STW 发生的核心阶段

不同 GC 算法的执行流程存在差异,但 STW 主要集中在以下三个核心阶段,且各阶段的停顿时长与影响因素差异显著:

  • 初始标记(Initial Mark)
    • 任务:标记 GC Roots 直接引用的对象,确立存活对象标记的起点边界。
    • 必要性:GC Roots 是可达性分析的根基,若不暂停用户线程,用户线程可能在标记过程中创建、销毁或修改根引用,导致标记起点错乱,进而引发后续存活对象判断失误。
    • 特点:停顿时间极短,通常为毫秒级甚至亚毫秒级。此阶段仅遍历 GC Roots 集合本身,不递归扫描整个堆的对象引用链,耗时仅取决于 GC Roots 的数量。
    • 适用 GC:所有实现并发标记能力的收集器(CMS、G1、ZGC、Shenandoah)均会在此阶段触发短暂 STW;Serial GC、Parallel GC 虽无并发标记概念,但其标记流程的第一步本质也是类似的根扫描,且全程处于 STW 状态。
  • 重新标记(Remark)
    • 任务:修正并发标记阶段因用户线程持续运行导致的标记结果偏差,确保存活对象标记的完整性与准确性。
    • 解决问题:并发标记期间,用户线程会执行对象创建、引用更新、对象销毁等操作,可能产生新的存活对象或使部分对象的引用失效,需通过重新扫描更新标记结果。
    • 特点:停顿时间长于初始标记,短于内存复制 / 压缩阶段;停顿时长与两个因素强相关:一是并发标记阶段用户线程的引用修改频率,二是堆内存活对象的数量;通过优化缓冲区处理逻辑,可进一步缩短停顿。
    • 适用 GC:
      CMS、G1 的核心 STW 阶段之一,是保障标记准确性的关键环节;
      ZGC/Shenandoah 通过染色指针等技术,将大部分标记修正工作转为并发执行,此阶段的 STW 被极度压缩,甚至可忽略不计;
      Serial GC、Parallel GC 无独立的重新标记阶段,因全程 STW,标记过程本身就是精准的,无需额外修正。
  • 内存复制 / 压缩(Evacuation/Compaction)
    • 任务:对存活对象执行内存空间整理操作 ------ 或复制存活对象到新的内存区域,或移动存活对象以消除内存碎片,同时更新所有指向这些对象的引用地址,最终回收垃圾对象占用的内存。
    • 必要性:内存空间的 复制 / 移动 / 压缩 属于独占性内存操作,若不暂停用户线程,用户线程可能访问正在被移动的对象,或修改对象引用,导致引用指向无效内存地址,引发程序崩溃或内存泄漏。
    • 特点:传统收集器中停顿时间最长的阶段,停顿时长与存活对象的数量和大小直接正相关 ------ 存活对象越多、对象越大,复制 / 移动的耗时越久。
    • 适用 GC:
      Serial GC、Parallel GC:全程 STW,此阶段占总停顿时间的绝大部分,且需扫描整个堆;
      G1:采用分区回收策略,仅选择存活对象占比低、回收收益高的 Region 集合(回收集 CSet)执行复制,复制过程同步完成内存压缩;可通过 -XX:MaxGCPauseMillis 参数控制单次回收的 Region 数量,实现延迟可控。
      ZGC/Shenandoah:通过并发重定位、引用屏障动态更新地址技术,将此阶段的核心工作转为并发执行,仅保留极短的 STW 用于完成地址映射的最终确认,停顿时间与堆大小几乎无关。

示例代码

我们来看如下代码,直观观测 JVM 的 STW 现象:

java 复制代码
import java.util.HashMap;

public class StopWorldTest {

    public static class MyThread extends Thread {

        HashMap map = new HashMap();

        @Override
        public void run() {
            try {
                while (true) {
                    if (map.size() * 512 / 1024 / 1024 >= 900) {
                        map.clear();
                        System.out.println("clean map");
                    }
                    byte[] b1;
                    for (int i = 0; i < 100; i++) {
                        b1 = new byte[512];
                        map.put(System.nanoTime(), b1);
                    }
                    //Thread.sleep(1);
                }
            } catch (Exception e) {
            }
        }

        public static class PrintThread extends Thread {
            public static final long starttime = System.currentTimeMillis();

            @Override
            public void run() {
                try {
                    while (true) {
                        // 8722
                        long t = System.currentTimeMillis() - starttime;
                        System.out.println(t / 1000 + "." + t % 1000);
                        Thread.sleep(100);
                    }

                } catch (Exception e) {
                }
            }

            public static void main(String args[]) {
                MyThread t = new MyThread();
                PrintThread p = new PrintThread();
                t.start();
                p.start();
            }
        }
    }
}

MyThread 线程(内存消耗线程):循环创建 512 字节的数组并放入HashMap,当总占用接近 900MB 时清空 map 释放内存,避免触发 OutOfMemoryError;

PrintThread 线程(时间戳输出线程):以 100ms 为固定间隔输出相对时间戳,正常情况下输出应该是连续的;若发生 STW,线程无法立即执行,会出现时间戳跳变,跳变的差值就是 STW 的大致时长。

使用如下参数执行:

bash 复制代码
-Xmx1g -Xms1g -Xmn812k -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails

其中 +UseSerialGC 即使用 Serial 串行 GC,SerialGC 在 Minor GC 和 Full GC 都会暂停所有应用线程,且单线程执行 GC,停顿时间更长,STW 现象更易观察。

如图时间戳产生大幅跳变

线程每次循环创建 100 个 byte[512] 数组,因 while(true) 无休眠机制,会以极高频率持续创建对象;而 JVM 新生代内存仅配置 812KB,新生代空间远小于对象创建速率,很快被占满并触发高频 Minor GC(新生代垃圾回收)。Serial GC 为单线程回收器,其新生代回收阶段会触发 STW 暂停所有用户线程;由于所有 byte[512] 数组均被 HashMap 强引用,GC 可达性分析判定这些对象为存活状态,因此 Minor GC 几乎无法回收任何内存,存活对象会被持续晋升至老年代。老年代内存随对象持续晋升快速耗尽,进而触发 Full GC,Full GC 需单线程扫描并回收整个堆内存,且因测试中 map.clear() 的触发阈值过高,未等该方法执行断开引用 Full GC 无法有效回收内存,仅能遍历所有存活对象,导致 STW 时长从 Minor GC 的毫秒级飙升至数百毫秒,仅当执行 map.clear() 断开 HashMap 对数组的强引用后,这些数组才会被标记为不可达对象,且必须等下一次 GC 执行,内存才会真正被释放。最终老年代完全耗尽,JVM 无法为新 byte[512] 数组分配内存,触发 "无法创建字节数组" 异常,程序无法继续正常执行。

高频 Minor GC 的短时长 STW 会造成时间戳轻微抖动,而单次 Full GC 的长时长 STW 是时间戳大幅跳变的核心原因。

从此例中可以看到,新生代GC进行较为频繁,但每一次GC耗时较短,而老年代GC发生次数较少,但每一次所消耗的时间较长,下面通过修改虚拟机参数改变这种现象:

bash 复制代码
-Xmx1g -Xms1g -Xmn900m -XX:SurvivorRatio=1 -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails

-Xmn900m:新生代扩容至 900MB,解决新生代过小导致高频 Minor GC 的问题;

-XX:SurvivorRatio=1:新生代中 Eden : From : To = 1:1:1,各占 300MB,增大 Survivor 区,减少对象过早晋升到老年代。

同时,修改第8行代码为:

java 复制代码
if (map.size() * 512 / 1024 / 1024 >= 550) {

将阈值设为 550MB,意味着:

当 map 占用 550MB 时就清空,此时新生代还有约 350MB 空闲空间,使得对象主要集中在新生代;当内存清空后,这些对象的引用被断开,Full GC 能一次性回收大部分内存,同时能避免老年代溢出。

执行结果如下:

修改 JVM 参数后,程序执行流程及 GC 日志呈现出清晰且可控的规律:程序运行初期,因-Xmn900m将新生代扩容至堆内存的 90%,极大提升了新生代的内存缓冲能力,对象高频创建不再瞬间占满新生代,使得 Minor GC 触发频率降低;但新生代单次回收的内存量从几百 KB 增至数百 MB,因此单次 Minor GC 耗时从毫秒级升至 0.23 秒左右。当新生代存活对象向老年代晋升时,因老年代被压缩至仅约 100MB,出现空间不足,JVM 触发保护机制立即执行 Full GC;后续程序进入稳定循环:老年代因对象持续晋升逐步占满,触发周期性 Full GC,且由于 map.clear() 阈值已降至 550MB(提前断开 HashMap 对 byte 数组的强引用),GC 能有效识别并回收不可达对象,使得每次 Full GC 可在 0.18-0.31 秒内完成回收。整个过程中程序依靠 "Minor GC(晋升失败)→ 触发 Full GC → 有效回收内存 → 对象重新创建" 的循环稳定运行,未出现内存耗尽的 OOM 异常。

相关推荐
それども16 小时前
分库分表的事务问题 - 怎么实现事务
java·数据库·mysql
Java面试题总结16 小时前
基于 Java 的 PDF 文本水印实现方案(iText7 示例)
java·python·pdf
马猴烧酒.16 小时前
【面试八股|Java集合】Java集合常考面试题详解
java·开发语言·python·面试·八股
以卿a16 小时前
C++(继承)
开发语言·c++·算法
lly20240616 小时前
XQuery 选择和过滤
开发语言
测试工程师成长之路16 小时前
Serenity BDD 框架:Java + Selenium 全面指南(2026 最新)
java·开发语言·selenium
lang2015092816 小时前
Java JSON绑定API:JSR 367详解
java·java-ee
czxyvX16 小时前
017-AVL树(C++实现)
开发语言·数据结构·c++
eWidget17 小时前
随机森林原理:集成学习思想 —— Java 实现多棵决策树投票机制
java·数据库·随机森林·集成学习·金仓数据库
你真是饿了17 小时前
1.C++入门基础
开发语言·c++