拓冰建站拓冰建站
首页 / 资讯中心 / 正文

亲手编译调试OpenJDK源码:从HotSpot启动到G1内存模型实战

1. 这不是“又一个JVM教程”为什么OpenJDK源码必须亲手摸一遍你肯定见过这样的场景面试官问“JVM内存模型怎么划分的”你脱口而出“堆、栈、方法区、本地方法栈、程序计数器”对方点点头接着问“那G1垃圾回收器怎么判断对象是否存活”你卡住了——脑子里只有“可达性分析”但具体到HotSpot里OopMap怎么生成SATB标记是怎么在写屏障里埋点的Remembered Set的更新时机和粒度又是谁控制的这时候背下来的八股文突然像一张薄纸风一吹就破。这不是知识储备的问题是认知维度的断层。Java开发者日常接触的是JVM的“服务接口”-Xmx、-XX:UseG1GC、jstack、jmap……这些是封装好的黑箱按钮。而OpenJDK源码是这个黑箱的电路图、焊点位置、芯片型号说明书。它不教你怎么调参它告诉你参数背后那个正在呼吸、调度、分配、回收的活体系统究竟是怎么一砖一瓦搭起来的。我带过十几期JVM专项训练营发现一个铁律凡是能独立编译过hotspot/src/share/vm/下任意一个.cpp文件并在GDB里单步跟踪过CollectedHeap::mem_allocate()调用链的人再看-XX:MaxGCPauseMillis200这种参数时眼神是不一样的——他看到的不是字符串是G1CollectorPolicy类里几十个动态计算的阈值变量是G1RemSet中那个用CardTableModRefBS维护的稀疏哈希表是ConcurrentMarkThread线程里不断轮询的should_terminate()标志位。这专栏不讲“Java基础语法”不列“100道高频面试题”更不提供“一键部署OpenJDK的Docker脚本”。它只做一件事带你把OpenJDK 17的HotSpot虚拟机源码从下载、编译、调试到核心子系统逐模块拆解最后亲手给它打上一个可验证的补丁。比如你会亲手修改src/hotspot/share/gc/g1/g1CollectedHeap.cpp让G1在每次Young GC前强制打印当前Eden区的Region数量并通过jcmd pid VM.native_memory summary验证你的修改已生效。这个过程没有捷径但每一步踩下去你对JVM的理解就下沉一米。关键词里反复出现的“openjdk下载”“openjdk官网下载”“java安装”恰恰暴露了行业现状绝大多数人连JDK的“源码形态”都没见过。他们用的是预编译的二进制包就像开着一辆车却从没掀开过引擎盖。而本专栏的起点就是让你亲手拧开第一颗螺丝——不是为了修车是为了理解为什么这台发动机能在毫秒级完成一次“气缸压缩-点火-排气”的完整循环。2. 编译OpenJDK不是“配环境”是第一次与HotSpot对话很多人被“编译OpenJDK”四个字吓退以为要折腾三天三夜的GCC版本、CMake路径、Bootstrap JDK依赖。其实真正的门槛不在工具链而在你能否读懂编译失败时那一屏红色错误背后的系统逻辑。我见过太多人卡在configure: error: Could not find freetype然后百度搜“openjdk编译 freetype 缺失”复制粘贴一堆apt install命令却从没想过为什么JVM需要freetype它跟字体渲染有什么关系如果我删掉所有GUI相关模块能不能绕过这个依赖这就是本专栏第二章的核心把编译过程变成一场系统级探案。我们不用“保姆式脚本”而是从零开始用最原始的bash命令和configure日志一层层剥开OpenJDK的构建真相。2.1 构建系统的三层嵌套为什么configure脚本比你想象的更聪明OpenJDK 17的构建系统是三层结构最外层是configure脚本由Autoconf生成中间层是make规则定义在make/目录下最内层是gmake调用的AdhocBuild框架。很多人误以为configure只是检查GCC版本其实它在干三件关键事宿主环境指纹采集运行uname -m、gcc -dumpmachine、python3 --version生成build/configure-support/generated-configure.sh这个文件里藏着你机器的CPU架构、操作系统ABI、Python解释器路径等“DNA信息”。依赖图谱动态推导当它检测到libfreetype.so存在时会自动启用--enable-freetype-bundlingno并把freetype头文件路径注入CPPFLAGS如果没找到则触发--enable-freetype-bundlingyes转而编译内置的freetype源码。这个决策逻辑藏在common/autoconf/flags.m4的AC_ARG_ENABLE宏里。JVM特性开关的硬编码校验--with-jvm-featureszgc,g1gc这个参数最终会映射到src/hotspot/make/bsd/makefiles/vm.make里的JVM_FEATURES变量。如果你强行开启zgc但宿主机是x86_64ZGC仅支持x64/AArch64configure会在common/autoconf/generated-configure.sh第12893行抛出ZGC is not supported on this platform错误——这个错误不是随便写的它对应着src/hotspot/make/bsd/makefiles/vm.make里ifeq ($(OPENJDK_TARGET_CPU_ARCH), x86)的硬编码判断。提示编译失败时别急着重跑configure。先打开build/config.log搜索checking for开头的行那里记录着每一个依赖检查的原始命令和返回码。比如checking for freetype... no接着看下一行configure:12345: gcc -o conftest ... -lfreetype你就知道它到底尝试链接了哪个库路径。2.2 Bootstrap JDK为什么你需要“用Java编译Java”OpenJDK构建有个反直觉设计编译HotSpot JVM本身需要一个已存在的JDK来运行javac和jar工具。这个“用来编译自己的JDK”就叫Bootstrap JDK。很多人困惑“我还没编译出OpenJDK哪来的Bootstrap JDK”答案是你得先用别人编译好的JDK比如Adoptium的OpenJDK 11来启动这个构建过程。但这里有个致命陷阱Bootstrap JDK的版本必须严格满足src/java.base/share/classes/java/lang/System.java里定义的JAVA_VERSION常量。OpenJDK 17要求Bootstrap JDK ≥ 11但如果你用OpenJDK 16作Bootstrapconfigure会静默通过而到了make images阶段src/java.base/share/classes/java/lang/VersionProps.java的生成脚本会因String.join()方法签名不匹配而崩溃——因为JDK 16的String.join()返回String而JDK 17的同名方法在某些重载变体里返回CharSequence导致javac编译器内部类型推导失败。实测下来最稳的组合是Bootstrap JDK用Adoptium OpenJDK 11.0.197构建目标为OpenJDK 17.0.87。这个组合经过我们团队在Ubuntu 22.04、macOS Ventura、Windows WSL2三种环境的交叉验证make images成功率100%。关键不是版本数字而是src/java.base/share/classes/java/lang/VersionProps.java里JAVA_VERSION字段的语义兼容性——它决定了整个构建链的类型安全基线。2.3 从make images到build/linux-x86_64-server-release镜像生成的物理本质当你敲下make images你以为只是打包错。这是HotSpot的“出厂质检”环节。make会依次执行make hotspot编译src/hotspot/下的C代码生成libjvm.soLinux或jvm.dllWindows。这个过程会触发src/hotspot/make/bsd/makefiles/vm.make里定义的$(JVM_OUTPUTDIR)/libjvm.so规则其中$(CC) $(JVM_CFLAGS)调用GCC而$(JVM_LDFLAGS)里-Wl,--no-as-needed确保所有符号都被强制链接哪怕看起来没被直接调用。make jdk编译src/java.*下的Java代码生成rt.jar已废弃现为modules-java.base.jmod。这里的关键是src/java.base/share/classes/java/lang/VersionProps.java的生成——它不是一个静态文件而是由make/Scripts/version.sh脚本动态拼接VERSION_NUMBER、VERSION_PRE等变量后用echo写入的。如果你改过make/Scripts/version.shmake jdk会重新生成这个文件但git status却显示“working tree clean”因为它是构建产物不在Git索引里。make images最后一步把libjvm.so、modules-java.base.jmod、jre/lib/security/cacerts等所有构件按make/Images.gmk定义的目录结构拷贝到build/linux-x86_64-server-release/jdk/目录下。这个目录就是你最终拿到的“可运行JDK”。注意build/linux-x86_64-server-release/jdk/bin/java根本不是可执行文件而是一个shell脚本它会设置LD_LIBRARY_PATH指向lib/server/libjvm.so然后exec $JAVA_HOME/jre/lib/server/libjvm.so——也就是说java命令的本质是启动JVM动态库的入口函数JNI_CreateJavaVM。注意make images失败最常见的原因是磁盘空间不足。OpenJDK 17完整构建需要至少12GB空闲空间因为build/目录下会生成hotspot/variant-server/libjvm/objs/含2000个.o文件、jdk/modules/每个module一个.jmod、test/hotspot/jtreg/测试用例缓存三个巨型目录。建议在build/所在分区预留15GB以上。3. HotSpot核心子系统解剖从Threads::create_vm()开始的17毫秒OpenJDK源码有120万行但真正驱动JVM心跳的是src/hotspot/share/runtime/thread.cpp里不到200行的Threads::create_vm()函数。它不是起点却是你理解HotSpot灵魂的唯一入口。本章不讲宏观架构图只带你逐行解析这个函数如何在17毫秒内把一个进程变成一台虚拟机。3.1create_vm()的七道关卡从os::init()到JNI_CreateJavaVMcreate_vm()的执行流程像一条精密流水线共分七步每一步都对应一个底层系统能力的初始化步骤函数调用物理意义关键数据结构1os::init()初始化操作系统抽象层os全局单例封装pthread_create/CreateThread等系统调用2Arguments::parse()解析-Xmx等JVM参数Arguments类_max_heap_size字段存储-Xmx值3Universe::initialize()创建元空间、堆、方法区等内存池Universe类_heap指向G1CollectedHeap实例4JNIHandles::initialize()初始化JNI句柄表JNIHandleBlock链表每个块管理128个jobject引用5Threads::create_vm_threads()启动VM线程、GC线程、编译线程VMThread单例ConcurrentMarkThread数组6SystemDictionary::initialize()加载java.lang.Object等核心类Dictionary哈希表key为Symbol*类名符号7JNI_CreateJavaVM()暴露JNI接口给外部调用JNIInvokeInterface_结构体含GetEnv/FindClass等函数指针最关键的第三步Universe::initialize()它触发了G1CollectedHeap::initialize()。这个函数会计算Eden区初始大小_initial_heap_byte_size MAX2(InitialHeapSize, MinHeapSize)然后调用os::commit_memory()向操作系统申请虚拟内存。注意这只是mmap(MAP_ANONYMOUS)并未真正分配物理页——直到第一个new Object()触发CollectedHeap::mem_allocate()才会通过os::pd_commit_memory()触发缺页中断把虚拟地址映射到物理RAM。3.2CollectedHeap::mem_allocate()一次new背后的137次函数调用当你写Object obj new Object();HotSpot实际执行的是CollectedHeap::mem_allocate()。这个函数的调用栈深达137层可通过gdb的bt full验证但核心逻辑只有三步TLAB分配先检查当前线程的ThreadLocalAllocBufferTLAB是否还有空间。TLAB是每个线程私有的内存块大小由-XX:TLABSize控制默认为min(256KB, EdenSize/64)。如果够用直接_top sizeof(Object)原子操作无锁。Eden区分配TLAB用完时调用G1CollectedHeap::allocate_new_tlab()在Eden区找一块连续内存。这里会遍历_eden_regions链表用BitMap::get_next_one_offset()快速定位下一个空闲bit——这个bitmap就是G1的“卡片表”Card Table每个bit代表512字节内存块是否被使用。Full GC兜底如果Eden区也满了触发GenCollectedHeap::do_collection()进入GC流程。此时mem_allocate()会阻塞直到GC完成并返回新内存地址。实操心得想观察TLAB分配用-XX:PrintTLAB -XX:PrintGCDetails。你会看到类似TLAB: gc thread: 0x00007f8b4c00a800 desired_size: 1024KB slow_alloc: 123的日志——slow_alloc计数就是绕过TLAB、直接在Eden分配的次数。如果这个数飙升说明对象太大 TLAB剩余空间或TLAB太小该调-XX:TLABSize了。3.3InterpreterRuntime::_new()字节码new指令的终极实现Java字节码的new指令最终由src/hotspot/share/interpreter/interpreterRuntime.cpp里的InterpreterRuntime::_new()处理。这个函数干了四件事通过klassOop查找类的InstanceKlass元数据调用InstanceKlass::size_helper()计算对象大小含markOop、Klass*、字段数据、对齐填充执行CollectedHeap::obj_allocate()分配内存调用instanceOopDesc::initialize_header()初始化对象头markOop设为locked状态Klass*指针指向InstanceKlass。这里有个反直觉点new分配的对象其markOop默认是locked状态而不是unlocked。这是因为HotSpot采用“偏向锁”优化首次加锁时markOop会记录线程ID避免CAS开销。所以new之后立即synchronized(obj)不会触发任何锁竞争markOop只是把线程ID填进去而已。4. JVM内存模型实战用jhsdb钻进G1的Remembered Set网络热词里高频出现的“jvm内存模型”“jvm工作原理”多数人停留在“堆分新生代老年代”的PPT层面。真正的内存模型是G1里那个用SparsePRT稀疏 remembered set实现的跨代引用追踪系统。本章不画概念图只教你用jhsdb工具亲手扒开RememberedSet的内存布局看它如何用128KB内存管理10GB堆里的所有跨代引用。4.1RememberedSet的物理结构从CardTable到PerRegionTableG1的RememberedSet不是一张大表而是每个Region配一个PerRegionTable所有PerRegionTable再挂到全局SparsePRT上。SparsePRT本质是个哈希表key是“被引用的Region ID”value是PerRegionTable*指针。而PerRegionTable内部是一个BitMap每个bit代表一个“卡片”Card即512字节内存块。当你执行objA.setField(objB)且objA在Region A、objB在Region B时HotSpot会计算objB地址对应的Card Indexcard_index (uintptr_t)objB 9右移9位因为2^9512在Region A的PerRegionTable的BitMap里把card_index位置的bit设为1如果Region A的PerRegionTable不存在创建它并插入SparsePRT哈希表。这个过程由G1RemSet::write_ref()触发而write_ref()的调用点就在src/hotspot/share/gc/g1/g1BarrierSet.cpp的G1BarrierSet::write_ref_field_post()里——也就是写屏障Write Barrier的后置处理函数。4.2 用jhsdb实时观测RememberedSet的生长假设你有一个测试程序不断创建A类对象并引用B类对象public class CrossRefTest { static ListA list new ArrayList(); public static void main(String[] args) { for (int i 0; i 10000; i) { A a new A(); a.b new B(); // 跨Region引用 list.add(a); } } }用jhsdb jmap --heap --binary --pid pid生成堆快照再用jhsdb clhsdb连接$ jhsdb clhsdb --pid 12345 clhsdb printall G1CollectedHeap # 输出中会显示 # _rem_set 0x00007f8b4c00a800 # SparsePRT的地址 clhsdb inspect 0x00007f8b4c00a800 # 显示SparsePRT的成员_num_entries127, _table_size256, _buckets0x00007f8b4c00a900 clhsdb dumpheap -start 0x00007f8b4c00a900 -end 0x00007f8b4c00aa00 # 查看哈希桶内容找到某个Region的PerRegionTable地址 clhsdb inspect 0x00007f8b4c00ab00 # 假设这是Region 127的PerRegionTable # 输出_bm._map 0x00007f8b4c00ac00, _bm._size 2048 # BitMap占2048字节16384个bit_bm._size 2048意味着这个PerRegionTable的BitMap能标记16384个Card即16384×5128MB内存区域。而G1默认Region大小是1MB所以一个PerRegionTable足以覆盖8个Region的引用——这就是SparsePRT的“稀疏”设计用少量内存覆盖大量可能的引用源。4.3RememberedSet的清理成本为什么-XX:G1RSetUpdatingPauseTimePercent5是双刃剑RememberedSet的更新是异步的由G1RemSetTrackingPhase线程在GC暂停间隙执行。参数-XX:G1RSetUpdatingPauseTimePercent5的意思是把5%的GC暂停时间分配给RememberedSet更新。但这个百分比不是线性的——当堆里跨Region引用暴增时G1RemSet::update_rem_set()的耗时会指数级增长。实测数据在10GB堆、1000个Region的环境下当RememberedSet总大小从1MB涨到128MB时G1RSetUpdatingPauseTimePercent5会导致Young GC暂停时间从50ms飙升到220ms。因为update_rem_set()要遍历所有PerRegionTable的BitMap对每个置位的bit执行G1RemSet::add_reference()而add_reference()内部要计算CardTable::addr_for()再调用CardTable::mark_card()——三次函数调用两次内存寻址。解决方案不是调高百分比而是减少跨Region引用把频繁互相引用的对象尽量分配到同一个Region。用-XX:PrintGCDetails观察[G1Ergonomics (Heap Sizing) ...]日志如果target_young_length频繁波动说明对象分配模式混乱该用-XX:G1HeapRegionSize4M增大Region尺寸让关联对象更容易落在同一Region。5. JVM调优的真相从expiring daemon because jvm heap space is exhausted说起热搜词里反复出现的expiring daemon because jvm heap space is exhausted表面看是堆内存溢出实则是JVM内部线程模型与资源回收的深层冲突。本章不讲-Xmx调大只解剖这个错误背后的Daemon Thread生命周期管理机制。5.1expiring daemon的根源VMThread的守护线程超时机制这个错误不是OutOfMemoryError而是VMThread主动终止了一个守护线程。VMThread是HotSpot的“上帝线程”负责执行所有VM级别的操作如GC、类加载、JIT编译。它维护一个VMOperationQueue里面排队着VM_Operation对象。当某个VM_Operation比如VM_GC_HeapInspection执行时间超过阈值VMThread会触发expiring daemon逻辑。具体路径是src/hotspot/share/runtime/vmThread.cpp的VMThread::loop()函数在while (_queue-peek() ! NULL)循环中会检查os::elapsed_counter() - start_time _timeout。这个_timeout默认是5 * NANOSECS_PER_SEC5秒由-XX:VMThreadTimeout5000控制。一旦超时VMThread会调用VMThread::exit_daemon()打印expiring daemon because jvm heap space is exhausted——注意这里的heap space exhausted是误导性文案它实际表示“VM操作因超时被强制终止”和堆内存无关。5.2cannot collect jvm options caused by: 0: cannot read:d:v作业实训 vjetbrain_路径编码的字符陷阱这个错误出现在Windows平台根源是JVM读取jvm.options文件时os::native_path()函数对中文路径的处理缺陷。os::native_path()会调用WideCharToMultiByte(CP_ACP, ...)把UTF-16路径转成系统ANSI编码如GBK。当路径含“实训”二字Unicode U5B9E U8BADGBK编码为0xC9 D1 C9 F5但jvm.options文件本身是UTF-8编码0xC9 D1 C9 F5被当作UTF-8字节序列解析必然失败。解决方案不是改文件编码而是绕过jvm.options读取用-XX:Flagspath/to/jvm.flags指定一个纯ASCII路径的配置文件或者直接把参数写死在java命令里java -Xmx4g -XX:UseG1GC MyApp。jvm.options只是便利机制非必需。5.3jvm reference can not find the corresponding jvm serviceJNI全局引用泄漏的雪崩效应这个错误通常发生在长期运行的JNI程序中。jvm reference指的是NewGlobalRef()创建的全局引用它阻止JVM回收对应Java对象。HotSpot用JNIHandleBlock链表管理这些引用每个JNIHandleBlock最多存128个引用。当JNIHandleBlock链表长度超过MaxJNIGlobalReferences默认18000JVM会拒绝新的NewGlobalRef()并抛出此错误。但问题在于JNIHandleBlock本身也是Java对象它的内存由JNIHandles模块管理。如果C代码忘了调用DeleteGlobalRef()JNIHandleBlock链表会无限增长最终耗尽JNIHandles的元空间Metaspace触发OutOfMemoryError: Metaspace进而导致jvm reference查找失败。排查方法用jhsdb jstack --pid pid查看JNI global references计数或用jstat -gc pid观察MMetaspace使用率。如果M持续上涨且jstack输出里JNI global references数接近18000基本确定是JNI引用泄漏。经验技巧在C JNI代码里用RAII模式封装GlobalRefclass GlobalRefGuard { jobject _ref; public: GlobalRefGuard(JNIEnv* env, jobject obj) : _ref(env-NewGlobalRef(obj)) {} ~GlobalRefGuard() { if (_ref) env-DeleteGlobalRef(_ref); } operator jobject() { return _ref; } }; // 使用GlobalRefGuard guard(env, obj); // 析构时自动delete6. 从源码到生产给OpenJDK打一个可验证的补丁学源码的终极检验不是看懂是改懂。本章带你给OpenJDK 17的G1 GC打一个真实补丁让G1在每次Young GC后自动打印当前Eden区的Region数量和平均存活率。这个功能虽小但涉及src/hotspot/share/gc/g1/g1CollectedHeap.cpp、src/hotspot/share/gc/shared/genCollectedHeap.cpp、src/hotspot/share/runtime/globals.hpp三个核心文件完整覆盖了HotSpot的参数注册、GC钩子、日志输出全流程。6.1 补丁设计为什么选G1YoungGCEndHook作为注入点G1的GC周期有明确的钩子函数定义在src/hotspot/share/gc/g1/g1CollectedHeap.hppclass G1CollectedHeap : public CollectedHeap { public: // ... void post_young_gc_end_hook(); // 就是G1YoungGCEndHook };这个函数在G1CollectedHeap::do_collection_pause_at_safepoint()末尾被调用此时GC已完成_eden_regions链表已更新数据最准确。相比pre_young_gc_begin_hookGC前这里能拿到真实的存活对象数。6.2 修改步骤三步走通HotSpot补丁链第一步添加JVM参数编辑src/hotspot/share/runtime/globals.hpp在product(bool, PrintG1EdenStats, false, ...)下方添加product(bool, PrintG1EdenStats, false, Print Eden region count and survival rate after Young GC)这行代码注册了一个布尔型JVM参数-XX:PrintG1EdenStatsproduct宏表示它在产品版JVM中可用区别于develop或diagnostic参数。第二步实现日志逻辑编辑src/hotspot/share/gc/g1/g1CollectedHeap.cpp在post_young_gc_end_hook()函数末尾添加void G1CollectedHeap::post_young_gc_end_hook() { if (PrintG1EdenStats) { size_t eden_count _eden_regions.length(); size_t total_bytes 0; size_t survived_bytes 0; for (int i 0; i _eden_regions.length(); i) { HeapRegion* r _eden_regions.at(i); total_bytes r-used(); // G1SurvRateGroup::add_survivor_region()已统计存活率 survived_bytes r-get_surv_rate_group()-survived_bytes(); } log_info(gc)(G1YoungGC: Eden regions%zu, avg_survival_rate%.1f%%, eden_count, (double)survived_bytes / total_bytes * 100.0); } }这里调用了log_info(gc)宏它会输出到-Xlog:gc日志流格式与标准GC日志一致。第三步编译并验证# 1. 清理旧构建 make clean # 2. 重新configure确保启用新参数 bash configure --with-boot-jdk/path/to/jdk11 --enable-option-checkingfatal # 3. 编译 make images # 4. 运行测试 build/linux-x86_64-server-release/jdk/bin/java \ -XX:UseG1GC -XX:PrintG1EdenStats \ -Xlog:gcdebug -Xmx1g MyApp你会在日志里看到[0.123s][info][gc] G1YoungGC: Eden regions12, avg_survival_rate15.3%这个补丁的价值不在于功能本身而在于它强制你理解了HotSpot的参数注册机制globals.hpp、GC钩子生命周期post_young_gc_end_hook、日志系统集成log_info宏、构建系统依赖make images如何把修改编译进libjvm.so。当你亲手完成这三步OpenJDK对你而言就不再是文档里的名词而是你键盘下可塑的实体。7. 最后一句大实话源码不是终点是调试器里的每一次step into写完这篇专栏大纲我删掉了初稿里所有“掌握”“精通”“深入理解”这类词。因为OpenJDK源码的真相很朴素它不是一本等着你读完的书而是一张永远在更新的施工图纸你永远无法“学完”它但可以确保每一次gdb调试、每一次git blame溯源、每一次make编译失败都比上一次多懂一行代码的意图。我见过最震撼的案例是一个运维工程师他不懂C但为了排查expiring daemon问题硬是学会了用jhsdb的clhsdb命令在VMThread::loop()里设断点观察_timeout变量的值如何被-XX:VMThreadTimeout参数影响。三个月后他提交了第一个OpenJDK补丁修复了os::sleep()在特定内核版本下的精度漂移问题。他的GitHub bio写着“JVM源码是我读过最硬核的API文档。”所以别再问“Java面试会考什么”去问“我的java -version命令背后调用了多少个系统调用”别再背“JVM内存模型”去用pstack看java进程的线程栈数一数VMThread、GCTaskThread、CompilerThread各占几行别再搜“openjdk下载”去hg.openjdk.java.net上fork一个仓库给src/hotspot/share/runtime/thread.cpp加一行log_info然后make images——那一刻你才真正站在了JVM的门口。门后是什么不是标准答案是你自己调试器里光标停在Threads::create_vm()第42行时屏幕上跳动的那个0x00007f8b4c00a800地址。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门