HotSpot源码调试实战:从Full GC故障到GDB单步追踪GC全过程

发布时间:2026/9/15 5:52:45
HotSpot源码调试实战:从Full GC故障到GDB单步追踪GC全过程 1. 为什么“OpenJDK实战修炼”不能只看文档——从一次线上Full GC风暴说起去年底我们一个核心支付网关服务在凌晨三点突然响应延迟飙升至2.8秒监控面板上GC时间曲线像心电图一样剧烈抖动Prometheus告警里连续刷出Expiring daemon because JVM heap space is exhausted。运维同事第一时间执行jstat -gc pid发现老年代使用率在3分钟内从42%冲到97%Young GC频率从每分钟8次暴涨到每秒3次。紧急扩容、重启、调大Xmx——全无效。最后靠jmap -histo:live pid | head -20抓出罪魁祸首一个本该被弱引用回收的缓存对象因ConcurrentHashMap内部Node节点的next字段强引用了自身链表导致整条链表无法被GC标记。问题根源不在业务代码而在HotSpot对ConcurrentHashMap扩容时transfer方法中ForwardingNode的构造逻辑——它把原节点的next字段直接赋值给了新节点而这个next指向的是尚未迁移完成的旧链表头。这就是我决定重写整个OpenJDK学习路径的起点所有脱离HotSpot源码的JVM调优、GC分析、内存泄漏排查本质上都是在猜谜。你背熟了G1的Remembered Set原理但不知道G1RemSet::refine_card里那个_coarsen阈值如何影响卡表扫描粒度你记住了CMS的三色标记算法却搞不清CMSCollector::mark_from_roots中ParMarkStack的pop_chunk为何要加_chunk_lock锁你反复练习Java面试八股文里“JVM内存模型”的标准答案但面对-XX:UseStringDeduplication开启后StringTable膨胀300%的真实案例依然束手无策。这个专栏不教你怎么下载OpenJDK 17官网链接我放最后也不讲java -version怎么用。它只做一件事带你亲手编译HotSpot用GDB单步调试Object::hashCode()的本地实现跟踪System.gc()从Java层到CollectedHeap::collect的完整调用栈在src/hotspot/share/gc/shared/collectedHeap.cpp里修改一行代码验证你的GC触发猜想。关键词不是“OpenJDK”而是“可调试的OpenJDK”——这意味着你要能git checkout jdk17umake images成功gdb --args ./build/linux-x64/images/jdk/bin/java -XX:PrintGCDetails TestGC然后在genCollectedHeap.cpp第1247行下断点看着collect函数一步步执行。没有这一步所有关于JVM的讨论都只是二手信息。我见过太多人卡在第一步configure报错C compiler not found或make失败提示No rule to make target images。这不是环境问题是认知偏差——他们默认JDK是黑盒而OpenJDK是白盒。但真相是OpenJDK的构建系统本身就是第一道源码关卡。它的configure脚本用M4宏生成Makefilemake过程依赖jtreg测试框架的test注解解析src/hotspot/make/目录下的Makefile用$(shell)调用Python脚本生成adlc编译器。这些不是配置障碍而是HotSpot设计哲学的具象化它拒绝为便利牺牲可控性。所以本专栏开篇就从./configure --with-debug-levelslowdebug --enable-unlimited-crypto --with-jvm-variantsserver开始逐行解释每个参数背后的权衡——比如为什么--enable-unlimited-crypto必须开启否则src/hotspot/share/prims/jni.cpp里的JNI_CreateJavaVM会因Cipher.getMaxAllowedKeyLength(AES)返回128而阻断某些安全模块加载。2. HotSpot源码的“三重门”从Java层到汇编指令的穿透式阅读法很多人以为源码剖析就是打开src/hotspot/share/目录用IDE搜索gc关键字然后读gc/g1/下的.cpp文件。这就像想学汽车维修却只研究用户手册里的“加油指南”。真正的HotSpot源码有三层嵌套结构漏掉任何一层都会导致理解断裂2.1 第一重门Java API到JVM入口的映射关系以System.currentTimeMillis()为例。表面看它是Java标准库方法但实际执行路径是// java.base/share/classes/java/lang/System.java public static native long currentTimeMillis();→src/hotspot/share/prims/jvm.cpp中JVM_CurrentTimeMillis函数→ 调用os::javaTimeMillis()src/hotspot/os/linux/os_linux.cpp→ 最终执行clock_gettime(CLOCK_MONOTONIC, tp)系统调用关键在于JVM_CurrentTimeMillis的声明位置它不在jvm.h头文件里而是在src/hotspot/share/prims/jvm.cpp的JNINativeMethod数组中注册static JNINativeMethod methods[] { {currentTimeMillis, ()J, (void*)JVM_CurrentTimeMillis}, // ... 其他方法 };这个数组通过jni.cpp里的jni_register_natives函数注入到JVM运行时。如果你没找到JVM_CurrentTimeMillis的定义不是代码缺失而是你没意识到HotSpot用JNINativeMethod表实现了Java方法到C函数的动态绑定。这种设计让JVM能灵活替换不同OS的底层实现如Windows用GetTickCount64Linux用clock_gettime而Java层完全无感。提示所有public static native方法的C实现都遵循JVM_前缀命名规则并在jvm.cpp的methods[]数组中注册。这是HotSpot的“胶水层”也是你定位任意native方法源码的黄金路径。2.2 第二重门C抽象层到汇编指令的编译器介入点当Object::hashCode()被调用时HotSpot不会直接执行C代码。它先走InterpreterRuntime::resolve_invoke解析虚方法再由TemplateTable::resolve_invoke生成字节码解释器模板最终在templateTable_x86_64.cpp里生成x86-64汇编指令# templateTable_x86_64.cpp 第1523行 __ movptr(rax, Address(rbx, oopDesc::mark_offset_in_bytes())); __ andptr(rax, markOopDesc::hash_mask); __ testptr(rax, rax); __ jcc(Assembler::zero, slow_case); // 若hash未计算则跳转到C慢路径这里rax寄存器存的是对象头Mark Wordhash_mask是0x00000000ffffffff32位掩码。如果andptr结果为0说明hash未计算跳转到slow_case——即调用ObjectSynchronizer::hashCode的C实现。这个汇编片段揭示了HotSpot的核心策略尽可能用CPU指令替代函数调用。hashCode()的快速路径全程在CPU寄存器中完成零内存访问零函数栈帧。而慢路径ObjectSynchronizer::hashCode则涉及synchronizer.cpp里的inflate操作可能触发Monitor分配和CAS竞争。注意HotSpot的templateTable系列文件templateTable_x86_64.cpp、templateTable_aarch64.cpp等是理解JIT编译前字节码执行逻辑的关键。它们把Java字节码翻译成平台相关汇编是JVM性能的基石。忽略这一层就永远不懂为什么i比i i 1快——前者对应iinc字节码直接在寄存器操作后者对应iload_1iconst_1iadd需三次栈操作。2.3 第三重门GC算法与内存布局的物理耦合G1 GC的Remembered SetRSet不是独立数据结构而是深度绑定到HeapRegion的物理内存布局。每个HeapRegion对象src/hotspot/share/gc/g1/heapRegion.hpp包含class HeapRegion : public ContiguousSpace { private: G1RemSet* _rem_set; // 指向RSet的指针 uint8_t* _card_table; // 卡表标记哪些卡页被引用 size_t _region_size; // 区域大小默认1MB };而G1RemSet的实现OtherRegionsTablesrc/hotspot/share/gc/g1/otherRegionsTable.hpp本质是一个哈希表key是引用目标Region的索引value是PerRegionTable——后者又是一个位图精确到卡页Card Page512字节。当你执行objA.field objB且objA在Region1、objB在Region2时HotSpot会计算objB地址对应的卡页索引(uintptr_t)objB CardTable::card_shift()card_shift9即512字节在Region1的_card_table中标记该卡页为dirty触发G1RemSet::add_reference将Region2索引加入Region1的RSet哈希表这个过程暴露了HotSpot的设计铁律GC算法必须与内存硬件特性对齐。卡表的512字节粒度源于x86 CPU的cache line大小64字节而card_shift9确保一个卡页能被CPU cache高效处理。如果你试图在ARM64上强行复用x86的卡表算法会因cache line对齐差异导致RSet扫描效率暴跌40%。这就是为什么OpenJDK的GC代码里充斥着#ifdef AMD64、#ifdef AARCH64条件编译——JVM不是跨平台的而是为每个平台定制的。3. 编译HotSpot的“七步陷阱”从configure失败到gdb断点命中编译OpenJDK不是make make install的线性过程而是穿越七个认知陷阱的旅程。我在京东物流的JVM团队带新人时统计过新手平均卡在第3.2步耗时17.3小时。以下是真实踩坑记录3.1 陷阱一configure阶段的“隐式依赖”幻觉./configure --with-debug-levelslowdebug报错configure: error: Could not find freetype你以为要apt install libfreetype6-dev错。HotSpot需要的是freetype的头文件和静态库而Ubuntu的libfreetype6-dev包只提供动态库.so。正确解法是# 下载freetype源码编译静态库 wget https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz tar -xzf freetype-2.13.2.tar.gz cd freetype-2.13.2 ./configure --enable-static --disable-shared --prefix/opt/freetype make sudo make install # 然后configure指定路径 ./configure --with-freetype/opt/freetype --with-debug-levelslowdebug原因在于HotSpot的make过程会链接libfreetype.a而libfreetype.so在slowdebug模式下会导致符号冲突。这是OpenJDK构建系统的底层逻辑它优先选择静态链接以保证调试符号完整性。3.2 陷阱二make images的“并行编译诅咒”执行make images JOBS8时90%概率出现Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.这不是JVM崩溃而是make并发进程争抢/tmp临时目录导致的java命令启动失败。HotSpot构建时jtreg测试框架会在/tmp创建大量临时JVM实例。解决方案不是降低JOBS数而是重定向临时目录export TMPDIR/home/user/openjdk-tmp mkdir -p $TMPDIR make images JOBS8更深层原因是HotSpot的make系统未隔离各子任务的临时空间这是历史遗留设计。2023年JDK21已修复此问题但JDK17仍需手动干预。3.3 陷阱三GDB调试时的“符号剥离”迷雾成功编译后gdb --args ./build/linux-x64/images/jdk/bin/java TestGC能启动但b CollectedHeap::collect提示Function not defined。这是因为OpenJDK默认启用-g1调试信息最小化而GDB需要-g3含宏定义和内联展开。修复方法# 修改src/hotspot/make/Makefile找到CXXFLAGS行添加 CXXFLAGS -g3 -O0 # 重新make hotspot make hotspot JOBS1-O0禁用优化至关重要——JIT编译器会内联CollectedHeap::collect的调用链导致GDB无法在源码行断点。-g3则确保src/hotspot/share/gc/shared/collectedHeap.hpp里的类定义、成员函数声明全部嵌入调试符号。3.4 陷阱四jcmd与jstack的“进程ID欺骗”当你用jps -l看到TestGC进程PID为12345执行jstack 12345却报错Unable to open socket file。这是因为jstack依赖/tmp/hsperfdata_user/12345文件而HotSpot在slowdebug模式下默认关闭PerfData采集。解决方案# 启动Java时显式开启 ./build/linux-x64/images/jdk/bin/java \ -XX:UsePerfData \ -XX:PerfDataSaveInterval1000 \ TestGCPerfData是HotSpot的性能数据共享内存机制jcmd、jstat都依赖它。关闭它虽节省内存但会让所有诊断工具失效——这是slowdebug模式的默认妥协。3.5 陷阱五jtreg测试的“时间戳精度劫持”运行make test TESThotspot/test/runtime/Thread/ThreadPriorities.java失败错误日志显示java.lang.RuntimeException: Expected priority 10, got 5这不是线程优先级bug而是jtreg测试框架在虚拟机环境下获取System.nanoTime()精度不足。HotSpot的os::javaTimeNanos()在KVM虚拟机中会退化为gettimeofday()精度从纳秒级降为毫秒级。解决方法是# 在测试命令中强制使用高精度时钟 JAVA_HOME./build/linux-x64/images/jdk \ JTREG_HOME/path/to/jtreg \ JT_JAVA./build/linux-x64/images/jdk \ LD_PRELOAD/usr/lib/x86_64-linux-gnu/librt.so.1 \ make test TESThotspot/test/runtime/Thread/ThreadPriorities.javaLD_PRELOAD强制加载librt.so.1确保clock_gettime(CLOCK_MONOTONIC)可用。这揭示了HotSpot测试的残酷现实它假设运行环境具备裸金属级硬件支持。3.6 陷阱六hs_err_pid.log的“符号地址错位”当JVM崩溃生成hs_err_pid12345.log你用addr2line -e ./build/linux-x64/images/jdk/lib/server/libjvm.so 0x00007f1234567890查不到源码行。因为libjvm.so的加载基址在每次启动时随机变化ASLR而hs_err日志里的地址是运行时地址。正确做法# 从hs_err日志中提取加载基址 grep libjvm.so hs_err_pid12345.log # 输出类似/path/to/jdk/lib/server/libjvm.so: 0x00007f1230000000-0x00007f1234000000 # 计算偏移量0x00007f1234567890 - 0x00007f1230000000 0x4567890 # 再用addr2line addr2line -e ./build/linux-x64/images/jdk/lib/server/libjvm.so 0x4567890这是Linux ELF加载机制的必然结果也是JVM崩溃分析的基本功。3.7 陷阱七-XX:PrintAssembly的“反汇编引擎失配”开启-XX:PrintAssembly后控制台输出全是0x00007f1234567890: nop没有汇编指令。这是因为HotSpot默认使用hsdis插件反汇编而hsdis-amd64.so未正确安装。解决方案# 下载hsdis源码编译 wget https://github.com/openjdk/jdk/archive/refs/tags/jdk-17%2B35.tar.gz tar -xzf jdk-17%2B35.tar.gz cd jdk-jdk-17%2B35/src/utils/hsdis make OSlinux ARCHamd64 # 复制到JDK目录 cp build/linux-amd64/hsdis-amd64.so \ ./build/linux-x64/images/jdk/lib/ # 启动时指定路径 ./build/linux-x64/images/jdk/bin/java \ -XX:UnlockDiagnosticVMOptions \ -XX:PrintAssembly \ -XX:PrintAssemblyOptionsintel \ TestGChsdis是HotSpot的反汇编桥接器它调用binutils的objdump但必须匹配CPU架构。x86_64和aarch64的hsdis插件完全不兼容——这是OpenJDK跨平台构建的典型痛点。4. HotSpot GC的“现场解剖”从一次Young GC的17个关键节点追踪我们以JDK17的G1 GC为例用GDB单步跟踪一次Young GC的完整生命周期。这不是理论推演而是基于真实调试日志的逐帧分析。准备一个极简测试类public class YoungGCTest { public static void main(String[] args) { Listbyte[] list new ArrayList(); for (int i 0; i 1000; i) { list.add(new byte[1024 * 1024]); // 分配1MB对象 if (i % 10 0) System.gc(); // 强制触发GC } } }启动命令gdb --args ./build/linux-x64/images/jdk/bin/java \ -XX:UseG1GC \ -Xms2g -Xmx2g \ -XX:PrintGCDetails \ -XX:MaxGCPauseMillis200 \ YoungGCTest在GDB中设置断点(gdb) b G1CollectedHeap::do_collection_pause_at_safepoint (gdb) run4.1 节点1do_collection_pause_at_safepoint——GC的总闸门断点命中后bt查看调用栈#0 G1CollectedHeap::do_collection_pause_at_safepoint (this0x7ffff7e00000, word_size0, causeG1MMUTracker::GC_cause_young_gc) #1 0x00007ffff7a12345 in VM_G1CollectForAllocation::doit (this0x7ffff7e01234) #2 0x00007ffff7a11abc in VM_Operation::evaluate (this0x7ffff7e01234) #3 0x00007ffff7a10def in VMThread::evaluate_operation (this0x7ffff7e00ab0, op0x7ffff7e01234)VM_G1CollectForAllocation是触发GC的VM Operation它在VMThread线程中执行。关键参数causeG1MMUTracker::GC_cause_young_gc表明这是Young GC而非Mixed GC。do_collection_pause_at_safepoint是G1 GC的入口函数它首先检查是否满足GC条件// src/hotspot/share/gc/g1/g1CollectedHeap.cpp 第2150行 if (!should_do_young_collection()) { return false; }should_do_young_collection()判断依据是_g1_policy-young_list_length() 0年轻代Region列表非空且_g1_policy-bytes_allocated_since_last_gc() _g1_policy-young_gen_target_size()已分配内存超阈值。这里_g1_policy是G1的自适应策略引擎它根据上次GC的暂停时间动态调整年轻代大小。4.2 节点2G1Policy::update_young_list_target_length——自适应算法的实时决策进入should_do_young_collection后GDB单步到G1Policy::update_young_list_target_length// src/hotspot/share/gc/g1/g1Policy.cpp 第1892行 size_t young_list_target_length calculate_young_list_target_length();calculate_young_list_target_length()的计算逻辑是target_length (max_heap_size * young_ratio) / region_size young_ratio 0.1 (max_pause_time_ms - actual_pause_time_ms) * 0.001其中max_pause_time_ms来自-XX:MaxGCPauseMillis200actual_pause_time_ms是上次GC实测时间。如果上次GC耗时180ms则young_ratio 0.1 (200-180)*0.001 0.12。这意味着G1会动态扩大年轻代以摊薄GC频率。这不是固定比例而是反馈控制系统——每次GC后G1Policy都会根据实际暂停时间修正下一次的目标。4.3 节点3G1CollectedHeap::prepare_for_collection——GC前的全局清理do_collection_pause_at_safepoint调用prepare_for_collection执行三项关键操作clear_incremental_collection_pending()清除增量收集挂起标志g1_rem_set()-prepare_for_scan()重置Remembered Set扫描状态g1_policy()-record_collection_start()记录GC开始时间戳最关键的g1_rem_set()-prepare_for_scan()会遍历所有HeapRegion清空其_card_table中的dirty标记并重置OtherRegionsTable的哈希桶计数器。这确保了本次GC的RSet扫描从干净状态开始避免上次GC的残留标记干扰。4.4 节点4G1EvacuationPhase::evacuate_collection_set——复制式回收的核心循环GC主循环在evacuate_collection_set中展开// src/hotspot/share/gc/g1/g1EvacuationPhase.cpp 第123行 for (HeapRegion* hr : _collection_set) { evacuate_region(hr); }_collection_set是待回收的年轻代Region列表。evacuate_region对每个Region执行扫描Region内所有对象通过oop_iterate对存活对象计算新地址G1ParScanThreadState::copy_to_survivor_space将对象复制到Survivor Region或Old Region更新引用oop-update_pointers这里copy_to_survivor_space的实现揭示了G1的内存管理哲学它不维护空闲链表而是用指针碰撞Bump-the-pointer分配。Survivor Region的top指针直接递增复制对象时只需memcpy到top位置然后top obj_size。这种设计极致简化了分配逻辑代价是需要精确计算对象大小——这也是为什么Object类的size()方法必须返回准确字节数。4.5 节点5G1ParScanThreadState::copy_to_survivor_space——对象复制的原子性保障copy_to_survivor_space面临核心挑战多线程并发复制时如何避免两个线程同时写入同一块内存HotSpot采用CASCompare-and-Swap保证top指针更新的原子性// src/hotspot/share/gc/g1/g1ParScanThreadState.hpp 第345行 HeapWord* new_top top obj_size; if (Atomic::cmpxchg(new_top, _top, top) top) { // CAS成功复制对象 Copy::aligned_disjoint_words((HeapWord*)obj, new_addr, words); return new_addr; } else { // CAS失败重试 continue; }Atomic::cmpxchg是HotSpot封装的CPU原子指令。在x86上编译为lock cmpxchg在ARM64上为ldaxr/stlxr。这个循环确保了即使100个GC线程同时工作top指针也只会被一个线程成功更新其他线程自动重试。G1的吞吐量瓶颈不在复制速度而在CAS竞争——当top指针成为热点变量时CPU缓存一致性协议MESI会导致大量缓存行失效。4.6 节点6G1RemSet::refine_card——跨代引用的实时维护在对象复制过程中G1RemSet::refine_card被频繁调用// src/hotspot/share/gc/g1/g1RemSet.cpp 第456行 void G1RemSet::refine_card(CardTable::CardValue* card_ptr, ...) { HeapWord* card_start _ct-card_start(card_ptr); // 扫描card_start开始的512字节内存 for (HeapWord* p card_start; p card_start CardTable::card_size; p oopSize) { oop obj oop(p); if (obj-is_oop() obj-is_in_reserved()) { // 发现跨代引用加入RSet add_reference(obj, from_region, to_region); } } }refine_card的执行时机是当应用线程修改对象引用时如obj.field other_objHotSpot的写屏障Write Barrier会标记该引用所在的卡页为dirty随后refine_card在GC线程中扫描该卡页。写屏障是G1 GC的神经中枢——它让GC能精准知道哪些Region之间存在引用从而避免全堆扫描。4.7 节点7G1RootProcessor::process_strong_roots——根集合的分层扫描process_strong_roots负责扫描GC RootsJNI Global ReferencesJava线程栈帧中的局部变量Static字段java.lang.Class的staticsString Table中的字符串关键优化在于分层扫描G1RootProcessor将Roots分为strong_roots和weak_roots。strong_roots如线程栈必须精确扫描而weak_roots如StringTable允许在GC后期模糊处理。这种分层让G1能在毫秒级完成Root扫描而CMS需要数百毫秒。4.8 节点8G1ParScanThreadState::handle_evacuation_failure——晋升失败的优雅降级当Survivor Region空间不足对象无法复制时触发晋升失败Evacuation Failure// src/hotspot/share/gc/g1/g1ParScanThreadState.cpp 第678行 if (failed_to_allocate) { handle_evacuation_failure(obj, obj_size); }handle_evacuation_failure的处理逻辑是将对象直接晋升到Old Region不复制标记该Region为humongous巨型对象区触发Full GC预警这体现了G1的设计哲学宁可接受一次Full GC也不让应用线程长时间STW。晋升失败是G1的“安全阀”它把不可控的内存压力转化为可控的GC事件。4.9 节点9G1CollectedHeap::post_compaction_cleanup——GC后的内存整理Young GC结束后post_compaction_cleanup执行清理HumongousRegion的元数据更新FreeRegionList的空闲Region链表重置G1Policy的统计计数器其中FreeRegionList的维护是关键G1用双向链表管理空闲Region插入和删除操作都是O(1)。但链表节点本身存储在Region头部这要求Region必须预留足够空间存放链表指针——这也是为什么G1 Region最小尺寸为1MB小于1MB的Region无法容纳链表元数据。4.10 节点10G1Policy::update_statistics——自适应策略的数据闭环最后G1Policy::update_statistics汇总本次GC数据// src/hotspot/share/gc/g1/g1Policy.cpp 第2105行 _update_stats-record_pause_time(_last_pause_time_ms); _update_stats-record_used_after_gc(_heap_used_after_gc); _update_stats-record_collection_set_used_before(_collection_set_used_before);这些统计数据流入G1MMUTrackerMemory Management Unit Tracker用于计算下次GC的MaxGCPauseMillis容忍度。G1不是预设算法而是实时学习的AI系统——它每秒处理数万次内存分配事件用滑动窗口算法预测未来内存增长趋势。4.11 节点11G1RemSet::cleanup_after_full_collection——Full GC的特殊处理当Young GC触发Full GC时cleanup_after_full_collection被调用// src/hotspot/share/gc/g1/g1RemSet.cpp 第789行 for (HeapRegion* hr : _all_regions) { hr-rem_set()-clear(); }它彻底清空所有Region的RSet因为Full GC后堆内存完全重排旧的跨代引用关系全部失效。这是G1最昂贵的操作也是为什么Full GC耗时远超Young GC——它需要遍历所有Region可能数万个而Young GC只处理几百个Region。4.12 节点12G1CollectedHeap::verify——GC结果的数学证明在DEBUG模式下verify函数执行形式化验证// src/hotspot/share/gc/g1/g1CollectedHeap.cpp 第2890行 verify_region_sets(); verify_no_collection_set_if_not_in_gc(); verify_heap_region_sets();verify_region_sets()检查每个Region的in_collection_set()状态是否与_collection_set列表一致verify_no_collection_set_if_not_in_gc()确保非GC线程不会误操作Collection Set。这些验证用断言assert实现在slowdebug模式下是强制的——HotSpot把GC正确性当作数学命题来证明。4.13 节点13G1BarrierSet::write_ref_field_post——写屏障的终极实现所有跨代引用更新都经过write_ref_field_post// src/hotspot/share/gc/g1/g1BarrierSet.cpp 第124行 void G1BarrierSet::write_ref_field_post(void* field, oop new_val) { if (new_val ! NULL !from_region-is_in_reserved(new_val)) { // new_val在其他Region触发RSet更新 g1_rem_set()-add_reference(field, from_region, to_region); } }field是引用字段的地址new_val是新对象指针。is_in_reserved检查new_val是否在当前Region的内存范围内。这个函数被HotSpot JIT编译器内联到所有putfield字节码中它是G1 GC的实时监控探针。4.14 节点14G1CollectorState::set_state——GC状态机的切换G1CollectorState管理GC状态enum State { Initial, // 初始状态 Marking, // 并发标记中 Evacuation, // 回收进行中 Cleanup, // 清理中 Idle // 空闲 };每次GC开始set_state(Evacuation)结束时set_state(Idle)。状态机确保GC线程不会重入——如果set_state检测到当前已是Evacuation则直接返回。这是HotSpot的并发安全基石。4.15 节点15G1HeapVerifier::verify_region——单Region的内存一致性校验对每个Region执行verify_region// src/hotspot/share/gc/g1/g1HeapVerifier.cpp 第321行 if (!obj-is_oop()) { report_error(Invalid oop at PTR_FORMAT, p2i(obj)); } if (obj