Skip to content

Java虚拟机

JVM全称Java Virtual Machine(Java虚拟机),它能够执行Java字节码。JVM是Java平台的一部分,负责将Java字节码转换为机器代码,并在运行时管理内存和资源。

主要功能包括:

  • 加载和执行Java字节码
  • 内存管理和垃圾回收
  • 优化热点代码

JVM架构图

常见JVM实现:

实现名称提供方主要特点
HotSpotOracle/OpenJDK默认JVM,拥有高效的JIT编译器(C1/C2)、成熟的GC策略与调优工具,社区广泛支持
OpenJ9Eclipse基金会启动快、内存占用低、可扩展性好,适合云环境与微服务,重视容器内资源友好性
GraalVMOracle多语言运行时(JavaScript、Python、Ruby等)、Graal JIT与Native Image,便于构建原生镜像
Azul ZingAzul Systems商业低延迟JVM,集成C4垃圾回收,针对延迟敏感的金融级应用优化,提供长期支持
JamVMGNU轻量级JVM,启动快、内存脚印小,适合嵌入式设备或教学场景,支持Class文件动态加载
Avian开源社区极简JVM设计,适用于资源受限的系统与即时编译,便于裁剪为嵌入式或移动平台专用版本
龙井阿里巴巴基于OpenJDK的企业级JVM,增强GC与JIT优化、容器友好调度、提供运行时诊断与可靠性保障,适合云原生与大规模服务场景

类加载子系统

类加载子系统负责从文件系统或网络中加载Java类文件,并将其转化为Java虚拟机可以使用的Java类。类加载主要涉及以下步骤:

加载 Loading

加载阶段, JVM从各种来源中获取类的二进制字节码, 并将其读入内存中, 创建对应的Class对象。

对象创建时机存储位置说明
InstanceKlass加载阶段(解析 class 文件时)方法区/元空间HotSpot 内部类元数据结构,JVM 自己使用,不对外暴露,包含字段、方法、常量池等
Class对象(java.lang.Class)加载阶段(与 InstanceKlass 关联)堆内存类的运行时镜像(mirror),与 InstanceKlass 绑定,用于反射访问

字节码的常见来源包括:

  • 磁盘上的本地JAR、WAR、class等文件
  • 网络资源(通过HTTP、FTP等协议获取)
  • 动态生成的字节码(运行时通过反射、CGLIB等工具动态生成的二进制数据)
  • 其他存储介质(数据库、压缩文件等)

JVM使用 类加载器(ClassLoader) 来完成加载工作,主要有三种内置的类加载器:

加载器实现加载路径父加载器用途
BootstrapC++$JAVA_HOME/lib加载JDK核心类库
ExtensionJava$JAVA_HOME/lib/extBootstrap加载JDK扩展库
ApplicationJavaCLASSPATHExtension加载应用程序和第三方库
Custom用户自定义自定义Application特殊加载需求

双亲委派机制

类加载器采用双亲委派机制(Parent Delegation Model)来加载类,确保核心类库的安全性和稳定性。

双亲委派机制

工作原理:

  1. 自下而上委派:子加载器收到类加载请求时,先委派给父加载器
  2. 自上而下加载:父加载器尝试加载,如果找到则返回;未找到则让子加载器加载
  3. 保证优先级:确保核心库(java.lang等)由启动类加载器加载
  4. 防止重复加载:同一个类只会被加载一次

双亲委派的好处:

  • 避免类的重复加载
  • 保护核心库(防止覆盖)
  • 建立类加载的优先级顺序
  • 增强JVM的安全性

链接 Linking

链接是类加载过程中的第二阶段,包括验证、准备、解析三个步骤。

验证(Verification)

确保类文件符合JVM规范。包括:

  • 文件格式验证:魔数0xCAFEBABE、版本号、常量池
  • 字节码验证:指令合法性、栈深度校验
  • 类型安全验证:方法覆写、访问权限检查

准备(Preparation)

为类的静态变量分配内存并初始化为默认值(int → 0, boolean → false, object → null)。

解析(Resolution)

将常量池中的符号引用转换为直接引用。过程如下:

符号引用 → 验证符号实体存在性 → 查找对应的类/方法/字段 → 直接引用(内存地址)

常见转换类型:

  • 类引用:CONSTANT_Class → instanceKlass指针
  • 方法引用:CONSTANT_Methodref → Method指针
  • 字段引用:CONSTANT_Fieldref → 字段偏移量

解析时机:大多数JVM采用懒惰解析,即首次使用时才解析符号引用

初始化 Initialization

初始化是类加载的最后阶段,执行类构造器方法(<clinit>),初始化静态变量和执行静态代码块。

执行引擎

解释器

解释器是JVM中执行字节码最直接的方式。它逐条读取字节码指令,解析其含义,并调用相应的本地方法来执行指令。

工作原理:

  • 逐行解释和执行字节码
  • 边解释边执行,无需等待编译
  • 优点是启动速度快,但执行速度较慢
  • 适合执行频率较低的代码

缺点:

  • 执行效率低,因为每次都需要解释
  • 对于热点代码(频繁执行的代码)效率很差

即时编译器 JIT

即时编译器(Just-In-Time Compiler)是JVM性能优化的关键。JIT将热点代码动态编译为本地机器码,显著提高执行速度。

工作原理:

  1. JVM持续监控代码执行情况,统计热点代码(频繁执行的代码段)
  2. 当代码执行次数达到阈值时,触发JIT编译
  3. 将字节码编译为本地机器码并缓存
  4. 后续执行直接调用本地机器码,无需重新解释

编译级别对比:

特性C1编译器C2编译器
编译速度快(毫秒级)慢(秒级)
编译代码质量一般优秀
优化程度轻量级深度
适用场景短期热点代码长期热点代码
编译阈值1000-5000次10000-20000次
何时启动第一次达到阈值代码继续热后

主要优化技术:

优化技术说明主要作用
方法内联(Inline)将小方法直接插入调用处,减少方法调用和栈操作的开销减少调用开销,提升执行速度
逃逸分析(Escape Analysis)分析对象是否会逃逸到方法/线程外;若不逃逸可在栈上分配或进行标量替换减少堆分配,降低GC压力
分支预测优化(Branch Prediction)基于静态或运行时信息优化条件分支的生成与布局,减少错误预测带来的成本降低CPU分支错预测和流水线冲刷
循环展开(Loop Unrolling)展开循环体以减少循环判断和分支次数,配合其他优化(向量化、指令重排)提高吞吐量,减少循环开销
死代码消除(Dead Code Elimination)移除在运行时不会被执行或不影响结果的代码片段减少代码体积,提升后续优化效果

垃圾回收器 GC

垃圾回收器(Garbage Collector,简称GC)是JVM中负责自动内存管理的组件。它能够在程序运行过程中,自动检测哪些对象已经不再被引用,并释放这些对象所占用的内存空间,从而避免内存泄漏和手动管理内存的复杂性。

对象引用

对象引用是Java中一个重要概念,它决定了对象在内存中的生命周期和垃圾回收时机。Java提供了四种引用类型,强度从强到弱分别为:强引用、软引用、弱引用、虚引用。

引用类型回收时机GC时回收内存不足时回收典型用途
强引用无引用时正常对象引用
软引用内存不足时缓存
弱引用下次GC时临时关联、WeakHashMap
虚引用对象回收前对象回收通知
强引用

通过new关键字创建的对象引用,是Java中最常见的引用类型。

特点:

  • 只要强引用存在,对象就不会被垃圾回收
  • 即使JVM内存不足而抛出OutOfMemoryError,也不会回收强引用指向的对象
  • 强引用是导致内存泄漏的主要原因

示例:

java
String str = "hello";  // 强引用
Object obj = new Object();  // 强引用
软引用

通过SoftReference类创建,表示对象在内存充足时不会被回收,内存不足时才会被回收。

特点:

  • 适合用于实现缓存(如图片缓存、数据缓存)
  • 内存压力大时会被回收,内存充足时保留
  • 一般不会导致OutOfMemoryError

示例:

java
SoftReference<String> softRef = new SoftReference<>("hello");
String str = softRef.get();  // 获取对象,如果被回收则返回null

应用场景: 缓存中间结果、浏览器缓存、图片加载缓存

弱引用

通过WeakReference类创建,表示对象只能生存到下一次垃圾回收。

特点:

  • 下一次GC执行时,弱引用指向的对象必定被回收(无论内存是否充足)
  • 适合用于临时性的关联关系
  • 常用于实现WeakHashMap等数据结构

示例:

java
WeakReference<String> weakRef = new WeakReference<>("hello");
String str = weakRef.get();  // 可能返回null(如果对象已被回收)

应用场景: 缓存键、对象池管理、事件监听器回调

虚引用

通过PhantomReference类创建,对象回收时会收到一个系统通知。

特点:

  • 虚引用无法获取对象实例(get()总是返回null)
  • 必须配合ReferenceQueue使用
  • 用于在对象被回收前进行清理工作
  • 最弱的引用类型,不会影响对象的生命周期

示例:

java
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantomRef = new PhantomReference<>("hello", queue);
// 对象被回收时,phantomRef会被加入queue

String str = phantomRef.get();  // 总是返回null

应用场景: 对象回收前的清理操作、Native资源释放、堆外内存回收

引用队列

ReferenceQueue用于在软引用、弱引用、虚引用指向的对象被回收时接收通知。

工作流程:

  1. 创建引用时关联一个ReferenceQueue
  2. 当对象被回收时,引用对象会被加入队列
  3. 通过poll()remove()获取被回收的引用

示例:

java
ReferenceQueue<String> queue = new ReferenceQueue<>();
WeakReference<String> ref = new WeakReference<>("hello", queue);

// GC后
Reference<?> recovered = queue.poll();  // 如果对象被回收,返回ref
if (recovered != null) {
    System.out.println("对象已被回收");
}

垃圾识别

垃圾识别的核心问题是判断哪些对象可以被回收。常见的识别策略有两种:引用计数法可达性分析,现代 JVM(HotSpot)全部采用后者。

引用计数法

引用计数法(Reference Counting)为每个对象维护一个引用计数器:每当有新的引用指向该对象时计数器 +1,每当引用失效时计数器 -1,当计数器降为 0 时对象即可被回收。

计数规则:

  1. 对象被新引用指向 → 计数器 +1
  2. 引用失效(变量置 null、超出作用域等)→ 计数器 -1
  3. 计数器为 0 → 对象可立即回收

示例:

java
// 引用计数法伪代码:计数规则
class RefCountObject {
    int refCount = 0;

    void addRef()  { refCount++; }          // 被引用:+1
    void release() {                        // 引用失效:-1
        if (--refCount == 0) {
            collect();                      // 计数为 0,立即回收
        }
    }
}

// 循环引用问题:A 引用 B,B 引用 A
class Node { Node next; }

Node a = new Node();
Node b = new Node();
a.next = b;   // b.refCount = 1
b.next = a;   // a.refCount = 1
a = null;     // 外部引用断开
b = null;     // 但 a、b 的计数仍为 1,永远无法回收

引用计数法循环引用

致命缺陷——无法处理循环引用: 当两个对象互相引用(A→B、B→A)且外部不再引用它们时,两者的计数器都不会降为 0,造成内存泄漏。解决该问题需要额外的兜底机制(如 Python 的标记-清除),实现复杂度随之上升。

优点: 实现简单直观;回收及时(计数为 0 立即可回收);可增量回收,单次暂停时间短;

缺点: 无法处理循环引用;每次引用赋值/失效都需更新计数器,开销大;多线程下计数操作还需原子性保障;

现状: 现代 JVM 不使用引用计数法;Python 以引用计数为主(辅以标记-清除处理循环引用),Swift 的 ARC 也属于引用计数。

可达性分析

可达性分析(Reachability Analysis)是现代 JVM 判定对象生死的主流方案:从一组称为 GC Roots 的根对象出发,沿着引用链向下遍历,能被遍历到的对象是存活对象,遍历不到的对象即为垃圾。

可达性分析

GC Roots 主要包括:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性(static 变量)引用的对象
  • 方法区中常量(如字符串字面量)引用的对象
  • 本地方法栈中 JNI(Native 方法)引用的对象
  • 活跃线程等 JVM 内部对象

示例:

java
public class GcRootsDemo {
    private static Object staticRef = new Object();   // ① 静态变量引用:GC Root
    private static final String CONST = "jvm";        // ② 常量池引用:GC Root

    public void demo() {
        Object local = new Object();                  // ③ 局部变量引用:GC Root
        // ④ 活跃线程、JNI 引用(Native 代码持有的对象)同样是 GC Root
    }
}

为什么能解决循环引用: 判定依据是"从 GC Roots 是否可达",与对象的引用计数无关——循环引用的两个对象只要从 GC Roots 不可达,就同样会被判定为垃圾并回收。

优点: 能正确处理循环引用;不依赖对象自身维护计数;现代 JVM 广泛采用;

缺点: 分析成本高,需要遍历整个对象引用图(是 GC 停顿耗时的主要来源之一);并发标记时还需配合三色标记(见下文)等手段保证正确性。

三色标记

三色标记是并发垃圾回收中标记阶段使用的算法,解决的核心问题:GC 线程标记对象图的同时应用线程还在修改引用,如何保证不漏掉仍在使用的对象。

三种颜色:

颜色含义结局
白色尚未被访问的对象标记结束仍为白色 → 候选垃圾,被回收
灰色已被访问(确认存活),但其引用的对象尚未处理完处理完变黑
黑色自身及其引用的对象都已处理完确定存活

工作流程: 初始时 GC Roots 全部染灰并放入待处理队列;循环取出一个灰色对象,将其引用的白色对象染灰、自身染黑;队列清空后,仍为白色的对象即垃圾。

为什么需要: 若标记期间应用线程把引用改为"黑色对象 → 白色对象",同时切断该白色对象原有的灰色引用路径,白色对象就会被漏标——标记结束后被当作垃圾回收,但黑色对象仍在引用它,这是并发 GC 的致命错误(误回收存活对象)。而标记过程中新产生的垃圾(浮动垃圾)只是多活一个周期,无害。

三色标记漏标推演

解决方案(写屏障): 应用线程修改引用时触发写屏障,保证不漏标:

策略做法使用者
增量更新(Incremental Update)黑色对象新增引用白色对象时,将白色对象重新染灰CMS
原始快照(SATB)引用被切断时记录该白色对象,仍按标记开始时的快照处理G1、Shenandoah

与收集器的对应: CMS 四阶段中的"并发标记 → 重新标记"基于增量更新修正漏标;G1 并发标记周期使用 SATB;ZGC 用指针颜色位(Marked0 / Marked1 交替标记)实现等价机制。而 Serial / Parallel 全程 STW、标记时对象图静止,不需要三色标记——注意"并行"指 GC 线程之间并行,"并发"才指 GC 与应用线程同时运行。

常见GC算法

算法优点缺点内存利用处理速度
Mark-Sweep简单易实现产生碎片中等
Copying无碎片浪费50%内存
Mark-Compact无碎片需移动对象
Generational兼顾效率和利用率实现复杂
分代GC(Generational GC)

将堆划分为年轻代(Young Generation)和老年代(Old Generation), 其中年轻代又细分为 Eden 区和两个 Survivor 区(S0 和 S1)。新创建的对象首先分配在 Eden 区,经过一次 Minor GC 后存活的对象会被移动到 Survivor 区,经过多次 Minor GC 后仍然存活的对象会被晋升到老年代。

分代GC堆结构

GC 术语表:

术语回收范围触发时机频率
Minor GC(新生代 GC)仅新生代(Eden + Survivor)Eden 区满高频
Major GC(老年代 GC)仅老年代老年代空间不足(如 CMS 并发回收)低频
Full GC(整堆 GC)整个堆(新生代 + 老年代)老年代满 / 晋升失败 / 元空间不足 / System.gc()低频

Minor GC 与 Full GC 的关系: Full GC 是超集——回收整个堆时天然包含一次新生代回收;当 Minor GC 的晋升对象超出老年代容纳能力(分配担保检查失败,即 Handle Promotion Failure)时,会级联升级为 Full GC。任何时刻只会有一个 GC 在执行(STW 互斥)。

优点: 分代分离可以针对不同生命周期的对象使用不同的回收策略,提高整体效率;
缺点: 需要维护晋升阈值,若晋升过快可能导致老年代频繁GC,或晋升过慢导致内存压力。

Mark-Sweep(标记-清除)GC

将堆分为两个状态:标记阶段将所有活跃对象标记,清除阶段回收未标记的对象。产生的碎片空间无法被重用,最终导致堆碎片化。

标记清除

优点: 实现简单,标记清除过程容易理解,适合回收暂时不需要的对象;
缺点: 回收后会产生大量碎片,导致空间利用率下降,且回收过程中需要暂停应用线程。

NOTE

内存空间碎片化是指在堆内存中存在大量不连续的空闲空间,这些空间虽然总量足够分配新对象,但由于不连续,无法满足大对象的分配需求,最终导致内存分配失败和性能下降。

Copying(复制)GC

将堆分为From Space和To Space两个相等区域。将From Space中的活跃对象复制到To Space,然后一次性释放整个From Space,从而避免碎片。

复制GC

优点: 回收速度快,无碎片,复制与释放操作效率高;
缺点: 需要预留一半内存作为备用区,内存利用率只有一半,复制过程也会带来额外开销。

Mark-Compact(标记-整理)GC

标记活跃对象后,将它们移动到堆的一端,然后一次性回收另一端的所有空间。

标记整理GC

优点: 回收后无碎片,且保留了对象的相对位置,便于管理;
缺点: 移动对象会触发大量指针更新,整理过程需要额外时间,且暂停时间较长。

常见垃圾回收器

HotSpot 虚拟机提供了多种垃圾回收器,分别面向不同的应用场景:有的追求高吞吐量(减少 GC 总耗时),有的追求低延迟(减少单次 STW 停顿),有的面向单核小内存的客户端环境。默认收集器随 JDK 版本演进:JDK 8 服务端默认 ParallelJDK 9+ 默认 G1JDK 21 起部分平台默认 ZGC

NOTE

下文反复出现的 STW(Stop-The-World) 指垃圾回收期间暂停所有应用线程执行,期间应用无法响应任何请求——STW 停顿时间直接决定应用的延迟表现(详细定义见本章末尾注)。

除 G1、ZGC 等整堆收集器外,多数收集器按新生代 / 老年代成对组合使用:

新生代收集器老年代收集器启用参数说明
SerialSerial Old-XX:+UseSerialGC单线程串行,JDK 8 客户端模式默认
Parallel ScavengeParallel Old-XX:+UseParallelGC多线程并行,吞吐量优先,JDK 8 服务端默认
ParNewCMS-XX:+UseConcMarkSweepGC并行新生代 + 并发老年代(JDK 9 废弃,JDK 14 移除)
G1-XX:+UseG1GCJDK 9+ 默认,停顿可预测
ZGC-XX:+UseZGC亚毫秒级暂停,与堆大小无关

TIP

表中的 "—" 表示该收集器是整堆收集器,不需要配对的老年代收集器:G1 将整个堆划分为 Region,Young GC 与 Mixed GC 都由 G1 自身完成;ZGC 更是对整堆进行并发标记与整理。而 Serial / Parallel / CMS 采用分代配对模式,新生代与老年代各需要一个收集器(如 Serial + Serial Old、ParNew + CMS),因此这两列成对出现。

Serial(串行回收器)

Serial 是单线程垃圾回收器:进行垃圾收集时(无论 Minor GC 还是 Full GC)都必须暂停所有应用线程(STW),直到回收结束。它在新生代使用复制算法(Serial),老年代使用标记-整理算法(Serial Old)。虽然是单线程,但由于没有线程切换与同步开销,在单核或小内存环境下反而效率最高。Parallel 并行回收器采用的正是同一套分代复制流程,仅将 GC 线程扩展为多个(见下文)。

Serial 串行回收器工作流程

回收流程:

  1. 触发:Eden 区空间不足时触发 Minor GC,STW 暂停所有应用线程
  2. 复制:单线程将 Eden 与源 Survivor 中的存活对象复制到空闲 Survivor 区(S0⇄S1 交替使用),每经历一次 Minor GC 对象年龄 +1
  3. 晋升:对象年龄达到晋升阈值(默认 15)时移入老年代
  4. 清空:清空 Eden 与源 Survivor 区,空间立即可用,STW 结束
  5. 老年代回收:老年代空间不足或晋升失败时,Serial Old 执行标记-整理,将存活对象向一端移动后一次性回收边界外的空间

优点: 实现简单、无多线程交互开销、单核下效率最高、内存占用小;

缺点: 全程 STW 停顿时间长,多核环境下无法利用多核优势;

适用场景: 客户端模式、单核 CPU 或内存受限(如嵌入式)环境、小堆(-Xmx 较小)应用;

常用参数: -XX:+UseSerialGC(同时启用 Serial + Serial Old)。

Parallel(并行回收器)

Parallel 与 Serial 的回收流程相同,区别在于使用多线程并行执行垃圾回收,是"吞吐量优先"(Throughput First)的收集器——目标是让 GC 的总耗时尽可能短。新生代使用并行复制(Parallel Scavenge),老年代使用并行标记-整理(Parallel Old)。

Parallel 并行回收器工作流程

回收流程: 与 Serial 基本一致,区别在于:

  1. 触发 Minor GC 后由多个 GC 线程(默认 -XX:ParallelGCThreads=N)并行扫描并复制存活对象
  2. 老年代回收时同样由多线程并行执行标记与整理

优点: 多核环境下吞吐量高,GC 总耗时短;支持自适应调节(-XX:+UseAdaptiveSizePolicy),可自动调整堆分区与晋升阈值;

缺点: 单次 STW 停顿时间较长,不适合对延迟敏感的应用;

适用场景: 批处理、科学计算、离线任务等不关注单次停顿、追求整体吞吐的服务器应用(JDK 8 服务端默认);

常用参数: -XX:+UseParallelGC-XX:+UseParallelOldGC-XX:ParallelGCThreads=N-XX:MaxGCPauseMillis=N(停顿目标)、-XX:GCTimeRatio=N(吞吐目标)。

CMS(并发标记清除)

CMS(Concurrent Mark Sweep)是第一个真正意义上的并发收集器,目标是最短回收停顿。新生代使用 ParNew(并行复制,STW),老年代采用 CMS 的标记-清除算法,其中耗时最长的标记与清除阶段与应用线程并发执行。

CMS 并发标记清除工作流程

回收流程(四个阶段):

  1. 初始标记(STW,很短):仅标记 GC Roots 直接关联的对象
  2. 并发标记(与应用线程并发):基于三色标记(见垃圾识别一节)从 GC Roots 出发遍历对象图,标记所有可达对象
  3. 重新标记(STW,较短):通过增量更新写屏障修正并发标记期间引用发生变化的对象
  4. 并发清除(与应用线程并发):清除标记为垃圾的对象

优点: 并发收集,单次停顿时间短,适合对响应时间敏感的应用;

缺点:

  • 浮动垃圾:并发阶段新产生的垃圾本次无法回收,需预留空间(默认老年代占用 92% 即触发回收)
  • 并发模式失败:并发阶段内存不足时退化为 Serial Old 执行 Full GC,全程 STW,停顿反而更长
  • 内存碎片:标记-清除算法产生碎片,大对象分配失败时需 Full GC 兜底整理
  • CPU 敏感:并发阶段占用 CPU 资源,与业务线程争抢

状态: JDK 9 起标记为废弃(Deprecated),JDK 14 被正式移除,由 G1 取代;

适用场景: 旧版本 JDK 中延迟敏感、堆内存适中的互联网应用(新项目应使用 G1 或 ZGC);

常用参数: -XX:+UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction=70(提前触发阈值)、-XX:+UseCMSCompactAtFullCollection(Full GC 时整理碎片)。

G1(Garbage-First)

G1 是 JDK 9+ 的默认收集器。它将整个堆划分为大小相等的 Region(默认 2048 个),逻辑上仍区分 Eden / Survivor / Old,但回收单位是 Region 而非整个代。回收时优先挑选垃圾占比最高、回收收益最大的 Region("Garbage-First" 名字的由来),并通过停顿预测模型把单次停顿控制在目标以内。

G1 的回收动作只有两种,都是 STW + 并行复制

回收动作触发时机回收对象
Young GCEden Region 被新对象占满全部年轻代 Region(Eden + Survivor)
Mixed GC一次并发标记周期结束后年轻代 + 老年代中垃圾占比最高的部分 Region

两种动作不是串联的一次性流程,而是两个独立循环:日常工作以 Young GC 为主,Mixed GC 按需穿插。

循环一:Young GC(高频,反复进行)

  1. 新对象持续分配在 Eden Region,Eden 逐渐被占满
  2. Eden 占满触发 Young GC(STW):多线程并行把存活对象复制到 Survivor,达到年龄阈值后晋升 Old;复制时借助 RSet 快速定位跨 Region 引用,无需全堆扫描
  3. 被清空的 Region 变为 Free、重新投入分配,回到第 1 步

循环二:并发标记 + Mixed GC(低频,按需进行)

  1. 触发:堆占用率达到 -XX:InitiatingHeapOccupancyPercent(默认 45%),说明老年代已积累到需要回收的程度
  2. 并发标记周期——只负责"找出老年代里哪些 Region 垃圾多",本身不回收,各子阶段穿插在多次 Young GC 之间完成:
    • 初始标记(STW,极短):标记 GC Roots 直接可达的对象(与一次 Young GC 共用停顿)
    • 根区扫描:扫描 Survivor 中指向老年代的引用,作为并发标记的起点
    • 并发标记:与应用线程并行遍历对象图(三色标记 + SATB 写屏障,见垃圾识别一节)
    • 重新标记(STW):补标并发期间新增的漏标对象
    • 清理(STW,很短):统计各 Region 存活对象占比,更新回收价值排序
  3. Mixed GC:标记周期结束后连续进行若干次,每次选取垃圾占比最高的若干 Old Region 作为 CSet(本次回收的 Region 集合),连同年轻代一起 STW 复制回收;单次回收量由停顿预测模型控制(默认 ≤200ms),因此一次标记通常对应多次 Mixed GC
  4. 堆占用率回落后停止 Mixed GC,回到只做 Young GC 的状态

停顿预测模型不是独立阶段,而是贯穿始终的"调度器":基于历史回收数据预测各 Region 的回收耗时,按 -XX:MaxGCPauseMillis(默认 200ms)动态决定每次 Mixed GC 的回收量。

G1 垃圾回收流程

关键技术: RSet(Remembered Set)记录跨 Region 引用,避免全堆扫描;超过 Region 一半的大对象(Humongous)直接进入老年代;

优点: 停顿可预测、支持大堆多核、复制算法无碎片、吞吐与延迟兼顾;

缺点: 小堆(<4GB)上优势不明显,可能不如 Parallel;RSet 占用额外内存(约 10%~20%);

适用场景: 现代大型服务器应用(默认推荐),堆内存 4GB 以上、对停顿有要求的场景;

常用参数: -XX:+UseG1GC-XX:MaxGCPauseMillis=200-XX:InitiatingHeapOccupancyPercent=45-XX:G1HeapRegionSize(Region 大小)、-XX:G1NewSizePercent

ZGC(Z Garbage Collector)

ZGC(Z Garbage Collector)是面向超大堆 + 极低延迟场景的整堆并发收集器,目标是把暂停时间控制在 10ms 以内(通常亚毫秒级),且暂停时间与堆大小无关。它基于**着色指针(Colored Pointers)读屏障(Load Barrier)**实现:对象状态只通过指针颜色标识,对象迁移时应用线程依然可以运行。

ZGC 垃圾回收流程

回收流程:

  1. 标记开始(STW,微秒级):进入并发标记前的短暂暂停
  2. 并发标记:与应用线程并发,遍历对象图标记存活对象
  3. 标记结束(STW,微秒级):完成标记
  4. 并发预备重映射:并发计算迁移集、构建重映射表
  5. 迁移开始(STW,微秒级):开始整理前的短暂暂停
  6. 并发整理 + 重映射:并发移动对象并更新引用,应用线程通过读屏障自动获取对象新地址,无需暂停

着色指针: 64 位指针中抽取 4 位作为颜色状态(Marked0 / Marked1 / Remapped / Finalizable),其余 42 位为对象地址(约 4TB 堆空间);对象状态变化只修改指针颜色,无需访问对象。

优点: 暂停 <1ms 且与堆大小无关、并发整理无碎片、可支撑 TB 级堆;

缺点: 内存占用与 CPU 开销较高,小堆场景优势不明显;

状态: JDK 11 引入(实验特性),JDK 15 转正,JDK 21 起部分平台默认使用;

适用场景: 超大堆、延迟敏感的应用(金融交易、在线广告、大型缓存);

常用参数: -XX:+UseZGC-XX:ConcGCThreads=N(并发 GC 线程数)、-XX:ZCollectionInterval(回收间隔)、-XX:ZUncommit(归还未使用内存)。

收集器对比总结
收集器回收算法GC 线程STW 停顿吞吐量适用场景JDK 状态
Serial复制 + 标记-整理单线程较低客户端/单核/小堆保留
Parallel复制 + 标记-整理多线程较长吞吐优先的服务器应用JDK 8 服务端默认
CMS复制 + 标记-清除多线程+并发较短(两次小 STW)中等延迟敏感(历史方案)JDK 9 废弃 / JDK 14 移除
G1Region 复制多线程+并发可预测(默认 ≤200ms)大堆多核通用场景JDK 9+ 默认
ZGC并发整理(着色指针)多线程+并发<1ms,与堆大小无关较高超大堆、极低延迟JDK 15 转正 / JDK 21 默认(部分)

注: STW 即 Stop-The-World,指的是 JVM 在某些操作(最常见的是垃圾回收)期间暂停所有用户线程的执行,只允许虚拟机自身线程运行完成必要的工作。STW 期间应用线程完全被挂起,无法响应请求或执行业务逻辑,因此会直接影响应用的响应延迟。

运行时数据区

运行时数据区是JVM在内存中为程序执行分配的区域,根据是否线程隔离分为线程私有区域和线程共享区域。

运行时数据区

程序计数器

每个线程都有自己独立的程序计数器。

  • 记录当前线程执行的字节码指令地址
  • 如果执行的是Native方法,则为空
  • 这是唯一不会抛出OutOfMemoryError的区域

Java虚拟机栈

  • 每个线程执行Java方法时,JVM会为该方法压入一个栈帧(Stack Frame),栈帧是Java虚拟机栈中最小的执行单位
  • 栈帧内含局部变量表、操作数栈、动态链接信息以及方法返回地址等数据,用于记录当前方法的执行状态与调用关系
  • 如果线程请求的栈深度超出JVM配置的最大值,会立即抛出StackOverflowError,防止过度递归或无限循环导致堆栈耗尽
  • 当尝试扩展线程栈以适应更多栈帧时失败(例如本地内存不足),会抛出OutOfMemoryError

Java虚拟机栈

栈帧组成详解:

组件说明大小
局部变量表存储方法参数和局部变量,按变量类型占用1-2个槽位编译时确定
操作数栈存储指令执行的中间结果,最大深度编译时确定编译时确定
动态链接指向运行时常量池中当前类的运行时常量池项,支持方法调用的动态分派取决于方法
方法返回地址方法调用后恢复执行位置的指针,PC寄存器值固定大小

栈帧生命周期:

  1. 方法调用 → 创建栈帧
  2. 栈帧入栈(压栈)
  3. 执行方法体中的字节码指令
  4. 方法返回 → 栈帧出栈(弹栈)
  5. 栈帧销毁

本地方法栈

执行Native方法时使用的栈, 与Java虚拟机栈类似,但服务于Native方法调用。具体使用何种语言实现由JVM决定.

  • JVM内存管理最大的一块区域
  • 所有对象和数组都分配在堆上
  • 垃圾回收器主要管理的区域
  • 堆可以是物理不连续的,但逻辑上应该是连续的
  • 堆溢出时,抛出OutOfMemoryError: Java heap space
  • 字符串常量池:位于堆中(JDK 7 起),是“字符串内容”到“堆中唯一 String 对象引用”的映射表,本身不存储字符串内容,全局唯一(详见字符串常量池一节)。

方法区

  • 存储类的结构信息:运行时常量池、字段数据、方法数据、方法代码、构造函数代码等
  • 元空间使用本地内存,大小不受JVM堆大小限制
  • 方法区溢出时,抛出OutOfMemoryError: Metaspace
  • HotSpot JVM的方法区实现, JDK7及之前版本为永久代(PermGen),JDK8及之后版本为元空间(Metaspace)。
  • 永久代使用堆内存,容易发生内存溢出;元空间使用本地内存,提升了性能和稳定性,但仍需合理配置以避免溢出。
  • 字符串常量池的位置变化:JDK 6 及以前位于方法区(永久代),JDK 7 起移至堆,与运行时常量池是不同结构(详见字符串常量池一节)。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分(JDK 8 及以后为元空间,JDK 7 及以前为永久代),每个类/接口各有一份,是类文件中常量池(Constant Pool)在运行时的拷贝,并支持运行期动态添加常量。

存储内容(三类):

类别内容说明
字面量数字字面量(int/long/float/double 等)、字符串内容字符串内容以 CONSTANT_String 条目记录在常量池中;使用时实际上是拿字符串内容去字符串常量池(堆)中查找对应的 String 对象引用
符号引用类(CONSTANT_Class)、方法(CONSTANT_Methodref)、字段(CONSTANT_Fieldref)等在类加载的解析阶段被替换为直接引用
动态常量运行期动态添加的常量(如 invokedynamic 产生的 CONSTANT_Dynamic类加载后仍可动态添加

运行时常量池结构

解析(符号引用 → 直接引用): 类加载的解析阶段将符号引用替换为直接引用,大多数 JVM 采用懒惰解析(首次使用时才解析):

符号引用直接引用
CONSTANT_ClassInstanceKlass 指针
CONSTANT_MethodrefMethod 指针
CONSTANT_Fieldref字段偏移量

与字符串常量池的区别: 两者是完全不同的结构——运行时常量池每类一份、存储字面量与符号引用(位于元空间);字符串常量池全局唯一、存储 String 对象的引用(JDK 7 起位于堆中)。JVM 根据运行时常量池中 CONSTANT_String 条目的描述信息,在字符串常量池中查找或创建实际的 String 对象——对象本体在堆中,常量池条目只是指向它的引用。

String.intern() 机制: 在字符串常量池中查找内容相同的字符串——找到则返回池中已有引用(复用,不新建对象);未找到则将当前字符串对象引用加入字符串常量池中,保证同一内容的字符串全局只有一个 String 实例。

动态性: 运行时常量池在类加载时生成,并支持运行期动态变化——字面量与符号引用在首次使用时被解析为直接引用(条目被替换),invokedynamic 等动态机制还会向其中添加新的常量条目(CONSTANT_Dynamic);它是类和对象元数据的重要组成部分。注意:String.intern() 操作的是字符串常量池(堆),不会向运行时常量池添加条目。

与 GC Roots 的关系: 运行时常量池中常量(如字符串字面量)引用的对象属于 GC Roots(见垃圾识别一节)——引用本身位于元空间,不依赖堆内对象存活。

字符串常量池

字符串常量池(String Constant Pool / StringTable)是 HotSpot 实现层面的全局数据结构(JVM 规范中并无此术语,规范只有运行时常量池),负责"字符串内容 → 堆中唯一 String 对象引用"的映射,保证相同内容的字符串全局共享同一个实例。字符串常量池本身只存放引用,真正的 String 对象及其 char[] 数据在堆中。

位置变化:

JDK 版本位置说明
JDK 6 及以前方法区(永久代)与运行时常量池同在永久代,大量字符串易导致 PermGen space 溢出
JDK 7 起移至堆中,受 -Xmx 管理;溢出时报 Java heap space

工作机制:

  1. 类加载时,类文件常量池中的 CONSTANT_String 条目进入运行时常量池(元空间),此时仅为符号
  2. 首次使用时(懒惰解析),JVM 到字符串常量池查找内容相同的字符串:找到则直接复用池中的 String 引用;未找到则在堆中创建 String 对象并将其引用加入字符串常量池
  3. String.intern() 手动触发同样的"查找 / 入字符串常量池"逻辑

示例:

java
String a = "jvm";             // 字面量:文本在类加载时进运行时常量池,首次使用时入字符串常量池
String b = "jvm";             // 字面量:复用池中引用
System.out.println(a == b);   // true,同一对象

String c = new String("jvm"); // 显式 new:堆中新对象,不进字符串常量池
System.out.println(a == c);           // false,不同对象
System.out.println(a == c.intern());  // true,intern 后指向字符串常量池中的对象

与运行时常量池的区别: 运行时常量池每类一份、存储字面量与符号引用(位于元空间);字符串常量池全局唯一一张表、存储 String 对象的引用(JDK 7 起位于堆中)——两者的位置、份数、内容均不同(对比图见运行时常量池一节)。

本地接口

本地接口(Native Interface)是Java与外部本地代码(Native Code)交互的桥梁,允许Java程序调用其他编程语言(主要是C/C++)编写的代码。

JNI

Java Native Interface(JNI)是Java官方定义的本地代码交互标准接口。

使用流程:

  1. 声明本地方法 - 使用native关键字声明本地方法,System.loadLibrary()加载库

    java
    package com.example.math;
    
    public class Calculator {
        public native int add(int a, int b);
    
        static {
            System.loadLibrary("calculator");  // 加载本地库
        }
    
        public static void main(String[] args) {
            System.out.println(new Calculator().add(5, 3));  // 输出: 8
        }
    }

    System.loadLibrary()库名称转换规则:

    • Linux"calculator"libcalculator.so(自动添加lib前缀和.so后缀)
    • Windows"calculator"calculator.dll(自动添加.dll后缀)
    • macOS"calculator"libcalculator.dylib(自动添加lib前缀和.dylib后缀)
  2. 生成头文件 - javacjavah -jni 生成本地语言头文件

    bash
    javac com/example/math/Calculator.java  # 编译Java文件
    javah -jni com.example.math.Calculator  # 生成 com_example_math_Calculator.h 头文件
  3. 实现本地方法 - 本地语言中按JNI规范命名Java_包名_类名_方法名()实现

    c
    #include <jni.h>
    #include "com_example_math_Calculator.h"  // 对应生成的头文件
    
    JNIEXPORT jint JNICALL
    Java_com_example_math_Calculator_add(JNIEnv *env, jobject thisObj, jint a, jint b) {
        return a + b;
    }
  4. 编译库文件 - 编译为.so(Linux)或.dll(Windows)动态库

    bash
    gcc -shared -fPIC \
        -I${JAVA_HOME}/include \
        -I${JAVA_HOME}/include/linux \
        -o libcalculator.so Calculator.c  # -shared: 生成共享库  -fPIC: 位置独立代码  -I: 头文件路径
  5. 运行程序 - Java程序调用native方法,JVM加载动态库执行

    bash
    java -Djava.library.path=. com.example.math.Calculator  # -D: 指定库查找路径  com.example.math.Calculator: 完全限定类名
    # 输出: 8

NOTE

  • 过度使用 JNI 既会降低跨平台性,又可能因 Native 代码的 bug 直接导致 JVM 崩溃,因此必须谨慎处理内存管理;
  • JNI 调用涉及方法查找、类型转换与上下文切换,开销显著,需尽量避免高频调用.

JVM参数

JVM参数用于配置Java虚拟机的行为,包括内存分配、垃圾回收、编译优化等。参数分为标准参数(-开头)和扩展参数(-XX:开头)两类。

堆内存参数

参数说明示例
-Xms堆初始大小-Xms512m
-Xmx堆最大大小-Xmx2048m
-Xmn年轻代大小-Xmn512m
-XX:NewRatio老年代与年轻代比例(老年代:年轻代)-XX:NewRatio=2
-XX:SurvivorRatioEden与Survivor比例(Eden:Survivor)-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold大对象直接进入老年代的阈值-XX:PretenureSizeThreshold=3145728

非堆内存参数

参数说明示例
-XX:MetaspaceSize元空间初始大小-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize元空间最大大小-XX:MaxMetaspaceSize=256m
-XX:CompressedClassSpaceSize压缩类空间大小-XX:CompressedClassSpaceSize=256m

垃圾回收器参数

参数说明示例
-XX:+UseG1GC使用G1垃圾回收器-XX:+UseG1GC
-XX:+UseParallelGC使用并行垃圾回收器(年轻代)-XX:+UseParallelGC
-XX:+UseParallelOldGC使用并行垃圾回收器(老年代)-XX:+UseParallelOldGC
-XX:+UseConcMarkSweepGC使用CMS垃圾回收器(已废弃)-XX:+UseConcMarkSweepGC
-XX:+UseSerialGC使用Serial垃圾回收器-XX:+UseSerialGC
-XX:+UseZGC使用ZGC垃圾回收器-XX:+UseZGC
-XX:ParallelGCThreads并行GC线程数-XX:ParallelGCThreads=8
-XX:ConcGCThreads并发GC线程数-XX:ConcGCThreads=2
-XX:MaxGCPauseMillisG1最大GC停顿时间(毫秒)-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercentG1启动并发GC的堆占用率-XX:InitiatingHeapOccupancyPercent=45
-XX:+PrintGCDetails打印GC详细信息-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps打印GC时间戳-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps打印GC日期戳-XX:+PrintGCDateStamps
-XloggcGC日志输出文件-Xloggc:/var/log/gc.log

JIT编译参数

参数说明示例
-Xint纯解释执行,禁用JIT编译-Xint
-Xcomp纯编译执行,禁用解释器-Xcomp
-XX:+TieredCompilation启用分层编译(C1+C2)-XX:+TieredCompilation
-XX:TieredStopAtLevel编译停止级别(1-4)-XX:TieredStopAtLevel=4
-XX:CompileThresholdJIT编译调用计数阈值-XX:CompileThreshold=10000
-XX:OnStackReplacePercentage栈上替换编译阈值百分比-XX:OnStackReplacePercentage=140
-XX:CICompilerCount编译器线程数-XX:CICompilerCount=4
-XX:-TieredCompilation禁用分层编译-XX:-TieredCompilation

类加载参数

参数说明示例
-cp-classpath类搜索路径-cp ./lib/*:./classes
-Xbootclasspath启动类搜索路径-Xbootclasspath:${JAVA_HOME}/lib/rt.jar
-XX:+TraceClassLoading追踪类加载事件-XX:+TraceClassLoading
-XX:+TraceClassUnloading追踪类卸载事件-XX:+TraceClassUnloading
-XX:+PrintClassHistogram打印类直方图-XX:+PrintClassHistogram

本地接口参数

参数说明示例
-Djava.library.path本地库搜索路径-Djava.library.path=./lib:/usr/local/lib

调试和诊断参数

参数说明示例
-XX:+UnlockDiagnosticVMOptions解锁诊断选项-XX:+UnlockDiagnosticVMOptions
-XX:+PrintVMOptions打印所有JVM参数-XX:+PrintVMOptions
-XX:+PrintCommandLineFlags打印命令行参数-XX:+PrintCommandLineFlags
-XX:+PrintFlagsFinal打印最终生效的参数-XX:+PrintFlagsFinal
-agentlib:jdwp启用JDWP远程调试-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
-XX:HeapDumpPath堆转储文件路径-XX:HeapDumpPath=/var/log/heapdump.hprof
-XX:+HeapDumpOnOutOfMemoryOOM时自动堆转储-XX:+HeapDumpOnOutOfMemory

其他常用参数

参数说明示例
-server服务器模式(优化吞吐量)-server
-client客户端模式(优化启动速度)-client
-XX:+AggressiveOpts启用激进优化-XX:+AggressiveOpts
-XX:+UseStringDeduplication启用字符串去重(G1)-XX:+UseStringDeduplication
-XX:StringDeduplicationAgeThreshold字符串去重年龄阈值-XX:StringDeduplicationAgeThreshold=3
-Dfile.encoding文件编码格式-Dfile.encoding=UTF-8
-Djava.io.tmpdir临时目录路径-Djava.io.tmpdir=/tmp
-Duser.timezone时区设置-Duser.timezone=Asia/Shanghai
-verbose:gc输出GC日志-verbose:gc
-XX:+DisableExplicitGC禁用System.gc()调用-XX:+DisableExplicitGC

一个典型的JVM启动命令:

bash
java -server \ # 以server模式启动
  -Xms1024m -Xmx2048m \
  -Xmn512m \
  -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:InitiatingHeapOccupancyPercent=45 \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -Xloggc:/var/log/gc.log \
  -XX:+HeapDumpOnOutOfMemory \
  -XX:HeapDumpPath=/var/log/heapdump.hprof \
  -Dfile.encoding=UTF-8 \
  -Djava.library.path=./lib \
  -Duser.timezone=Asia/Shanghai \
  com.example.Application

Released under the MIT License.