JVM提高吞吐量实践

准备工作,先准备一段代码,代码如下:

public class TestGC {
    public static void main(String[] args) throws InterruptedException {
        public static void main(String[] args) throws InterruptedException {
        System.out.println("Starting GC test...");
        java.util.List<byte[]> list = new java.util.ArrayList<>();
        // 第一阶段:分配内存并持有引用
        for (int i = 0; i < 50; i++) {
            list.add(new byte[1024 * 1024]); // 每次分配 1MB
            System.out.println("Allocated " + (i + 1) + " MB");
            Thread.sleep(100); // 短暂休眠,模拟实际分配节奏
        }
        System.out.println("Phase 1 completed. Total size: " + list.size() + " MB");
        // 第二阶段:释放引用,制造垃圾
        System.gc(); // 显式请求 GC
        list.clear(); // 清空列表,所有 byte[] 成为垃圾
        System.out.println("List cleared. Objects are now garbage.");

        // 第三阶段:继续分配,触发 GC
        for (int i = 0; i < 50; i++) {
            byte[] temp = new byte[1024 * 1024]; // 临时分配 1MB
            System.out.println("Allocated temp " + (i + 1) + " MB");
            Thread.sleep(100); // 模拟分配节奏
        }
        System.out.println("Phase 2 completed. Check GC logs for activity.");
    }
}

第一阶段:分配并持有引用:
分配 50 个 1MB 的 byte[](共 50MB),放入 list 中。这会逐渐填满新生代(Eden 区),可能触发 Minor GC。
Thread.sleep(100) 减缓分配速度,让你更容易观察 GC 日志的时间点。
第二阶段:释放引用:
调用 list.clear(),清空 ArrayList,使所有 50MB 的 byte[] 失去强引用,成为垃圾。
这为后续 GC 提供了可回收的对象。
第三阶段:触发 GC:
继续分配新的 50 个 1MB byte[],但不加入 list,仅用临时变量 temp。
由于 temp 在每次循环后超出作用域,会立即成为垃圾。
新分配的 50MB 会填满堆内存,触发 GC(可能是 Young GC 或 Full GC),此时之前释放的 50MB 垃圾可以被回收
编译成字节码

javac TestGC.java

运行,并添加参数输出日志,初始化堆大小100M,最大堆大小为100M,打印GC日志,日志输出至对应目录

java -Xms100m -Xmx100m -XX:+PrintGCDetails -Xloggc:/Users/pt/jvm.log -XX:+UseG1GC TestGC 
[0.012s][info][gc] Using G1
  - 解读:JVM 启动后 0.012 秒,日志表明正在使用 G1 垃圾回收器。这是 Java 9 之后的默认 GC 算法,适合大内存和低延迟场景。
[0.013s][info][gc,init] Version: 17.0.8+9-LTS-211 (Release)
  - 解读:JVM 在 0.013 秒初始化 GC,使用的 Java 版本是 17.0.8(LTS 长期支持版,构建号 9-LTS-211),Release 表示正式版。
[0.013s][info][gc,init] CPUs: 8 total, 8 available
  - 解读:系统有 8 个 CPU 核心,且全部可用于 JVM 的垃圾回收(并行工作线程会基于此分配)。
[0.013s][info][gc,init] Memory: 8192M
  - 解读:系统总物理内存为 8192MB(8GB),这是 JVM 能感知到的宿主机内存。
[0.013s][info][gc,init] Large Page Support: Disabled
  - 解读:大页面支持被禁用。大页面可以提高内存访问效率,但这里未启用(可能需要操作系统和 JVM 参数支持)。
[0.013s][info][gc,init] NUMA Support: Disabled
  - 解读:NUMA(非均匀内存访问)支持被禁用。NUMA 优化多处理器内存分配,但在当前配置下未开启。
[0.013s][info][gc,init] Compressed Oops: Enabled (Zero based)
  - 解读:压缩对象指针(Compressed Oops)启用,且基于零地址。Java 在 64 位系统上用 32 位指针表示对象地址以节省内存。
[0.013s][info][gc,init] Heap Region Size: 1M
  - 解读:G1 GC 将堆划分为多个 Region,每个 Region 大小为 1MB。这是 G1 的分区管理基础。
[0.013s][info][gc,init] Heap Min Capacity: 104M
  - 解读:堆的最小容量为 104MB(由 `-Xms` 参数控制,初始分配的堆大小)。
[0.013s][info][gc,init] Heap Initial Capacity: 104M
  - 解读:堆的初始容量为 104MB,与最小容量一致,表示启动时分配的内存。
[0.013s][info][gc,init] Heap Max Capacity: 104M
  - 解读:堆的最大容量为 104MB(由 `-Xmx` 参数控制,堆可扩展到的上限)。
[0.013s][info][gc,init] Pre-touch: Disabled
  - 解读:预触碰(Pre-touch)功能禁用。预触碰会在启动时预分配堆内存页面,这里未启用。
[0.013s][info][gc,init] Parallel Workers: 8
  - 解读:并行 GC 工作线程数为 8,与 CPU 核心数一致,用于 Minor GC 等并行任务。
[0.013s][info][gc,init] Concurrent Workers: 2
  - 解读:并发 GC 工作线程数为 2,用于 G1 的并发标记和清理任务。
[0.013s][info][gc,init] Concurrent Refinement Workers: 8
  - 解读:并发精细化(Reference 处理)工作线程数为 8,处理引用更新。
[0.013s][info][gc,init] Periodic GC: Disabled
  - 解读:定期 GC 被禁用。G1 默认不按固定时间间隔触发 GC,而是基于内存压力。
[0.044s][info][gc,metaspace] CDS archive(s) mapped at: [0x0000000400000000-0x0000000400be4000-0x0000000400be4000), size 12468224, SharedBaseAddress: 0x0000000400000000, ArchiveRelocationMode: 1.
  - 解读:0.044 秒时,类数据共享(CDS)存档映射到指定内存地址,大小约 12MB,用于加速类加载。
[0.044s][info][gc,metaspace] Compressed class space mapped at: 0x0000000401000000-0x0000000441000000, reserved size: 1073741824
  - 解读:压缩类空间映射到指定地址,保留大小为 1GB,用于存储类元数据(Metaspace 的一部分)。
[0.044s][info][gc,metaspace] Narrow klass base: 0x0000000400000000, Narrow klass shift: 0, Narrow klass range: 0x100000000
  - 解读:类指针的压缩基地址和范围,用于优化类元数据的存储。
[2.366s][info][gc,start    ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)
  - 解读:2.366 秒时,第一次 GC(GC(0))触发,是暂停式新生代 GC(Minor GC),由“G1 Humongous Allocation”(超大对象分配)引起,标记并发阶段开始。
[2.366s][info][gc,task     ] GC(0) Using 2 workers of 8 for evacuation
  - 解读:此次 GC 使用 2 个并行工作线程(总共可用 8 个)进行对象疏散(拷贝存活对象)。
[2.367s][info][gc,phases   ] GC(0)   Pre Evacuate Collection Set: 0.1ms
  - 解读:预疏散收集集阶段耗时 0.1 毫秒,准备要回收的区域。
[2.367s][info][gc,phases   ] GC(0)   Merge Heap Roots: 0.0ms
  - 解读:合并堆根(如栈变量、静态变量)耗时 0.0 毫秒,确定 GC 起点。
[2.368s][info][gc,phases   ] GC(0)   Evacuate Collection Set: 0.8ms
  - 解读:疏散收集集耗时 0.8 毫秒,将存活对象从 Eden 区移动到 Survivor 区。
[2.368s][info][gc,phases   ] GC(0)   Post Evacuate Collection Set: 0.2ms
  - 解读:疏散后处理耗时 0.2 毫秒,更新引用等收尾工作。
[2.368s][info][gc,phases   ] GC(0)   Other: 0.7ms
  - 解读:其他杂项耗时 0.7 毫秒,可能包括日志记录等开销。
[2.368s][info][gc,heap     ] GC(0) Eden regions: 1->0(19)
  - 解读:Eden 区从 1 个 Region 减少到 0 个(清理后),预计下次分配可使用 19 个 Region。
[2.368s][info][gc,heap     ] GC(0) Survivor regions: 0->1(3)
  - 解读:Survivor 区从 0 个增加到 1 个(存活对象移动至此),容量为 3 个 Region。
[2.368s][info][gc,heap     ] GC(0) Old regions: 0->0
  - 解读:老年代 Region 数量不变,仍为 0,未涉及老年代回收。
[2.368s][info][gc,heap     ] GC(0) Archive regions: 2->2
  - 解读:归档 Region(存储 CDS 数据)数量不变,为 2 个。
[2.368s][info][gc,heap     ] GC(0) Humongous regions: 44->44
  - 解读:超大对象 Region 数量不变,为 44 个,表明超大对象未被回收。
[2.368s][info][gc,metaspace] GC(0) Metaspace: 171K(384K)->171K(384K) NonClass: 164K(256K)->164K(256K) Class: 7K(128K)->7K(128K)
  - 解读:Metaspace 使用情况,GC 前后均为 171KB(总容量 384KB),非类元数据 164KB(容量 256KB),类元数据 7KB(容量 128KB),未变化。
[2.368s][info][gc          ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 45M->45M(104M) 2.007ms
  - 解读:总结此次 GC,暂停式新生代回收,触发原因是超大对象分配,堆从 45MB->45MB(总容量 104MB),耗时 2.007 毫秒。
[2.368s][info][gc,cpu      ] GC(0) User=0.00s Sys=0.00s Real=0.00s
  - 解读:CPU 时间,用户态 0 秒,系统态 0 秒,真实时间 0 秒(精度较低,实际见 2.007ms)。
[2.368s][info][gc          ] GC(1) Concurrent Undo Cycle
  - 读:GC(1) 开始并发撤销周期,可能是清理并发标记的中间状态。
[2.368s][info][gc,marking  ] GC(1) Concurrent Cleanup for Next Mark
  - 解读:并发清理,为下次标记做准备。
[2.368s][info][gc,marking  ] GC(1) Concurrent Cleanup for Next Mark 0.467ms
  - 解读:并发清理耗时 0.467 毫秒。
[2.368s][info][gc          ] GC(1) Concurrent Undo Cycle 0.536ms
  - 解读:并发撤销周期总耗时 0.536 毫秒。
[2.473s][info][gc,start    ] GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation)
  - 解读:2.473 秒时,第二次 GC(GC(2))触发,同样是暂停式新生代 GC,因超大对象分配引起。
[2.474s][info][gc,task     ] GC(2) Using 2 workers of 8 for evacuation
  - 解读:使用 2 个线程进行对象疏散。
[2.475s][info][gc,phases   ] GC(2)   Pre Evacuate Collection Set: 0.1ms
  - 解读:预疏散耗时 0.1 毫秒。
[2.475s][info][gc,phases   ] GC(2)   Merge Heap Roots: 0.1ms
  - 解读:合并堆根耗时 0.1 毫秒。
[2.475s][info][gc,phases   ] GC(2)   Evacuate Collection Set: 0.9ms
  - 解读:疏散收集集耗时 0.9 毫秒。
[2.475s][info][gc,phases   ] GC(2)   Post Evacuate Collection Set: 0.2ms
  - 解读:疏散后处理耗时 0.2 毫秒。
[2.475s][info][gc,phases   ] GC(2)   Other: 0.4ms
  - 解读:其他耗时 0.4 毫秒。
[2.475s][info][gc,heap     ] GC(2) Eden regions: 1->0(24)
  - 解读:Eden 区从 1 个 Region 清为 0 个,下次可用 24 个 Region。
[2.475s][info][gc,heap     ] GC(2) Survivor regions: 1->1(3)
  - 解读:Survivor 区保持 1 个 Region,容量为 3 个。

三次GC 分别如下
总运行时间:从 [0.012s] 到 [2.475s],即 2.475s - 0.012s = 2.463 秒(2463 毫秒)。
GC 暂停时间:
GC(0):2.007ms
GC(2):约 1.7ms
总暂停时间 = 2.007ms + 1.7ms = 3.707ms
GC(1) 是并发操作(0.536ms),不暂停应用线程,所以不计入暂停时间。
计算吞吐量
应用程序运行时间 = 总运行时间 - GC 暂停时间
= 2463ms - 3.707ms = 2459.293ms
吞吐量 = (应用程序运行时间 / 总运行时间) × 100%
= (2459.293ms / 2463ms) × 100%
≈ 99.85%
GCEasy分析吞吐量

吞吐量还是挺高的,现在我们来实现降低吞吐量
降低吞吐量的核心是增加 GC 的频率、耗时或资源竞争,从而减少应用程序的有效运行时间。以下是修改命令的几种方法:

  1. 减小堆内存
    减少 -Xms 和 -Xmx,增加内存压力,使 GC 更频繁触发:
java -Xms64m -Xmx64m -XX:+PrintGCDetails -Xloggc:/Users/pt/jvm.log -XX:+UseG1GC TestGC

效果:堆大小降至 64MB,50MB 分配接近极限。第一阶段填满堆,第三阶段分配额外 50MB 时频繁触发 GC。吞吐量降低原因,GC 次数增加,暂停时间占比升高,应用运行时间减少。
2.强制更频繁的 GC

 -XX:+UseGCOverheadLimit 和 -XX:GCTimeLimit=10,限制 GC 时间占比:
java -Xms150m -Xmx150m -XX:+PrintGCDetails -Xloggc:/Users/pt/jvm.log -XX:+UseG1GC -XX:+UseGCOverheadLimit -XX:GCTimeLimit=10 TestGC
效果:

-XX:+UseGCOverheadLimit 启用 GC 开销限制。
-XX:GCTimeLimit=10 设置 GC 时间上限为 10%(默认 98%),若超限抛出 OutOfMemoryError。
吞吐量降低原因:GC 被迫更频繁运行,且可能提前失败,减少有效工作时间。
3. 调整 G1GC 参数增加暂停时间
通过设置更严格的暂停目标和更小的 Region 大小,增加 GC 开销:

java -Xms150m -Xmx150m -XX:+PrintGCDetails -Xloggc:/Users/pt/jvm.log -XX:+UseG1GC -XX:MaxGCPauseMillis=10 -XX:G1HeapRegionSize=512k TestGC

效果:-XX:MaxGCPauseMillis=10:将最大暂停时间设为 10ms,迫使 G1GC 更频繁执行。-XX:G1HeapRegionSize=512k:减小 Region 大小(默认 1MB),1MB 对象变为 Humongous 对象,增加管理开销。
吞吐量降低原因:GC 暂停更频繁且耗时增加,应用分配效率降低。
4. 限制并行 GC 线程减少 G1GC 的并行工作线程,延长 GC 时间:

java -Xms150m -Xmx150m -XX:+PrintGCDetails -Xloggc:/Users/pt/jvm.log -XX:+UseG1GC -XX:ParallelGCThreads=1 -XX:ConcGCThreads=1 TestGC

效果:-XX:ParallelGCThreads=1:并行 GC 线程数设为 1(默认基于 CPU 核心数)。
-XX:ConcGCThreads=1:并发 GC 线程数设为 1。
吞吐量降低原因:GC 执行变慢,暂停和并发阶段耗时增加,应用运行受阻。
5. 启用更耗时的 GC 算法:切换到 Serial GC(单线程,吞吐量最低):

java -Xms150m -Xmx150m -XX:+PrintGCDetails -Xloggc:/Users/pt/jvm.log -XX:+UseSerialGC TestGC