【JVM 终极通关指南】万字长文从底层到实战全维度深度拆解 Java 虚拟机

我的主页:
寻星探路
个人专栏:
《JAVA(SE)—-如此简单!!! 》
《从青铜到王者,就差这讲数据结构!!!》
《数据库那些事!!!》
《JavaEE 初阶启程记:跟我走不踩坑》
《JavaEE 进阶:从架构到落地实战 》
《测试开发漫谈》
《测开视角・力扣算法通关》
《从 0 到 1 刷力扣:算法 + 代码双提升》
《Python 全栈测试开发之路》
没有人天生就会编程,但我生来倔强!!!
寻星探路的个人简介:
导读:为什么每个大厂程序员都必须精通 JVM?
Java 程序的运行效率、系统的稳定性、甚至高并发下的抗压能力,都与 JVM 密切相关。如果你只会写业务代码而不懂 JVM,就像只会开车而不懂发动机原理的赛车手。当遇到 OutOfMemoryError(内存溢出)或 GC 频繁导致系统卡顿时,你将束手无策。本文将带你从 JVM 的历史起源开始,一步步深入其核心,最终掌握调优真谛。
一、JVM 宏观世界与发展史
1.1 什么是 JVM?
JVM(Java Virtual Machine)即 Java 虚拟机。它通过软件模拟出一套具有完整硬件功能、运行在隔离环境中的计算机系统。
JVM 与通用虚拟机的区别:
硬件模拟度:VMware 和 VirtualBox 模拟物理 CPU 指令集,拥有复杂的硬件寄存器。
定制化设计:JVM 模拟的是 Java 字节码指令集。为了极致的效率,JVM 裁剪了大部分硬件寄存器,仅保留了核心的 PC 寄存器。
跨平台灵魂:JVM 是一台“被定制”的虚拟计算机,它使得字节码能运行在任何安装了相应 JVM 的操作系统上。
1.2 JVM 史诗级发展史:群雄割据到 HotSpot 霸榜
了解发展史能让你明白技术演进的逻辑:
-
1.2.1 Sun Classic VM (1996):世界上首款商业 JVM。它最大的缺陷是解释器与编译器无法协同。一旦外挂 JIT 编译器,解释器就罢工。JDK 1.4 时被彻底淘汰。
-
1.2.2 Exact VM (JDK 1.2):现代虚拟机的雏形。它支持了热点探测和编译器与解析器的混合工作模式,但仅在 Solaris 平台短暂发光。
-
1.2.3 HotSpot VM (武林霸主):最初由 Longview Technologies 设计,后经 Sun 和 Oracle 之手。其核心亮点是热点代码探测技术,能通过计数器找到最有价值的代码进行 JIT(即时编译),在响应速度与执行性能间达到完美平衡。
-
1.2.4 JRockit (极速之王):由 BEA 开发,专注于服务器端。它不含解释器,全部代码直接编译,号称世界上最快的 JVM。后来与 HotSpot 合并。
-
1.2.5 J9 JVM (IBM 悍将):IBM 内部代号 J9,在 IBM 自家硬件上性能极其恐怖。现已开源为 OpenJ9。
-
1.2.6 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:330
} 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 SQLExcept
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
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) {
// isDriverAllowed 对于 mysql 连接 jar 进⾏加载
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName())
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getC
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
这样一来就破坏了双亲委派模型,因为 DriverManager 位于rt.jar 包,由 BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的Jar包中,是由子类加载器(线程上下文加载器Thread.currentThread)·getContextClassLoader)来加载的,这样就破坏了双亲委派模型了(双亲委派模型讲的是所有类都应该交给父类来加载,但JDBC显然并不能这样实现)。它的交互流程图如下所示:

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

对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。
在Java语言中,可作为GC Roots的对象包含下面几种:
从上面我们可以看出“引用”的功能,除了最早我们使用它(引用)来查找对象,现在我们还可以使用“引用”来判断死亡对象了。所以在JDK1.2时,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。
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; // 必须加 volatile
public static Singleton getSingleton() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
深度揭秘:new Singleton() 并非原子操作。如果不加 volatile,由于指令重排序,线程可能会获取到一个未完成构造的对象。volatile 的加入能保证可见性并禁止重排序。
结语:JVM 的调优心态
JVM 并不是一个死板的软件,它是一个高度动态平衡的系统。要掌握 JVM,不能仅靠背诵理论,更要学会在 jvisualvm、MAT 和 GC 日志中寻找蛛丝马迹。希望这篇两万字长文能成为你通向 Java 专家之路的阶梯!
网硕互联帮助中心




评论前必须登录!
注册