一、JVM 宏观世界与发展史
1.1 什么是 JVM?
JVM(Java Virtual Machine)即 Java 虚拟机。它通过软件模拟出一套具有完整硬件功能、运行在隔离环境中的计算机系统。
JVM 与通用虚拟机的区别:
- 硬件模拟度:VMware 和 VirtualBox 模拟物理 CPU 指令集,拥有复杂的硬件寄存器。
- 定制化设计:JVM 模拟的是 Java 字节码指令集。为了极致的效率,JVM 裁剪了大部分硬件寄存器,仅保留了核心的 PC 寄存器。
- 跨平台灵魂:JVM 是一台'被定制'的虚拟计算机,它使得字节码能运行在任何安装了相应 JVM 的操作系统上。
1.2 JVM 史诗级发展史:群雄割据到 HotSpot 霸榜
了解发展史能让你明白技术演进的逻辑:
- Sun Classic VM (1996):世界上首款商业 JVM。它最大的缺陷是解释器与编译器无法协同。一旦外挂 JIT 编译器,解释器就罢工。JDK 1.4 时被彻底淘汰。
- Exact VM (JDK 1.2):现代虚拟机的雏形。它支持了热点探测和编译器与解析器的混合工作模式,但仅在 Solaris 平台短暂发光。
- HotSpot VM (武林霸主):最初由 Longview Technologies 设计,后经 Sun 和 Oracle 之手。其核心亮点是热点代码探测技术,能通过计数器找到最有价值的代码进行 JIT(即时编译),在响应速度与执行性能间达到完美平衡。
- JRockit (极速之王):由 BEA 开发,专注于服务器端。它不含解释器,全部代码直接编译,号称世界上最快的 JVM。后来与 HotSpot 合并。
- J9 JVM (IBM 悍将):IBM 内部代号 J9,在 IBM 自家硬件上性能极其恐怖。现已开源为 OpenJ9。
- Taobao JVM (国产之光):基于 OpenJDK 深度定制。其 GCIH (GC Invisible Heap) 技术实现了离堆内存,极大降低了 GC 频率。目前已支撑了天猫、淘宝的亿级流量。
二、JVM 运行全流程详解
2.1 字节码到机器码的旅程
JVM 的运行可以概括为以下步骤:
- 编译:
.java 源码被编译为 .class 字节码。
- 加载:类加载器 (ClassLoader) 将字节码加载到内存。
- 存储:将数据存入运行时数据区 (Runtime Data Area)。
- 执行:执行引擎 (Execution Engine) 将字节码指令翻译成系统指令,或通过 JIT 编译。
- 交互:通过本地库接口 (Native Interface) 调用 C/C++ 等本地代码。

三、内存布局与运行时数据区 (Runtime Data Area)
这是 JVM 最核心的知识点,理解了这里,你才能读懂 Dump 文件。

3.1 堆内存 (Heap) —— 线程共享
作用:存放程序中创建的所有对象实例。
参数控制:-Xms (初始堆)、-Xmx (最大堆)。
结构划分:
- 新生代 (Young Gen):Eden 区、Survivor 0 (S0)、Survivor 1 (S1)。默认比例为 8:1:1。
- 老年代 (Old Gen):存放长期存活的对象。
- 回收流程:Eden 区满触发 Minor GC,活对象进 S0/S1;交换 15 次后进老年代。

3.2 虚拟机栈 (JVM Stack) —— 线程私有

- 本质:描述方法执行的内存模型。每个方法对应一个栈帧 (Stack Frame)。
- 栈帧内部结构:
- 局部变量表:存基本类型和引用。内存空间在编译期确定。
- 操作数栈:计算时的临时中转站。
- 动态链接:指向运行时常量池的方法引用。
- 方法出口:存储 PC 寄存器的地址。
3.3 程序计数器 (PC Register) —— 线程私有
- 作用:记录当前线程执行的行号。
- 唯一性:这是 JVM 规范中唯一没有规定 OOM 情况的区域。
3.4 方法区与元空间 (Metaspace) —— 线程共享
- 演进:JDK 7 叫永久代(受 JVM 限制);JDK 8 叫元空间(使用本地内存,不受 JVM 限制)。
- 存储内容:类信息、常量、静态变量、运行时常量池。

3.5 异常案例:内存溢出分析
- Java 堆溢出:不断创建无法回收的对象,提示
java.lang.OutOfMemoryError: Java heap space。需检查内存泄漏或调大 -Xmx。
- 栈溢出 (StackOverflow):通常由无限递归引起,抛出
StackOverflowError。
四、类加载机制全方位深度剖析
4.1 类加载的生命周期

类加载包含以下 5 个关键步骤:
- 加载 (Loading):通过全限定名获取二进制流,生成
java.lang.Class 对象。
- 验证 (Verification):确保 Class 文件符合规范,不危害虚拟机。
- 准备 (Preparation):为 static 变量分配内存并赋初始值(如
int 为 0)。
- 解析 (Resolution):符号引用替换为直接引用。
- 初始化 (Initialization):真正开始执行 Java 代码逻辑(
<clinit> 方法)。
4.2 双亲委派模型 (Parents Delegation Model)
- 工作原理:收到加载请求,先委派给父类。父类不能加,子类才动手。
- 三大级别:启动类加载器 (Bootstrap)、扩展类加载器 (Extension)、应用程序类加载器 (Application)。
- 核心价值:防止核心 API 被篡改,避免重复加载。
双亲委派模型的优点
- 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。
- 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了。
4.3 经典案例:破坏双亲委派的 JDBC
在 JDBC 中,接口 Driver 在 rt.jar(Bootstrap 加载),但实现类在各个厂商的 Jar 包中(如 MySQL,需 App 加载)。此时 Bootstrap 加载器无法'下探'去加载 App 层级的类。
我们先来看下 JDBC 的核心使用代码:
public class JdbcTest {
public static void main(String[] args) {
Connection connection = null;
try {
connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306");
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println(connection.getClass().getClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());
System.out.println(Connection.class.getClassLoader());
}
}
解决方案:引入 Thread Context ClassLoader (线程上下文加载器),在 DriverManager 中实现逆向调用,从而打破双亲委派。
我们进入 DriverManager 的源码类就会发现它是存在系统的 rt.jar 中的,如下图所示:

由双亲委派模型的加载流程可知 rt.jar 是有顶级父类 BootstrapClassLoader 加载的,如下图所示:

而当我们进入它的 getConnection 源码是却发现,它在调用具体的类实现时,使用的是子类加载器(线程上下文加载器 Thread.currentThread().getContextClassLoader)来加载具体的数据库包(如 mysql 的 jar 包),源码如下:
@CallerSensitive
public static Connection getConnection(String url, java.util.Properties info) throws SQLException {
return (getConnection(url, info, Reflection.getCallerClass()));
}
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized (DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if (url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
SQLException reason = null;
for (DriverInfo aDriver : registeredDrivers) {
if (isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
(con != ) {
println( + con);
(con);
}
} (SQLException ex) {
(reason == ) {
reason = ex;
}
}
} {
println( + aDriver.getClass().getName());
}
}
(reason != ) {
println( + reason);
reason;
}
println( + url);
( + url, );
}
这样一来就破坏了双亲委派模型,因为 DriverManager 位于 rt.jar 包,由 BootStrap 类加载器加载,而其 Driver 接口的实现类是位于服务商提供的 Jar 包中,是由子类加载器(线程上下文加载器 Thread.currentThread().getContextClassLoader)来加载的,这样就破坏了双亲委派模型了(双亲委派模型讲的是所有类都应该交给父类来加载,但 JDBC 显然并不能这样实现)。它的交互流程图如下所示:

五、垃圾回收 (GC) 理论与实践
5.1 判定对象'死亡'的两种方案
- 引用计数法:简单高效,但无法解决循环引用(A 调 B,B 调 A)。
引⽤计数描述的算法为:
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就 +1;当引用失效时,计数器就 -1;
任何时刻计数器为 0 的对象就是不能再被使用的,即对象已"死"。
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如 Python 语言就
采用引用计数法进行内存管理。
但是,在主流的 JVM 中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题
范例:观察循环引用问题
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
[GC (System.gc())6092K->856K(125952K),0.0007504 secs]
从结果可以看出,GC 日志包含"6092K->856K(125952K)",意味着虚拟机并没有因为这两个对象互相引用就不回收他们。即 JVM 并不使用引用计数法来判断对象是否存活。
- 可达性分析算法:从 GC Roots 出发搜索,搜不到即判定为垃圾。
在上面我们讲了,Java 并不采用引用计数法来判断对象是否已"死",而采用"可达性分析"来判断对象是否存活 (同样采用此法的还有 C#、Lisp-最早的一门采用动态内存分配的语⾔)。
此算法的核心思想为:通过一系列称为"GCRoots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到 GCRoots 没有任何的引用链相连时 (从 GCRoots 到这个对象不可达) 时,证明此对象是不可用的。以下图为例:

对象 Object5-Object7 之间虽然彼此还有关联,但是它们到 GC Roots 是不可达的,因此他们会被判定为可回收对象。
在 Java 语言中,可作为 GC Roots 的对象包含下面几种:
- 虚拟机栈 (栈帧中的本地变量表) 中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(Native 方法) 引用的对象。
从上面我们可以看出'引用'的功能,除了最早我们使用它(引用)来查找对象,现在我们还可以使用'引用'来判断死亡对象了。所以在 JDK1.2 时,Java 对引用的概念做了扩充,将引用分为强引用 (Strong Reference)、软引用 (Soft Reference)、弱引用 (Weak Reference) 和虚引用 (Phantom Reference) 四种,这四种引用的强度依次递减。
- 强引用: 强引用指的是在程序代码之中普遍存在的,类似于
"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收被引用的对象实例。
- 软引用: 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在 JDK1.2 之后,提供了 SoftReference 类来实现软引用。
- 弱引用: 弱引用也是用来描述非必需对象。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后提供了 WeakReference 类来实现弱引用。
- 虚引用: 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在 JDK1.2 之后,提供了 PhantomReference 类来实现虚引用。
5.2 垃圾回收算法全解析
- 标记 - 清除 (Mark-Sweep):基础算法,但会产生内存碎片。
- 复制算法 (Copying):新生代主力。将内存分为 Eden 和 Survivor,提高空间利用率(8:1:1)。
- 标记 - 整理 (Mark-Compact):老年代主力。将存活对象移向一端,彻底消除碎片。
- 分代收集 (Generational):核心思想——新生代用复制,老年代用标记 - 清除/整理。
5.3 现代垃圾收集器大比拼
- Serial/Serial Old:单线程,STW 停顿久,适用于 Client 模式。
- Parallel Scavenge:吞吐量优先,适合后台运算。
- CMS:低停顿优先,首个并发收集器。
- G1 (Garbage First):JDK 9 默认。将堆分为 Region,可预测停顿时间。
六、JMM 内存模型与并发安全性
6.1 JMM 核心机制
JMM(Java Memory Model)规范了主内存与线程工作内存的交互。它主要解决三大问题:可见性、原子性、有序性。
6.2 单例模式中的 DCL 与 Volatile
源码复现:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
深度揭秘:new Singleton() 并非原子操作。如果不加 volatile,由于指令重排序,线程可能会获取到一个未完成构造的对象。volatile 的加入能保证可见性并禁止重排序。