ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JVM源码目录结构全解析:从顶层到HotSpot核心模块

JVM源码目录结构全解析:从顶层到HotSpot核心模块

1. 从一份“找不到北”的源码压缩包说起

很多人第一次把JVM源码包解压开,看到满屏的目录和文件,第一反应是“这都什么跟什么”。hotspot、jdk、langtools、nashorn、corba……十几个顶层目录一字排开,每个里面又是层层嵌套的子目录,光看名字根本猜不出哪个才是JVM真正的“心脏”。我当初也是这么过来的,下载完源码,打开根目录,鼠标滚轮滚了三遍,愣是不知道该点进哪个文件夹。

这篇文章就是来解决这个问题的。它不打算教你写JVM、也不打算逐行分析C++代码,而是聚焦一件更基础、但几乎所有人都会卡住的事:把JVM源码的目录结构彻底理清楚。搞清楚哪个目录对应哪个模块、哪个文件负责哪块功能、它们之间是什么关系,你后面无论是想读GC代码、看JIT编译流程,还是研究类加载机制,都能直接定位到目标文件,而不是在几十个目录里瞎转。

适合谁看?如果你已经会写Java、用过JVM调优参数,但从来没打开过JVM源码,或者打开过但被目录结构劝退,那这篇就是写给你的。我会用“先看地图再进城市”的方式,从顶层目录一路拆到HotSpot内部的核心子目录,把每个目录的职责、关键文件、以及“为什么这样划分”讲清楚。全程不堆术语,遇到复杂概念就用生活化的类比解释,保证你看完能自己画出JVM源码的目录地图。

提示:本文基于OpenJDK的经典目录布局展开,不同版本之间目录名称和层级会有细微差异,但核心模块的划分逻辑是一致的。读的时候重点关注“为什么这样分”,而不是死记某个版本的具体路径。

2. 顶层目录:先分清“JDK全家桶”和“JVM本体”

打开OpenJDK源码根目录,你会看到一堆并列的文件夹。很多人误以为这些全都是JVM的代码,其实不是。顶层目录里只有一部分跟JVM直接相关,剩下的属于JDK的其他组件。先把这层关系理清,后面才不会迷路。

2.1 顶层目录的职责划分

我习惯把顶层目录分成三类:JVM本体类、JDK工具与库类、辅助与构建类。下面这张表是我自己整理的核心目录对照,你可以直接拿来当速查表用。

目录名类别核心职责是否JVM本体
hotspotJVM本体JVM运行时核心,含GC、JIT、类加载、解释器是
jdkJDK工具与库Java类库源码(java.lang、java.util等)否
langtoolsJDK工具与库javac编译器、javadoc等工具源码否
nashornJDK工具与库JavaScript引擎实现否
corbaJDK工具与库CORBA相关支持(新版本已移除)否
make辅助与构建构建脚本和编译规则否
test辅助与构建测试用例集合否

这张表里最关键的一行就是hotspot。JVM源码入门,90%的精力都应该花在这个目录里。其他目录不是不重要,但它们属于JDK层面的东西,跟你理解“JVM怎么跑Java代码”这件事关系没那么直接。

2.2 为什么hotspot是绝对核心

hotspot这个名字来源于它的一个核心技术特征:热点代码探测。JVM在运行时会监控哪些方法被频繁调用,把这些“热点”方法交给JIT编译器编译成本地机器码,从而大幅提升执行效率。这个机制是整个JVM性能优势的根基,所以这个目录就叫hotspot。

你可以把hotspot理解成JVM的“发动机舱”。里面装着:

  • 类加载系统:负责把.class文件读进来、验证、准备、解析、初始化
  • 运行时数据区:堆、栈、方法区、程序计数器等内存结构的实现
  • 执行引擎:解释器、JIT编译器、GC收集器
  • 本地接口:JNI的实现

而jdk目录里的东西,更像是“车身和内饰”——Java类库、工具、API。你写Java代码时调用的String、ArrayList、HashMap,它们的源码在jdk目录里;但这些类被加载、被编译、被回收的过程,全部由hotspot目录里的代码控制。

注意:有些教程一上来就让你读jdk/src/share/classes/java/lang/Object.java,这没错,但那是Java类库层面的事。如果你想理解JVM本身,第一步应该是进hotspot,而不是进jdk。

2.3 版本差异带来的目录变化

不同版本的OpenJDK,顶层目录会有出入。比如早期版本有corba和nashorn,后来被移除了;JDK 9引入模块化之后,jdk目录的内部结构也发生了大调整。但hotspot这个目录名和它的核心地位,从很早一直保持到现在。

我建议你在读源码之前,先确认自己用的是哪个版本,然后对照该版本的官方目录说明看一眼。不用背,知道“我读的是哪个版本、这个版本里哪些目录已经没了”就够了。我自己的习惯是在源码根目录放一个README-版本.txt,记录版本号和目录变动,省得下次打开又忘。

3. 进入hotspot:JVM真正的“五脏六腑”

从根目录进到hotspot,你会看到它下面又分了好几个子目录。这一层是很多人第二次迷路的地方——src、make、test、agent……看起来又是一堆并列项。别急,这一层的逻辑比顶层清晰得多,因为hotspot的目录划分基本就是按“功能模块”来的。

3.1hotspot子目录的功能地图

先看整体划分:

子目录职责重要程度
src所有JVM核心源码最高
make编译构建脚本中
testJVM层测试用例中
agentJVMTI代理相关低(入门阶段可跳过)
os操作系统相关代码(部分版本独立)中

入门阶段,你99%的时间都会花在src目录里。其他目录要么是构建用的,要么是测试用的,要么是特定场景才涉及的。所以下面重点拆src。

3.2src内部的“按语言分家”

进到hotspot/src,你会看到类似这样的结构:

src/ share/ os/ os_cpu/ cpu/

这是HotSpot源码组织的一个核心设计:按“平台无关”和“平台相关”分离。

  • share:平台无关的通用代码,占绝大部分
  • os:操作系统相关的代码(如Linux、Windows各自的实现)
  • cpu:CPU架构相关的代码(如x86、ARM各自的实现)
  • os_cpu:操作系统与CPU组合相关的代码

为什么要这样分?因为JVM要跑在各种操作系统和CPU架构上,但大部分逻辑(比如对象分配、GC算法)是通用的。把通用部分放share,把平台差异部分单独抽出来,这样移植到新平台时只需要改os和cpu里的代码,share里的核心逻辑不用动。这是一种典型的跨平台分层设计。

提示:入门阶段直接看share目录就够了。os和cpu里的代码涉及大量汇编和平台细节,等你把通用逻辑搞清楚再回头看,否则容易被劝退。

3.3share下面的“功能分区”

hotspot/src/share/vm是JVM源码最核心的目录,没有之一。它下面又按功能分成了多个子目录,我挑最重要的几个列出来:

子目录功能对应JVM知识点
runtime运行时核心线程管理、内存管理、JNI、反射
memory内存管理堆、栈、方法区的底层实现
gc_implementation垃圾收集器Serial、Parallel、CMS、G1
interpreter解释器字节码解释执行
compilerJIT编译器C1、C2编译器
classfile类文件处理.class文件解析
oops对象模型Java对象在JVM中的表示
prims本地方法JNI实现、Unsafe等

这张表就是你的“JVM源码导航图”。想研究GC,进gc_implementation;想看类加载,进classfile和runtime;想理解JIT,进compiler。每个目录对应一个明确的JVM子系统,划分得非常干净。

3.4 为什么oops和memory要分开

这里有个容易混淆的点:oops和memory都跟内存有关,为什么要分成两个目录?

简单说,oops管的是“对象长什么样”,memory管的是“内存怎么分配和回收”。oops是“Object-Oriented Programming System”的缩写,里面定义的是Java对象在JVM内部的表示方式——对象头、实例数据、对齐填充这些。而memory目录里是堆的分配策略、内存屏障、卡表等更底层的机制。

打个比方:oops是“汽车的设计图纸”,规定了一辆车由哪些部件组成;memory是“停车场的管理规则”,规定车怎么停、怎么挪、什么时候清走。两者配合,但职责不同。

4. 关键目录深挖:从runtime到gc_implementation

上一节给了你一张功能地图,这一节挑几个最核心的目录往下钻一层,让你知道每个目录里具体有什么、入门时该看哪些文件。

4.1runtime:JVM的“中枢神经”

hotspot/src/share/vm/runtime是我个人认为最值得先读的目录。它里面放的是JVM运行时的基础支撑代码,很多其他模块都依赖它。

几个关键文件:

  • thread.cpp/thread.hpp:Java线程与操作系统线程的映射
  • os.cpp/os.hpp:操作系统抽象层接口
  • javaCalls.cpp:Java方法调用的入口
  • reflection.cpp:反射机制实现
  • synchronizer.cpp:同步与锁的实现
  • globals.cpp:全局变量定义

入门建议从thread.cpp和javaCalls.cpp入手。前者帮你理解“一个Java线程在JVM里到底是什么”,后者帮你理解“调用一个Java方法时JVM做了什么”。这两个问题搞清楚了,后面看解释器和编译器会顺很多。

4.2memory:堆和栈的底层实现

memory目录里最核心的文件是universe.cpp和heap.cpp。universe这个词在JVM里指的是“运行时内存空间的整体视图”,包括堆、方法区、各种元数据空间。heap.cpp则是堆的具体实现。

还有一个文件值得注意:metaspace.cpp。JDK 8之后,永久代被元空间取代,元空间的管理逻辑就在这个文件里。如果你对“为什么永久代会被移除”这个问题感兴趣,读这个文件比看任何博客都直接。

4.3gc_implementation:垃圾收集器的“家族树”

这个目录下面按收集器分成了多个子目录:

gc_implementation/ serial/ parallel/ cms/ g1/ shared/

shared里放的是各收集器共用的基类和工具,其他目录各自实现具体的收集算法。入门建议先看serial,因为它最简单、代码量最少,把Serial收集器的完整流程读一遍,再去看Parallel和G1会轻松很多。

注意:不同版本中GC目录的组织方式有变化,新版本可能把G1相关代码移到别处,或者引入新的收集器目录。读之前先确认版本。

4.4interpreter和compiler:执行引擎的两条路

interpreter目录里是字节码解释器的实现。核心文件是bytecodeInterpreter.cpp和templateTable.cpp。前者是解释执行的主循环,后者定义了每个字节码对应的操作模板。

compiler目录则复杂得多,它下面又分了c1和c2两个子目录,分别对应C1(客户端编译器)和C2(服务端编译器)。C1编译快、优化少;C2编译慢、优化激进。JVM会根据方法的“热度”决定用哪个编译器,这就是“分层编译”的核心思路。

入门阶段不建议直接啃compiler,代码量大、优化理论多。先把interpreter搞清楚,理解“字节码是怎么被一条条执行的”,再去碰JIT会更有底气。

5. 读源码前必须建立的三个认知

目录结构本身不难,难的是很多人带着错误的预期去读,结果读了两天就放弃了。这一节分享三个我在读JVM源码过程中踩过的坑和总结出的认知,帮你少走弯路。

5.1 不要试图“从头读到尾”

JVM源码不是一本小说,不能从第一页翻到最后一页。它是按模块组织的,模块之间有依赖关系,但不存在一条“线性阅读路径”。正确的做法是带着问题读:比如“一个对象创建时发生了什么”,然后沿着interpreter→runtime→memory→oops这条线去追,而不是按目录顺序硬啃。

5.2 先读头文件,再读实现文件

HotSpot的.hpp头文件里通常有详细的注释,解释了类的职责、关键字段的含义、以及设计意图。而.cpp实现文件里是具体的逻辑,代码量大、细节多。先读头文件建立整体印象,再挑关键方法读实现,效率会高很多。

5.3 善用“调用链”而不是“文件树”

目录结构是静态的,但JVM的运行是动态的。真正理解一个功能,靠的是追调用链:从入口函数开始,一层层往下跟,看它调了谁、谁又调了谁。比如追一次new Object()的完整流程,你会自然地从interpreter走到runtime再走到memory,这比死记目录结构有用得多。

提示:追调用链时建议用支持“跳转到定义”的编辑器,配合grep或ripgrep搜索函数名,效率比手动翻目录高一个数量级。

6. 把目录结构变成自己的“源码地图”

最后分享一个我自己的做法:画一张自己的JVM源码地图。不是照抄官方目录树,而是按“我想研究的问题”来组织。比如:

  • 我想研究GC → 地图上标出gc_implementation、memory、oops
  • 我想研究类加载 → 标出classfile、runtime
  • 我想研究JIT → 标出compiler、interpreter

这张地图不用很精确,关键是你自己画的。画的过程就是理解目录关系的过程。我当初画了三版才觉得顺手,第一版太细,第二版太粗,第三版才找到“按问题组织”的节奏。

另外,建议在源码根目录放一个NOTES.md,记录你每次读源码的发现:哪个文件负责什么、哪个函数是关键入口、哪个地方有坑。读JVM源码是个长期工程,笔记比记忆可靠得多。

等你把目录结构理顺了,后面无论是读GC代码还是看JIT优化,都会发现“找文件”这件事不再消耗你的精力,你可以把全部注意力放在代码逻辑本身。这才是目录结构入门真正的价值——它不是知识,而是工具。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进