
摘要本文系统讲解 Java 虚拟机JVM的核心知识体系覆盖 JVM 整体架构、类加载子系统、运行时数据区、对象的创建与内存布局、垃圾回收算法与主流收集器、常用调优参数与诊断工具、JDK 版本演进带来的关键变化以及高频面试题解析并配合大量可运行代码示例、对比表格和 13 张 Mermaid 架构图帮助读者从原理到实战建立完整的 JVM 知识框架从容应对线上故障排查、性能调优与面试考察。一、JVM 是什么为什么 Java 能「一次编写、到处运行」JVM 的全称是 Java Virtual Machine即 Java 虚拟机。它是运行 Java 字节码的抽象计算机也是 Java 平台最核心的组成部分。Java 之所以敢宣称「Write Once, Run Anywhere」靠的正是 JVM同一个.class字节码文件放到 Windows、Linux、macOS 等不同操作系统上只要安装了对应该平台的 JVM就能以几乎相同的方式运行。从运行过程看一个 Java 程序的执行链路大致是这样的源代码程序员编写.java文件这是人类可读的高级语言代码。编译通过javac编译器把.java编译为.class字节码文件。加载类加载器把.class文件的字节码读入 JVM 内存。执行执行引擎解释执行字节码必要时通过 JIT 即时编译器把热点代码编译成本地机器码。理解 JVM 的价值可以从三个方面来看排查问题线上 OOM、内存泄漏、频繁 Full GC、CPU 飙高等问题几乎都离不开对 JVM 内存模型和 GC 机制的理解。性能调优合理设置堆大小、选择合适的垃圾回收器、优化代码减少对象创建都能显著提升吞吐量和降低延迟。深入语言理解 JVM 有助于看清 Java 的类加载、反射、泛型擦除、自动装箱、字符串常量池等语言特性背后的实现原理。需要注意的是JVM 并不等同于 Java。JVM 只负责执行符合规范的字节码而字节码可以由多种语言编译而来例如 Kotlin、Scala、Groovy、Clojure、JRuby 等这些语言都可以运行在 JVM 上。这是 JVM 作为「多语言平台」的重要价值。二、JVM 整体架构总览JVM 的内部结构可以粗略划分为三大部分类加载子系统、运行时数据区和执行引擎。先把整体框架建立起来后续章节再逐一深入。以下是各个模块的职责概览模块组成职责类加载子系统加载、链接、初始化把字节码加载到内存生成对应的 Class 对象并执行静态初始化。运行时数据区PC、栈、堆、方法区等存放程序运行过程中需要的各种数据是内存管理的核心区域。执行引擎解释器、JIT 编译器、GC负责执行字节码指令、把热点代码编译为机器码、回收不再使用的对象。本地方法接口JNI为 Java 调用本地 C/C 代码提供桥梁。其中运行时数据区又分为线程私有和线程共享两类线程私有程序计数器、虚拟机栈、本地方法栈。每个线程单独一份随线程创建而创建随线程结束而销毁。线程共享堆、方法区元空间、运行时常量池。它们是所有线程共享的数据区域也是垃圾回收的主要战场。下面这张图展示了线程私有区域与线程共享区域的划分理解这个边界非常重要堆和方法区是所有线程共用的因此对它们的访问需要考虑线程安全问题而栈和程序计数器天然就是线程隔离的不存在跨线程共享的竞争问题。三、类加载子系统字节码如何变成类JVM 并不是一启动就把所有类都加载进来而是按需加载。类从被加载到虚拟机内存到被卸载出内存完整生命周期包括加载、验证、准备、解析、初始化、使用、卸载七个阶段其中验证、准备、解析三个阶段统称为「链接」。3.1 加载 Loading加载阶段主要完成三件事通过类的全限定名获取定义此类的二进制字节流。字节流来源非常灵活可以来自本地.class文件、网络、ZIP 包、数据库甚至运行时动态生成。把字节流所代表的静态存储结构转换为方法区的运行时数据结构。在内存中生成一个代表该类的java.lang.Class对象作为方法区中该类各种数据的访问入口。3.2 链接 Linking链接阶段又细分为三个步骤验证 Verify确保被加载类符合 JVM 规范不会危害虚拟机自身安全。包括文件格式验证、元数据验证、字节码验证和符号引用验证。准备 Prepare为类变量static变量分配内存并设置默认初始值。注意这里分配的是类变量而不是实例变量int默认是 0boolean默认是 false引用类型默认是 null。static final常量在编译期就确定了字面量准备阶段会直接赋值。解析 Resolve把常量池内的符号引用转换为直接引用。所谓符号引用就是用一组符号描述所引用的目标而直接引用就是指向目标的指针、相对偏移量或句柄。3.3 初始化 Initialization初始化是类加载的最后一步也是真正执行类中定义的 Java 程序代码字节码的阶段。此阶段执行clinit()方法即类构造器用于给静态变量赋显式值并执行静态代码块。以下代码可以验证初始化时机javapublic class ClassInitDemo { static int a 10; static final int B 20; static int c; static { c 30; System.out.println(静态代码块执行); } public static void main(String[] args) { System.out.println(main 方法执行); } }运行结果是先输出「静态代码块执行」再输出「main 方法执行」这说明静态变量赋值和静态代码块都在类初始化阶段按顺序执行而static final常量 B 在准备阶段就已经被赋值。3.4 类加载器与双亲委派模型JVM 提供了以下几种内建的类加载器类加载器层级负责加载的内容Bootstrap ClassLoader 引导类加载器最顶层加载 JDK 的核心类库如java.*等由 C 实现Java 中获取为 null。Extension ClassLoader 扩展类加载器第二层加载 JDK 扩展目录下的类库。Application ClassLoader 应用类加载器第三层加载应用程序classpath下的类是用户代码的默认加载器。自定义类加载器用户定义通过继承ClassLoader实现用于加载网络、加密等特殊来源的字节码。双亲委派模型的核心规则是一个类加载器在接到加载请求时先不自己加载而是把请求委托给父类加载器只有当父类加载器无法完成加载时子类加载器才尝试自己加载。这样可以避免核心类库被恶意篡改同时保证同一个类只被加载一次。javapublic class ClassLoaderDemo { public static void main(String[] args) { ClassLoader loader ClassLoaderDemo.class.getClassLoader(); System.out.println(loader); // 通常是 AppClassLoader System.out.println(loader.getParent()); // ExtClassLoader System.out.println(loader.getParent().getParent()); // null即 Bootstrap } }下面用流程图画一下双亲委派的过程为什么要打破双亲委派双亲委派并非完美历史上有些场景需要破坏它例如 JDBC 的驱动加载DriverManager由引导类加载器加载但它需要调用应用类路径下的驱动实现类又比如 Tomcat 为了实现多个 Web 应用之间类隔离让每个应用使用各自的 WebappClassLoader。理解机制之后再理解这些打破方式就容易多了。四、运行时数据区内存都花在哪里运行时数据区是 JVM 内存管理的核心。只有把每一块区域的职责、生命周期和可能抛出的异常搞清楚才能在遇到StackOverflowError、OutOfMemoryError时快速定位。4.1 程序计数器 PC Register程序计数器是一块很小的内存空间用来记录当前线程正在执行的字节码指令地址。它是唯一一个在 JVM 规范中没有规定任何OutOfMemoryError情况的区域。在 JVM 的多线程模型中线程切换后需要能恢复到正确的执行位置靠的就是程序计数器。如果线程正在执行的是本地方法程序计数器的值为 undefined。4.2 Java 虚拟机栈 JVM Stack虚拟机栈是线程私有的每个方法执行时都会创建一个栈帧Stack Frame压入栈中。栈帧中保存局部变量表、操作数栈、动态链接、方法返回地址等信息。方法调用就是栈帧入栈方法返回就是栈帧出栈。如果栈深度超过 JVM 允许的深度会抛出StackOverflowError如果栈无法申请到足够内存会抛出OutOfMemoryError。无限递归可以复现栈溢出javapublic class StackOverflowDemo { private static int depth 0; public static void recurse() { depth; recurse(); } public static void main(String[] args) { try { recurse(); } catch (StackOverflowError e) { System.out.println(栈溢出递归深度 depth); } } }局部变量表存放方法参数和方法内部定义的局部变量其大小在编译期就可以基本确定。操作数栈则用于进行算术运算、方法调用时的参数传递等。槽位Slot是局部变量表的最小存储单元boolean、byte、char、int、short、float、reference占一个 Slotlong和double占两个 Slot。4.3 本地方法栈 Native Method Stack本地方法栈和虚拟机栈作用类似区别在于虚拟机栈执行 Java 方法而本地方法栈执行本地方法Native Method。在 HotSpot 虚拟机中本地方法栈和虚拟机栈是合二为一的不再单独区分。4.4 堆 Heap堆是 JVM 内存中最大的一块所有线程共享在虚拟机启动时创建。堆的唯一目的就是存放对象实例几乎所有的对象都在这里分配内存。堆也是垃圾回收器管理的主要区域因此也被称为「GC 堆」。从垃圾回收的角度堆通常可以划分为新生代和老年代新生代又细分为 Eden 区和两块 Survivor 区From/To。也可以从内存分配的角度把堆划分为多个线程私有的分配缓冲区 TLABThread Local Allocation Buffer以减少多线程并发分配对象时的竞争。堆的内存逻辑上是连续的但物理上可以不连续。当堆中没有内存可分配给新对象且堆无法继续扩展时会抛出OutOfMemoryError: Java heap space。4.5 方法区与元空间 Method Area / Metaspace方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。在 JDK 8 之前HotSpot 使用永久代Permanent Generation实现方法区从 JDK 8 开始永久代被移除改用本地内存中的元空间Metaspace。这一变化带来了明显的好处不再受堆大小限制元空间使用本地内存仅受物理内存和系统参数约束不再占用堆空间。动态扩展默认情况下元空间可以动态增长只有在类元数据激增时才可能触发垃圾回收或抛出OutOfMemoryError: Metaspace。更易调优通过-XX:MetaspaceSize设置初始元空间大小通过-XX:MaxMetaspaceSize设置上限避免元空间无限制膨胀。需要特别注意的是运行时常量池也从方法区中移到了堆中。字符串常量池位于堆内这也是为什么字符串对象可以被垃圾回收器正常回收。五、对象的创建与内存布局对象是 Java 程序运行的基本单位。理解对象从创建到消亡的完整过程是理解内存分配、逃逸分析、垃圾回收的基础。5.1 对象的创建过程当 JVM 遇到一条new指令时会经历以下步骤类加载检查检查该指令的参数能否在常量池中定位到一个类的符号引用并判断该类是否已经加载、解析、初始化如果没有则先执行类加载。分配内存为新生对象分配内存。分配方式有「指针碰撞」和「空闲列表」两种具体取决于堆是否规整。初始化零值将分配到的内存空间初始化为零值保证对象实例字段即使不赋初值也能直接使用。设置对象头存储对象是哪个类的实例、哈希码、GC 分代年龄等信息。执行init()方法执行构造方法按程序员意愿初始化对象。对象创建流程可以总结为下图5.2 对象的内存布局HotSpot 虚拟机中对象在堆内存中的布局可以划分为三部分对象头、实例数据和对齐填充。区域组成说明对象头Mark Word、类型指针存储哈希码、GC 分代年龄、锁状态标志、指向类元数据的指针等。实例数据对象字段存储对象真正有效的信息包括从父类继承下来的字段。对齐填充占位字节不是必然存在仅用于保证对象大小为 8 字节的整数倍。5.3 对象访问定位Java 程序通过栈上的 reference 数据操作堆上的具体对象。访问定位主要有两种方式句柄访问和直接指针访问。句柄访问reference 指向句柄池中的句柄句柄再指向对象实例数据和类型数据好处是对象被移动时只需修改句柄reference 本身不用改。直接指针访问reference 直接指向对象实例数据速度快HotSpot 主要采用这种方式。六、垃圾回收算法与主流收集器垃圾回收Garbage CollectionGC是 JVM 自动内存管理的核心机制目标是回收不再使用的对象释放堆内存。6.1 如何判断对象已死判断对象是否可回收主要有两种方式引用计数法给对象添加一个引用计数器被引用时加一引用失效时减一计数为零说明可回收。缺点是难以解决循环引用问题Java 没有采用。可达性分析从一组 GC Roots 出发沿着引用链向下搜索无法到达的对象即为可回收对象。Java 采用这种方式。可作为 GC Roots 的对象包括虚拟机栈中引用的对象、方法区中静态变量引用的对象、常量引用的对象、本地方法栈 JNI 引用的对象等。6.2 垃圾回收算法算法核心思想优点缺点标记-清除标记所有存活对象统一清除未标记对象。实现简单。产生内存碎片效率不稳定。标记-复制把存活对象复制到另一块区域原区域整体清理。无碎片分配效率高。浪费一半内存对象存活率高时复制成本高。标记-整理标记后让存活对象向一端移动再清理边界外内存。无碎片。移动对象需要更新引用停顿时间较长。分代收集按对象生命周期划分区域新生代用复制老年代用整理或清除。兼顾效率与空间。需要维护跨代引用等复杂机制。6.3 主流垃圾收集器HotSpot 提供多种收集器适应不同场景收集器适用区域特点Serial / Serial Old新生代 / 老年代单线程暂停所有用户线程适合客户端小内存场景。Parallel Scavenge / Parallel Old新生代 / 老年代多线程并行追求吞吐量是 JDK 8 默认组合。ParNew新生代Serial 的多线程版本可与 CMS 配合。CMS老年代以最短停顿时间为目标标记-清除实现可能产生碎片。G1整堆面向大内存把堆划分为多个 Region可控停顿JDK 9 起默认。ZGC整堆低延迟目标停顿不超过 10ms支持超大堆JDK 21 起分代。从收集器演进看目标始终是在吞吐量和停顿时间之间取得平衡。大内存、低延迟场景优先考虑 G1、ZGC、Shenandoah 等现代收集器。七、JVM 常用调优参数与诊断工具7.1 常用内存参数参数作用-Xms设置堆初始大小如-Xms512m。-Xmx设置堆最大大小生产环境通常与-Xms保持一致以避免动态扩容。-Xmn设置新生代大小。-Xss设置每个线程栈大小默认约 1MB。-XX:MetaspaceSize设置元空间初始大小。-XX:MaxMetaspaceSize设置元空间最大大小。7.2 常用 GC 参数与日志bash-XX:UseG1GC -XX:MaxGCPauseMillis200 -Xlog:gc*:filegc.log:time,level,tags从 JDK 9 开始GC 日志统一使用-Xlog参数不再使用旧的-XX:PrintGCDetails等参数。7.3 常用诊断工具工具用途jps查看 Java 进程 PID。jstat监控堆内存、类加载、GC 统计信息。jmap导出堆转储快照查看对象分布。jstack查看线程快照定位死锁和线程阻塞。jconsole / jvisualvm图形化监控工具适合交互式分析。Arthas阿里开源的在线诊断工具支持命令行排查线上问题。线上排查的典型思路是先看现象再用 jstat 观察 GC 频率和内存变化用 jmap 分析堆对象用 jstack 检查线程状态最后结合日志定位问题。八、JDK 版本演进带来的关键变化JVM 的发展与 JDK 版本演进紧密相关。理解关键版本的变化有助于在升级时规避风险。版本关键变化对 JVM 的影响JDK 8移除永久代引入元空间Lambda 与 Stream默认 Parallel 收集器。方法区实现变更函数式编程带来新的字节码指令。JDK 9模块化系统 JPMSG1 成为默认收集器String 底层改为 byte 数组。启动性能和内存占用优化GC 默认策略变化。JDK 11引入 ZGC 实验特性新增 HTTP Client统一 GC 日志。低延迟 GC 能力增强日志体系更规范。JDK 17 LTSZGC、Shenandoah 转正密封类等语言特性。生产级低延迟 GC 更加成熟稳定。JDK 21 LTSZGC 支持分代虚拟线程转正。并发能力大幅提升GC 吞吐进一步提升。对于多数生产系统JDK 17 是目前较稳妥的 LTS 选择对延迟极为敏感的在线服务可以关注 JDK 21 的虚拟线程和分代 ZGC。九、高频面试题解析这里整理几道常见的 JVM 面试题帮读者建立答题框架。9.1 JVM 内存模型有哪些区域答线程私有的程序计数器、虚拟机栈、本地方法栈以及线程共享的堆、方法区元空间、运行时常量池。堆与方法区是 GC 的主要区域栈和程序计数器天然线程隔离。9.2 什么情况下会发生 StackOverflowError 和 OutOfMemoryError答栈深度超过虚拟机允许范围时抛StackOverflowError如无限递归栈无法申请到足够内存、堆无法扩展、元空间不足时抛OutOfMemoryError常见提示包括Java heap space、Metaspace、unable to create new native thread等。9.3 什么是双亲委派模型为什么要打破它答类加载请求先委托给父类加载器父类无法完成时子类才自己加载目的是避免核心类库被篡改并保证类唯一性。JDBC、Tomcat 等场景因需要反向调用应用类库或实现类隔离而打破该模型。9.4 Minor GC、Major GC 和 Full GC 有什么区别答Minor GC 发生在新生代由 Eden 区满触发频率高、速度快Major GC 通常指老年代 GC常与 Full GC 混用Full GC 覆盖整个堆和方法区停顿最长应尽量避免频繁发生。9.5 如何排查线上频繁 Full GC答先用 jstat 观察 GC 频率和堆变化再用 jmap 导出堆快照分析大对象和内存泄漏结合对象创建路径、缓存策略和堆参数定位根因最后通过调整堆大小、优化代码或更换收集器解决。十、总结JVM 的知识体系可以概括为「一个执行环境、两个核心机制、三类调优手段」一个执行环境JVM 通过类加载子系统把字节码加载进内存由运行时数据区存放数据再由执行引擎完成解释与编译执行从而实现「一次编写、到处运行」。两个核心机制类加载机制决定了类如何被加载、链接、初始化垃圾回收机制决定了内存如何被自动管理。三类调优手段通过内存参数控制堆、栈和元空间的大小通过选择合适的垃圾收集器在吞吐量与延迟之间取舍通过 jstat、jmap、jstack、Arthas 等工具定位线上问题。从 JDK 8 到 JDK 21JVM 在内存布局、垃圾收集器和并发能力上持续演进。掌握这些关键变化不仅能帮助你升级时规避风险也能让你在技术选型时做出更合理的判断。