掘金 人工智能 10月31日 09:58
Java 21 分代式 ZGC 深度解析
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了 Java 21 中引入的分代式 ZGC(Generational ZGC)特性。文章首先回顾了 ZGC 的基本概念、核心技术(如染色指针、读屏障)及其在低延迟和处理大内存方面的优势。随后,详细阐述了分代回收的原理,并对比了 ZGC 在引入分代前后的工作流程和性能影响。最后,通过实际的命令行参数和日志输出来展示如何开启和观察 ZGC 的运行,并总结了分代式 ZGC 在吞吐量和延迟优化方面的显著进步。

🔵 **ZGC 核心技术与优势**:ZGC(The Z Garbage Collector)是一款为大内存、低延迟服务设计的垃圾回收器,其核心在于实现几乎全并发的垃圾回收过程,以最大程度减少 GC 停顿(STW)。关键创新技术包括染色指针(Colored Pointers),通过在指针中嵌入元数据来标记对象和实现对象地址的多重视图;以及读屏障(Load Barriers),在应用线程读取对象引用时注入代码,辅助 GC 进行标记和重映射。这些技术使得 ZGC 能够实现亚毫秒级的 GC 停顿,且停顿时间不受堆大小影响,满足了高性能应用的需求。

🟢 **分代回收的原理与必要性**:分代回收基于“弱分代假说”,即大部分对象生命周期短暂(朝生夕死),而老对象则相对稳定。JVM 将堆划分为年轻代(伊甸园区、幸存区)和老年代。年轻代内存较小,GC 频繁,采用复制算法高效处理大量短生命周期对象;老年代内存较大,对象存活率高,GC 频率低,采用标记-整理算法处理。分代回收通过针对不同代采取不同策略,显著提高了整体 GC 效率和应用吞吐量。

🟡 **分代式 ZGC 的引入与影响**:在 Java 21 之前,ZGC 是单代回收器,每次 GC 需扫描整个堆,可能影响应用吞吐量。Java 21 引入的分代式 ZGC(Generational ZGC)将分代思想融入 ZGC,通过针对年轻代进行更频繁、更高效的 GC,并降低老年代 GC 的频率,进一步优化了应用的吞吐量和延迟。虽然分代式 ZGC 的实现过程复杂,但它在 Java 23 已成为默认选项,显示了其重要性和前瞻性。

🟠 **ZGC 的实践与日志分析**:文章提供了启用 ZGC(包括分代式 ZGC)的命令行参数示例,如 `-XX:+UseZGC` 和 `-XX:+ZGenerational`,并展示了 `-Xlog:gc*` 参数生成的详细 GC 日志。通过分析这些日志,可以观察到 ZGC 的各个阶段(如 Pause Mark Start, Concurrent Mark, Pause Relocate Start, Concurrent Relocate)以及不同代(如果启用分代)的回收情况,有助于理解 ZGC 的工作机制和性能表现。

想要搞清楚 Java 21 中的 分代式 ZGC(Generational ZGC) 这个特性,我们需要先搞清楚什么是 ZGC。

ZGC 简介

ZGC(The Z Garbage Collector) 是由 Oracle 开发的一款垃圾回收器,最初在 Java 11 中以实验性功能推出,并经过几个版本的迭代,最终在 Java 15 中被宣布为 Production Ready,相比于其他的垃圾回收器,ZGC 更适用于大内存、低延迟服务的内存管理和回收。下图展示的是不同的垃圾回收器所专注的目标也各不相同:

低延迟服务的最大敌人是 GC 停顿,所谓 GC 停顿指的是垃圾回收期间的 STW(Stop The World),当 STW 时,所有的应用线程全部暂停,等待 GC 结束后才能继续运行。要想实现低延迟,就要想办法减少 GC 的停顿时间,根据 JEP 333 的介绍,最初 ZGC 的目标是:

经过几年的发展,目前 ZGC 的最大停顿时间已经优化到了不超过 1 毫秒(Sub-millisecond,亚毫秒级),且停顿时间不会随着堆的增大而增加,甚至不会随着 root-set 或 live-set 的增大而增加(通过 JEP 376 Concurrent Thread-Stack Processing 实现),支持处理最小 8MB,最大 16TB 的堆:

ZGC 之所以能实现这么快的速度,不仅是因为它在算法上做了大量的优化和改进,而且还革命性的使用了大量的创新技术,包括:

关于这些技术点,网上的参考资料有很多,有兴趣的同学可以通过本文的更多部分进一步学习,其中最有意思的莫过于 染色指针读屏障,下面重点介绍这两项。

染色指针

在 64 位的操作系统中,一个指针有 64 位,但是由于内存大小限制,其实有很多高阶位是用不上的,所以我们可以在指针的高阶位中嵌入一些元数据,这种在指针中存储元数据的技术就叫做 染色指针(Colored Pointers)。染色指针是 ZGC 的核心设计之一,以前的垃圾回收器都是使用对象头来标记对象,而 ZGC 则通过染色指针来标记对象。ZGC 将一个 64 位的指针划分成三个部分:

其中,前面的 16 位暂时没用,预留给以后使用;后面的 44 位表示对象的地址,所以 ZGC 最大可以支持 2^44=16T 内存;中间的 4 位即染色位,分别是:

此外,染色指针不仅用来标记对象,还可以实现对象地址的多重视图,上述 Marked0、Marked1、Remapped 三个染色位其实代表了三种地址视图,分别对应三个虚拟地址,这三个虚拟地址指向同一个物理地址,并且在同一时间,三个虚拟地址有且只有一个有效,整个视图映射关系如下:

这三个地址视图的切换是由垃圾回收的不同阶段触发的:

读屏障

读屏障(Load Barriers) 是 ZGC 的另一项核心技术,当应用程序从堆中读取对象引用时,JIT 会向应用代码中注入一小段代码:

在上面的代码示例中,只有第一行是从堆中读取对象引用,所以只会在第一行后面注入代码,注入的代码类似于这样:

String n = person.name; // Loading an object reference from heapif (n & bad_bit_mask) {  slow_path(register_for(n), address_of(person.name));}

这行代码虽然简单,但是用途却很大,在垃圾回收的不同阶段,触发的逻辑也有所不同:在标记阶段,通过读屏障操作,可以让应用线程帮助 GC 线程一起完成对象的标记或重映射;在转移阶段,如果对象地址发生变化,还能自动实现对象转移。

ZGC 工作流程

整个 ZGC 可以划分成下面六个阶段:

其中有三个是 STW 阶段,尽管如此,但是 ZGC 对 STW 的停顿时间有着严格的要求,一般不会超过 1 毫秒。这六个阶段的前三个可以统称为 标记(Mark)阶段

ZGC 的后三个阶段统称为 转移(Relocation)阶段(也叫重定位阶段):

为什么要分代?

在 ZGC 面世之前,Java 内置的所有垃圾回收器都实现了分代回收(G1 是逻辑分代):

垃圾回收器(别名)用法说明
Serial GC、Serial Copying-XX:+UseSerialGC串行,用于年轻代,使用复制算法
Serial Old、MSC-XX:+UseSerialOldGC串行,用于老年代,使用标记-整理算法
ParNew GC-XX:+UseParNewGCSerial GC 的并行版本,用于年轻代,使用复制算法
Parallel GC、Parallel Scavenge-XX:+UseParallelGC并行,用于年轻代,使用复制算法
Parallel Old、Parallel Compacting-XX:+UseParallelOldGC并行,用于老年代,使用标记-整理算法
CMS、Concurrent Mark Sweep-XX:+UseConcMarkSweepGC并发,用于老年代,使用标记-清除算法
G1、Garbage First-XX:+UseG1GC并发,既可以用于年轻代,也可以用于老年代,使用复制 + 标记-整理算法,用来取代 CMS

这些分代回收器之间可以搭配使用,周志明老师在《深入理解 Java 虚拟机》这本书中总结了各种回收器之间的关系:

其中,Serial + CMS 和 ParNew + Serial Old 这两个组件在 Java 9 之后已经被取消,而 CMS 与 Serial Old 之间的连线表示 CMS 在并发失败的时候(Concurrent Mode Failure)会切换成 Serial Old 备用方案。

分代的基本思想源自于 弱分代假说(Weak Generational Hypothesis),这个假说认为绝大部分对象都是朝生夕死的,也就是说年轻对象往往很快死去,而老对象往往会保留下来。根据这个假说,JVM 将内存区域划分为 年轻代(Young Generation)老年代(Old Generation),新生代又进一步划分为 伊甸园区(Eden)第一幸存区(S0)第二幸存区(S1)

伊甸园区用来分配新创建的对象,如果没有足够的空间,就会触发一次 年轻代 GC(Young GC,Minor GC) 来释放内存空间,这里一般使用 标记-复制(Mark-Copy) 算法,将存活的对象标记下来,然后复制到一个幸存区中;年轻代的内存空间一般较小,所以可以更频繁地触发 GC,清理掉那些朝生夕死的对象,从而提高应用程序的性能;如果 GC 后伊甸园区还没有足够的空间存放新创建的对象,或者幸存区中某个对象的存活时间超过一定的阈值,这时就会将对象分配到老年代,如果老年代的空间也满了,就会触发一次 老年代 GC(Old GC,Full GC);老年代的内存空间要大的多,而且其中的对象大部分是存活的,GC 发生的频率要小很多,所以不再使用标记-复制算法,而是采用移动对象的方式来实现内存碎片的整理。

但是在上面的 ZGC 的工作流程中,我们却没有看到分代的影子,这也就意味着每次 ZGC 都是对整个堆空间进行扫描,尽管 ZGC 的 STW 时间已经被优化到不到 1ms,但是其他几个阶段是和应用线程一起执行的,这势必会影响到应用程序的吞吐量。让 ZGC 支持分代是一项巨大的工程,开发团队足足花了三年时间才让我们有幸在 Java 21 中体验到这一令人激动的特性。

除了 ZGC,Java 11 之后还引入了一些新的垃圾回收器:

垃圾回收器用法说明
ZGC-XX:+UseZGC低延迟 GC,from JDK 11
Epsilon GC-XX:+UseEpsilonGCNo-op GC,什么都不做,用于测试,from JDK 11
Shenandoah-XX:+UseShenandoahGCCPU 密集型 GC,from JDK 12

ZGC 实践

使用 -XX:+PrintCommandLineFlags,可以打印出 Java 的默认命令行参数:

$ java -XX:+PrintCommandLineFlags -version-XX:ConcGCThreads=1 -XX:G1ConcRefinementThreads=4 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=128639872 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=2058237952 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedOops -XX:+UseG1GC openjdk version "21" 2023-09-19OpenJDK Runtime Environment (build 21+35-2513)OpenJDK 64-Bit Server VM (build 21+35-2513, mixed mode, sharing)

从上面的结果可以看出,Java 21 默认使用的仍然是 G1 垃圾回收器,它从 Java 9 就开始做为默认垃圾回收器了。

注意:Java 8 中默认的垃圾回收器是 Parallel GC。

如果想开启 ZGC,我们需要加上 -XX:+UseZGC 参数:

$ java -XX:+UseZGC -Xmx100M -Xlog:gc ZgcTest.java

其中 -Xlog:gc 参数表示打印出 GC 过程中的日志(就是 Java 8 的 -XX:+PrintGC 参数),输出结果如下:

[0.157s][info][gc] Using The Z Garbage Collector[0.420s][info][gc] GC(0) Garbage Collection (Warmup) 14M(14%)->12M(12%)[0.472s][info][gc] GC(1) Garbage Collection (System.gc()) 18M(18%)->8M(8%)

也可以使用 -Xlog:gc* 参数打印出 GC 过程中的详细日志(就是 Java 8 的 -XX+PrintGCDetails 参数),输出结果如下:

$ java -XX:+UseZGC -Xmx100M -Xlog:gc* ZgcTest.java[0.010s][info][gc,init] Initializing The Z Garbage Collector[0.011s][info][gc,init] Version: 21+35-2513 (release)[0.011s][info][gc,init] Using legacy single-generation mode[0.011s][info][gc,init] Probing address space for the highest valid bit: 47[0.011s][info][gc,init] NUMA Support: Disabled[0.011s][info][gc,init] CPUs: 4 total, 4 available[0.011s][info][gc,init] Memory: 7851M[0.011s][info][gc,init] Large Page Support: Disabled[0.011s][info][gc,init] GC Workers: 1 (dynamic)[0.011s][info][gc,init] Address Space Type: Contiguous/Unrestricted/Complete[0.011s][info][gc,init] Address Space Size: 1600M x 3 = 4800M[0.011s][info][gc,init] Heap Backing File: /memfd:java_heap[0.011s][info][gc,init] Heap Backing Filesystem: tmpfs (0x1021994)[0.012s][info][gc,init] Min Capacity: 8M[0.012s][info][gc,init] Initial Capacity: 100M[0.012s][info][gc,init] Max Capacity: 100M[0.012s][info][gc,init] Medium Page Size: N/A[0.012s][info][gc,init] Pre-touch: Disabled[0.012s][info][gc,init] Available space on backing filesystem: N/A[0.014s][info][gc,init] Uncommit: Enabled[0.014s][info][gc,init] Uncommit Delay: 300s[0.134s][info][gc,init] Runtime Workers: 1[0.134s][info][gc     ] Using The Z Garbage Collector[0.149s][info][gc,metaspace] CDS archive(s) mapped at: [0x0000006800000000-0x0000006800cb0000-0x0000006800cb0000), size 13303808, SharedBaseAddress: 0x0000006800000000, ArchiveRelocationMode: 1.[0.149s][info][gc,metaspace] Compressed class space mapped at: 0x0000006801000000-0x0000006841000000, reserved size: 1073741824[0.149s][info][gc,metaspace] Narrow klass base: 0x0000006800000000, Narrow klass shift: 0, Narrow klass range: 0x100000000[0.357s][info][gc,start    ] GC(0) Garbage Collection (Warmup)[0.357s][info][gc,task     ] GC(0) Using 1 workers[0.357s][info][gc,phases   ] GC(0) Pause Mark Start 0.007ms[0.366s][info][gc,phases   ] GC(0) Concurrent Mark 8.442ms[0.366s][info][gc,phases   ] GC(0) Pause Mark End 0.005ms[0.366s][info][gc,phases   ] GC(0) Concurrent Mark Free 0.000ms[0.367s][info][gc,phases   ] GC(0) Concurrent Process Non-Strong References 1.092ms[0.367s][info][gc,phases   ] GC(0) Concurrent Reset Relocation Set 0.000ms[0.373s][info][gc,phases   ] GC(0) Concurrent Select Relocation Set 5.587ms[0.373s][info][gc,phases   ] GC(0) Pause Relocate Start 0.003ms[0.375s][info][gc,phases   ] GC(0) Concurrent Relocate 2.239ms[0.375s][info][gc,load     ] GC(0) Load: 0.65/0.79/0.63[0.375s][info][gc,mmu      ] GC(0) MMU: 2ms/99.7%, 5ms/99.9%, 10ms/99.9%, 20ms/99.9%, 50ms/100.0%, 100ms/100.0%[0.375s][info][gc,marking  ] GC(0) Mark: 1 stripe(s), 2 proactive flush(es), 1 terminate flush(es), 0 completion(s), 0 continuation(s) [0.375s][info][gc,marking  ] GC(0) Mark Stack Usage: 32M[0.375s][info][gc,nmethod  ] GC(0) NMethods: 889 registered, 90 unregistered[0.375s][info][gc,metaspace] GC(0) Metaspace: 8M used, 8M committed, 1088M reserved[0.375s][info][gc,ref      ] GC(0) Soft: 142 encountered, 0 discovered, 0 enqueued[0.375s][info][gc,ref      ] GC(0) Weak: 747 encountered, 602 discovered, 224 enqueued[0.375s][info][gc,ref      ] GC(0) Final: 0 encountered, 0 discovered, 0 enqueued[0.375s][info][gc,ref      ] GC(0) Phantom: 146 encountered, 144 discovered, 143 enqueued[0.375s][info][gc,reloc    ] GC(0) Small Pages: 7 / 14M, Empty: 0M, Relocated: 3M, In-Place: 0[0.375s][info][gc,reloc    ] GC(0) Large Pages: 1 / 2M, Empty: 0M, Relocated: 0M, In-Place: 0[0.375s][info][gc,reloc    ] GC(0) Forwarding Usage: 1M[0.375s][info][gc,heap     ] GC(0) Min Capacity: 8M(8%)[0.375s][info][gc,heap     ] GC(0) Max Capacity: 100M(100%)[0.375s][info][gc,heap     ] GC(0) Soft Max Capacity: 100M(100%)[0.375s][info][gc,heap     ] GC(0)                Mark Start          Mark End        Relocate Start      Relocate End           High               Low         [0.375s][info][gc,heap     ] GC(0)  Capacity:      100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)   [0.375s][info][gc,heap     ] GC(0)      Free:       84M (84%)          82M (82%)          82M (82%)          88M (88%)          88M (88%)          78M (78%)    [0.375s][info][gc,heap     ] GC(0)      Used:       16M (16%)          18M (18%)          18M (18%)          12M (12%)          22M (22%)          12M (12%)    [0.375s][info][gc,heap     ] GC(0)      Live:         -                 6M (6%)            6M (6%)            6M (6%)             -                  -          [0.375s][info][gc,heap     ] GC(0) Allocated:         -                 2M (2%)            2M (2%)            3M (4%)             -                  -          [0.375s][info][gc,heap     ] GC(0)   Garbage:         -                 9M (10%)           9M (10%)           1M (2%)             -                  -          [0.375s][info][gc,heap     ] GC(0) Reclaimed:         -                  -                 0M (0%)            7M (8%)             -                  -          [0.375s][info][gc          ] GC(0) Garbage Collection (Warmup) 16M(16%)->12M(12%)[0.403s][info][gc,start    ] GC(1) Garbage Collection (System.gc())[0.403s][info][gc,task     ] GC(1) Using 1 workers[0.403s][info][gc,phases   ] GC(1) Pause Mark Start 0.006ms[0.410s][info][gc,phases   ] GC(1) Concurrent Mark 7.316ms[0.410s][info][gc,phases   ] GC(1) Pause Mark End 0.006ms[0.410s][info][gc,phases   ] GC(1) Concurrent Mark Free 0.001ms[0.412s][info][gc,phases   ] GC(1) Concurrent Process Non-Strong References 1.621ms[0.412s][info][gc,phases   ] GC(1) Concurrent Reset Relocation Set 0.001ms[0.414s][info][gc,phases   ] GC(1) Concurrent Select Relocation Set 2.436ms[0.414s][info][gc,phases   ] GC(1) Pause Relocate Start 0.003ms[0.415s][info][gc,phases   ] GC(1) Concurrent Relocate 0.865ms[0.415s][info][gc,load     ] GC(1) Load: 0.65/0.79/0.63[0.415s][info][gc,mmu      ] GC(1) MMU: 2ms/99.7%, 5ms/99.8%, 10ms/99.9%, 20ms/99.9%, 50ms/100.0%, 100ms/100.0%[0.415s][info][gc,marking  ] GC(1) Mark: 1 stripe(s), 2 proactive flush(es), 1 terminate flush(es), 0 completion(s), 0 continuation(s) [0.415s][info][gc,marking  ] GC(1) Mark Stack Usage: 32M[0.415s][info][gc,nmethod  ] GC(1) NMethods: 983 registered, 129 unregistered[0.415s][info][gc,metaspace] GC(1) Metaspace: 9M used, 9M committed, 1088M reserved[0.415s][info][gc,ref      ] GC(1) Soft: 155 encountered, 0 discovered, 0 enqueued[0.415s][info][gc,ref      ] GC(1) Weak: 729 encountered, 580 discovered, 58 enqueued[0.415s][info][gc,ref      ] GC(1) Final: 0 encountered, 0 discovered, 0 enqueued[0.415s][info][gc,ref      ] GC(1) Phantom: 49 encountered, 47 discovered, 46 enqueued[0.415s][info][gc,reloc    ] GC(1) Small Pages: 6 / 12M, Empty: 0M, Relocated: 1M, In-Place: 0[0.415s][info][gc,reloc    ] GC(1) Large Pages: 2 / 4M, Empty: 2M, Relocated: 0M, In-Place: 0[0.415s][info][gc,reloc    ] GC(1) Forwarding Usage: 0M[0.415s][info][gc,heap     ] GC(1) Min Capacity: 8M(8%)[0.415s][info][gc,heap     ] GC(1) Max Capacity: 100M(100%)[0.415s][info][gc,heap     ] GC(1) Soft Max Capacity: 100M(100%)[0.415s][info][gc,heap     ] GC(1)                Mark Start          Mark End        Relocate Start      Relocate End           High               Low         [0.415s][info][gc,heap     ] GC(1)  Capacity:      100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)   [0.415s][info][gc,heap     ] GC(1)      Free:       84M (84%)          84M (84%)          84M (84%)          92M (92%)          92M (92%)          82M (82%)    [0.415s][info][gc,heap     ] GC(1)      Used:       16M (16%)          16M (16%)          16M (16%)           8M (8%)           18M (18%)           8M (8%)     [0.415s][info][gc,heap     ] GC(1)      Live:         -                 4M (5%)            4M (5%)            4M (5%)             -                  -          [0.415s][info][gc,heap     ] GC(1) Allocated:         -                 0M (0%)            2M (2%)            2M (2%)             -                  -          [0.415s][info][gc,heap     ] GC(1)   Garbage:         -                11M (11%)           9M (9%)            1M (1%)             -                  -          [0.415s][info][gc,heap     ] GC(1) Reclaimed:         -                  -                 2M (2%)           10M (10%)            -                  -          [0.415s][info][gc          ] GC(1) Garbage Collection (System.gc()) 16M(16%)->8M(8%)[0.416s][info][gc,heap,exit] Heap[0.416s][info][gc,heap,exit]  ZHeap           used 8M, capacity 100M, max capacity 100M[0.416s][info][gc,heap,exit]  Metaspace       used 9379K, committed 9600K, reserved 1114112K[0.416s][info][gc,heap,exit]   class space    used 1083K, committed 1216K, reserved 1048576K

从日志中可以看到 ZGC 的整个过程。默认情况下并没有开启分代式 ZGC,如果想开启分代式 ZGC,我们还需要加上 -XX:+ZGenerational 参数:

$ java -XX:+UseZGC -XX:+ZGenerational -Xmx100M -Xlog:gc* ZgcTest.java

这个输出比较多,此处就省略了,从输出中可以看到不同分代的回收情况。关于 ZGC,还有很多微调参数,详细内容可参考 ZGC 的官方文档

注意,在 Java 23 中分代式 ZGC 已经是默认选项,不需要再用 -XX:+ZGenerational 参数开启,另外,在 Java 24 中非分代模式已被正式移除。

小结

今天我们学习了 Java 21 引入的 分代式 ZGC(Generational ZGC) 这一新的垃圾回收特性,它在原有 ZGC 的基础上融入了分代回收的思想,进一步优化了应用的吞吐量和延迟性能。

单代 ZGC 汇聚了多个技术创新,包括 染色指针读屏障 等技术,具有亚毫秒级的 STW 延迟,但由于每次 GC 都要扫描整个堆,这会对应用吞吐量造成影响;分代式 ZGC 继承了 ZGC 的低延迟特性,同时通过分代设计,使得频繁发生的年轻代 GC 更加高效,而老年代 GC 的频率大幅降低,从而实现了更好的整体性能。

欢迎关注

如果这篇文章对您有所帮助,欢迎关注我的同名公众号:日习一技,每天学一点新技术

我会每天花一个小时,记录下我学习的点点滴滴。内容包括但不限于:

目标是让大家用5分钟读完就能有所收获,不需要太费劲,但却可以轻松获取一些干货。不管你是技术新手还是老鸟,欢迎给我提建议,如果有想学习的技术,也欢迎交流!

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Java Java 21 ZGC Generational ZGC Garbage Collection JVM Low Latency High Throughput Colored Pointers Load Barriers Generational Hypothesis
相关文章