JVM 核心知识
一、类加载子系统
1.1 类加载完整生命周期
JVM 采用懒加载机制,类不会在启动时一次性全部加载,而是用到才加载、不用不加载,节省内存、提升启动速度。
完整生命周期:加载 → 链接 → 初始化 → 使用 → 卸载
阶段详解
加载:读取磁盘 .class 字节码文件,解析为内存数据结构,生成 Class 对象。
链接(分为三步)
验证:校验字节码格式、安全性、合法性,防止恶意/错误字节码。
准备:为static 静态变量分配内存并赋默认初始值(0、null、false),仅赋系统默认值。
解析:将代码中的符号引用统一转换为内存直接引用。
初始化:类加载唯一执行业务逻辑的阶段。JVM 主动执行静态代码块、为静态变量赋予代码中自定义初始值,是静态资源真正初始化的阶段。
使用:程序正常调用类的属性、方法、执行业务逻辑。
卸载:类无任何引用、类加载器被回收,释放元空间内存。
1.2 四大类加载器体系
1.2.1 启动类加载器(Bootstrap ClassLoader)
顶层核心加载器,HotSpot 虚拟机中由C++ 编写,Java 代码无法获取其引用,调用获取方法返回
null。核心职责:加载 Java 底层核心类库(rt.jar 等基础核心依赖)。
加载特点:按名索骥,非全盘扫描
- JVM 启动不会遍历加载 lib 目录所有 jar,核心类库范围在 JVM 底层 C++ 源码中硬编码固定。
核心安全两道防线(防止核心类篡改)
字节码校验机制:若直接替换官方 rt.jar,JVM 启动会校验核心类结构、本地方法映射。一旦检测到类结构被篡改、方法缺失,直接抛出
SecurityException/LinkageError,拒绝启动。双亲委派机制:禁止自定义核心类覆盖原生类。例如自定义
java.lang.String无法生效。
双亲委派拦截流程
程序加载自定义
java.lang.String时,应用类加载器优先向上委派;依次委派至扩展类加载器、最终抵达启动类加载器;
启动类加载器检测到核心类库已存在该类,直接返回官方原生 String 类;
自定义核心类彻底失效,永远不会被加载执行。
1.2.2 扩展/平台类加载器(Extension / Platform ClassLoader)
负责加载 Java 拓展类库、系统扩展依赖,承接启动类加载器与应用类加载器的中间委派能力。
1.2.3 应用程序类加载器(Application ClassLoader)
又称系统类加载器,可通过
ClassLoader.getSystemClassLoader()获取。日常开发中,所有自定义类、第三方 Jar 包默认由该加载器加载。
1.2.4 自定义类加载器(Custom ClassLoader)
实现方式:开发者继承
ClassLoader类,重写findClass()方法。使用场景:热部署、代码加密、自定义资源加载、特殊业务类加载需求。
1.3 Class 常量池
Java 源码编译为 .class 文件后,文件内部会生成一张常量池表,存储编译期固定数据,是运行时常量池的基础。
存储内容
字面量:字符串字面量、final 修饰常量、基本数据类型固定值。
符号引用:类和接口全限定名、字段名称与描述符、方法名称与描述符。
核心作用
编译期无法确定类、方法、字段的真实内存地址,因此用字符串符号临时占位;在类加载解析阶段,统一替换为真实内存直接引用。
二、执行引擎子系统
2.1 核心职责
将 .class 字节码指令,翻译、优化为操作系统、CPU 可识别的机器指令并执行,是 JVM 真正执行业务逻辑的核心模块。
2.2 执行架构:解释器 + JIT 混合模式
现代 HotSpot JVM 默认采用混合执行模式,兼顾项目快速启动与长期运行高性能。
┌──────────────────────────────────────────────────┐ │ 执行引擎子系统 │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ │ │ 解释器 │ │ JIT编译器 │ │ 垃圾 │ │ │ │ (Interpreter)│ │ (JIT Compiler)│ │ 收集器 │ │ │ └──────────────┘ └──────────────┘ │ (GC) │ │ │ │ │ └────────┘ │ └───────────┼────────────────┼─────────────────────┘ ▼ ▼ 逐行翻译 热点编译 启动快、执行慢 启动慢、执行极快两者协作机制
项目启动初期:解释器逐行解释执行字节码,无需预编译,保证项目秒级启动、快速响应。
运行过程中:JVM 内置计数器持续探测热点代码(高频循环、频繁调用的代码)。
热点命中后:JIT 编译器后台将热点字节码编译、优化为本地机器码并缓存。
无缝替换执行:后续再次调用该代码,直接执行优化后的机器码,放弃逐行解释,大幅提升运行吞吐量。
2.3 GC 模块定位
垃圾回收器(GC)是执行引擎子系统的核心组件,后台静默运行,自动回收堆内存中失效对象,防止内存溢出、内存泄漏。
三、垃圾回收 GC 核心体系
3.1 七大经典垃圾回收器分类
新生代 (Young): Serial ─── Parallel Scavenge ─── ParNew │ │ │ │ │ │ 老年代 (Old): Serial Old ─── Parallel Old ─── CMS 整堆 / 区域化回收: G1 (Garbage First)3.1.1 串行收集器(Serial + Serial Old)
工作模式:单线程回收,GC 期间全程 STW(暂停所有用户线程)。
回收算法:新生代复制算法、老年代标记-整理算法。
适用场景:客户端程序、单核 CPU、极小内存环境(几十MB~两百MB)。
3.1.2 并行吞吐量收集器(Parallel Scavenge + Parallel Old)
定位:JDK8 默认垃圾回收器。
工作模式:多线程并行回收,依旧存在 STW,但利用多核 CPU 大幅缩短回收耗时。
核心目标:最大化 CPU 利用率,追求高吞吐量。
适用场景:后台批处理、数据计算、无高交互的服务端业务。
3.1.3 并发低延迟收集器(ParNew + CMS)
核心优势:部分 GC 阶段与用户线程并发执行,大幅降低停顿时间。
核心目标:追求低延迟、低停顿。
缺点:采用标记-清除算法,产生内存碎片;对 CPU 资源消耗敏感。
版本现状:JDK9 标记废弃,JDK14 彻底移除。
3.2 三色标记算法(并发 GC 核心原理)
三色标记是 CMS、G1、ZGC 等现代并发回收器的核心存活判定算法,用于并发标记阶段精准区分对象存活状态。
[ 黑色 (Black) ] ───> [ 灰色 (Gray) ] ───> [ 白色 (White) ] (自身及下属全完工) (自身完工,下属未遍历完) (未访问 / 垃圾对象)状态定义
白色:初始状态,所有对象默认白色;标记结束仍为白色则判定为垃圾。
灰色:中间过渡状态,当前对象已扫描,但引用的子对象未全部遍历完成。
黑色:完全存活对象,自身及所有引用子对象全部扫描完毕,安全存活,不会被回收。
3.2.1 正常标记流程
初始状态:堆内所有对象标记为白色。
初始标记:从 GC Roots 出发,将直接关联的第一层对象标记为灰色。
并发标记:遍历灰色对象引用,将白色子对象依次置灰;遍历完成的灰色对象转为黑色。
最终状态:所有灰色对象全部转正,剩余白色对象统一回收。
3.2.2 并发标记致命问题:对象漏标
单 GC 线程标记时三色标记无问题,但用户线程与 GC 线程并发执行时,会出现漏标问题,导致存活对象被误回收。
漏标场景还原
初始状态:黑色对象 A 引用灰色对象 B,灰色对象 B 引用白色对象 C。
用户线程同时执行两步操作:
B 断开对 C 的引用(B.c = null);
黑色 A 新建引用指向白色 C(A.c = C)。
【篡改前】 【篡改后】 [ A (黑) ] [ A (黑) ] ──(新引用)──> [ C (白) ] │ ▼ [ B (灰) ] ──> [ C (白) ] [ B (灰) ] (已断开引用)问题结果
GC 仅扫描灰色对象 B,B 无引用 C,扫描完毕后 B 变为黑色;黑色对象不会二次扫描,导致存活对象 C 始终为白色,最终被当做垃圾回收,引发空指针异常。
结论:CMS 的重新标记、G1 的最终标记,都是为了修复并发漏标问题。
3.2.3 CMS 与 G1 漏标修复机制对比
1. CMS 重新标记(增量更新算法)
核心逻辑:并发期间检测到黑色对象指向白色对象,通过写屏障将黑色对象重置为灰色。
修复阶段:全程 STW,批量重新扫描所有被重置的灰色对象引用链。
缺点:需扫描大量对象,甚至连带扫描新生代,STW 停顿时间长、性能差。
2. G1 最终标记(SATB 原始快照算法)
核心逻辑:并发期间检测到灰色对象断开白色对象引用,通过写屏障将旧引用存入 SATB 缓冲区,并提前将对象置灰。
修复阶段:STW 仅处理缓冲区少量漏网数据,无需全量扫描。
优点:停顿时间极短、开销可控、性能稳定。
3.3 GC 分类体系(面试高频)
3.3.1 部分收集(Partial GC)
① 新生代收集(Minor GC / Young GC)
回收范围:仅新生代 Eden、S0、S1 区域。
触发条件:Eden 区内存耗尽。
核心特点:对象朝生夕灭,触发频繁;采用复制算法,速度极快,STW 耗时仅几毫秒至几十毫秒。
② 老年代收集(Major GC / Old GC)
回收范围:仅针对老年代区域。
触发条件:老年代内存空间不足。
特点:速度比 Minor GC 慢 10 倍以上,STW 耗时更长;多数场景会伴随 Full GC。
③ 混合收集(Mixed GC,G1 专属)
回收范围:全部新生代 + 部分垃圾最多的老年代 Region。
触发机制:老年代内存占用达到阈值
XX:InitiatingHeapOccupancyPercent。特点:按回收价值筛选区域,精准回收,可控停顿时间,适配大内存服务。
3.3.2 整堆收集(Full GC,最重 GC)
回收范围:新生代、老年代、元空间,整堆全量回收。
四大核心触发条件(必考)
老年代空间不足,对象晋升失败;
元空间/方法区加载类过多,内存耗尽;
代码主动调用
System.gc()(建议回收,大概率触发 Full GC);Minor GC 空间分配担保失败。
核心特点:全局 STW,暂停所有业务线程,服务卡顿严重;线上调优核心目标:减少 Full GC 频次与耗时。
四、运行时数据区子系统
运行时数据区分为线程共享区(全局共用)和线程私有区(线程隔离、随线程生死)两大模块。
4.1 线程共享内存区域
4.1.1 堆 Heap(GC 核心区域)
JVM 最大内存区域,所有对象实例、数组默认在堆内存分配,是 GC 主要管理和回收的区域。
对象完整内存结构
[ 栈 ] [ 堆 (Heap) ] user ──(存储指针)──> ┌──────────────────────────────┐ │ 1. 对象头 (Mark Word + Klass) │ ├──────────────────────────────┤ │ 2. 实例数据 (id, name, age) │ ├──────────────────────────────┤ │ 3. 对齐填充 (补齐为8字节倍数) │ └──────────────────────────────┘对象创建完整流程
类加载检查:校验目标类是否已加载,未加载则优先执行类加载流程。
内存分配:堆内存规整采用「指针碰撞」,内存碎片多采用「空闲列表」。
并发安全保障(TLAB):为每个线程分配私有缓冲区,多线程创建对象互不抢占、避免并发冲突。
内存零值初始化:对象内存(不含对象头)默认赋 0、null,保证实例变量可直接访问。
设置对象头:写入哈希码、GC 分代年龄、锁状态、类指针等核心元数据。
构造方法初始化:执行
<init>方法,按业务代码为属性赋值,完成对象创建。
堆内存分代结构
┌────────────────────────────────────────────────────────────┐ │ 堆内存 (Heap) │ ├─────────────────────────────────────┬──────────────────────┤ │ 新生代 (Young) │ 老年代 (Old) │ ├──────────────────┬──────────┬───────┤ │ │ Eden (伊甸园) │ From (S0)│To (S1)│ 长期存活的老对象 │ └──────────────────┴──────────┴───────┴──────────────────────┘新生代(对象新手村)
Eden 区:绝大多数新对象的诞生、驻留区域。
Survivor S0/S1:存活对象中转站。
GC 流转规则:Eden 满触发 Minor GC,存活对象复制至 S0/S1,年龄+1;下次 GC 时,Eden + 上一轮 Survivor 存活对象转移至另一块 Survivor 区,来回流转、年龄递增。
老年代(对象养老院)
存储长期存活、生命周期稳定的对象,三种晋升机制:
高龄晋升:对象年龄达到阈值(默认15,可通过
-XX:MaxTenuringThreshold修改),晋升老年代。大对象直接晋升:超大数组、长字符串等大对象,新生代无法容纳,直接分配至老年代,避免频繁复制损耗性能。
动态年龄判定:Survivor 中同年龄对象总大小超过 Survivor 区域一半,该年龄及以上对象直接晋升老年代。
垃圾存活判定:可达性分析算法
HotSpot 虚拟机核心垃圾判定算法:以GC Roots为起始节点,遍历引用链,不可达对象判定为垃圾。
GC Roots 核心来源
虚拟机栈局部变量表引用的对象(方法内正在使用的局部对象);
方法区静态属性、常量引用的对象;
字符串常量池中的引用对象;
本地方法栈 JNI 引用的 Native 关联对象;
JVM 内部常驻对象(Class 对象、系统异常对象、类加载器等)。
4.1.2 元空间 Metaspace(JDK8+)
JDK8 废弃永久代(PermGen),采用元空间,直接使用操作系统本地内存,不占用堆内存。
核心存储内容
类元信息:类名、父类、接口、修饰符、类结构描述;
字段、方法信息:名称、类型、参数、修饰符、方法字节码;
运行时常量池、字符串常量池引用;
虚方法表、接口方法表;
JIT 编译优化缓存、逃逸分析、计数器数据。
运行时常量池
每个类加载后独立生成,存储编译期字面量、符号引用;在类加载解析阶段,将符号引用动态翻译为内存直接引用。
字符串常量池
位置变迁:JDK7 前在方法区,JDK7 及以后迁移至堆内存;
特性:全局共享、自动去重;创建字符串时优先查询常量池,存在则复用引用,不存在则新建对象。
虚方法表(vtable / itable)
vtable:存储所有可重写方法的内存地址,支撑多态动态绑定;
itable:存储接口方法的具体实现地址;
方法调用时直接查表定位指令入口,执行效率极高。
4.2 线程私有内存区域
线程私有区域随线程创建而生、随线程销毁回收,线程间完全隔离,无并发竞争问题。
4.2.1 程序计数器
JVM 最小内存单元,仅存储指令指针地址,唯一无 OOM 的内存区域。
执行普通 Java 方法:记录当前执行的字节码指令地址。
执行 Native 方法:计数器值为 Undefined,由操作系统直接执行,不受 JVM 管控。
4.2.2 虚拟机栈(Java 栈)
每个方法执行对应一个栈帧,方法调用、执行、结束对应栈帧入栈、运行、出栈。每个栈帧包含四大核心组件:
1. 局部变量表
编译期确定内存大小,是固定长度数组,存放方法参数、局部变量、基本数据类型、对象引用指针、返回地址。运行期间大小不可变更。
2. 操作数栈
执行引擎的临时计算工作台,基于栈式架构运行。所有加减乘除、赋值运算均通过压栈、弹栈完成,是字节码运算的核心载体。
3. 动态链接
栈帧持有运行时常量池引用,运行期将字节码中的符号引用,动态转换为方法、字段的直接内存引用,支撑多态与动态绑定。
4. 方法返回地址
记录方法调用位置,方法正常 return 退出或异常退出时,恢复上层方法执行上下文,继续执行业务逻辑。
4.2.3 本地方法栈
专门为native本地方法服务。Native 方法由 C/C++ 编写,编译为系统底层动态库,用于操作操作系统内存、硬件、实现高性能底层能力,弥补 Java 底层操作短板。