JVM 核心知识

JVM 核心知识

一、类加载子系统

1.1 类加载完整生命周期

JVM 采用懒加载机制 ,类不会在启动时一次性全部加载,而是用到才加载、不用不加载,节省内存、提升启动速度。

完整生命周期:加载 → 链接 → 初始化 → 使用 → 卸载

阶段详解

  • 加载:读取磁盘 .class 字节码文件,解析为内存数据结构,生成 Class 对象。

  • 链接(分为三步)

    1. 验证:校验字节码格式、安全性、合法性,防止恶意/错误字节码。

    2. 准备 :为 static 静态变量分配内存并赋默认初始值(0、null、false),仅赋系统默认值。

    3. 解析 :将代码中的符号引用 统一转换为内存直接引用

  • 初始化:类加载唯一执行业务逻辑的阶段。JVM 主动执行静态代码块、为静态变量赋予代码中自定义初始值,是静态资源真正初始化的阶段。

  • 使用:程序正常调用类的属性、方法、执行业务逻辑。

  • 卸载:类无任何引用、类加载器被回收,释放元空间内存。

1.2 四大类加载器体系

1.2.1 启动类加载器(Bootstrap ClassLoader)

  • 顶层核心加载器,HotSpot 虚拟机中由 C++ 编写 ,Java 代码无法获取其引用,调用获取方法返回 null

  • 核心职责:加载 Java 底层核心类库(rt.jar 等基础核心依赖)。

  • 加载特点:按名索骥,非全盘扫描

    • JVM 启动不会遍历加载 lib 目录所有 jar,核心类库范围在 JVM 底层 C++ 源码中硬编码固定
核心安全两道防线(防止核心类篡改)
  1. 字节码校验机制 :若直接替换官方 rt.jar,JVM 启动会校验核心类结构、本地方法映射。一旦检测到类结构被篡改、方法缺失,直接抛出 SecurityException/LinkageError,拒绝启动。

  2. 双亲委派机制 :禁止自定义核心类覆盖原生类。例如自定义 java.lang.String 无法生效。

双亲委派拦截流程
  1. 程序加载自定义 java.lang.String 时,应用类加载器优先向上委派;

  2. 依次委派至扩展类加载器、最终抵达启动类加载器;

  3. 启动类加载器检测到核心类库已存在该类,直接返回官方原生 String 类

  4. 自定义核心类彻底失效,永远不会被加载执行。

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 默认采用混合执行模式,兼顾项目快速启动与长期运行高性能。

Plain 复制代码
┌──────────────────────────────────────────────────┐
│                  执行引擎子系统                   │
│                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌────────┐  │
│  │  解释器       │  │  JIT编译器   │  │  垃圾   │  │
│  │ (Interpreter)│  │ (JIT Compiler)│  │ 收集器  │  │
│  └──────────────┘  └──────────────┘  │ (GC)   │  │
│           │                │         └────────┘  │
└───────────┼────────────────┼─────────────────────┘
            ▼                ▼
         逐行翻译           热点编译
         启动快、执行慢      启动慢、执行极快

两者协作机制

  1. 项目启动初期:解释器逐行解释执行字节码,无需预编译,保证项目秒级启动、快速响应。

  2. 运行过程中 :JVM 内置计数器持续探测热点代码(高频循环、频繁调用的代码)。

  3. 热点命中后:JIT 编译器后台将热点字节码编译、优化为本地机器码并缓存。

  4. 无缝替换执行:后续再次调用该代码,直接执行优化后的机器码,放弃逐行解释,大幅提升运行吞吐量。

2.3 GC 模块定位

垃圾回收器(GC)是执行引擎子系统的核心组件,后台静默运行,自动回收堆内存中失效对象,防止内存溢出、内存泄漏。

三、垃圾回收 GC 核心体系

3.1 七大经典垃圾回收器分类

Plain 复制代码
新生代 (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 等现代并发回收器的核心存活判定算法,用于并发标记阶段精准区分对象存活状态。

Plain 复制代码
[ 黑色 (Black) ]  ───>  [ 灰色 (Gray) ]  ───>  [ 白色 (White) ]
 (自身及下属全完工)        (自身完工,下属未遍历完)     (未访问 / 垃圾对象)

状态定义

  • 白色:初始状态,所有对象默认白色;标记结束仍为白色则判定为垃圾。

  • 灰色:中间过渡状态,当前对象已扫描,但引用的子对象未全部遍历完成。

  • 黑色:完全存活对象,自身及所有引用子对象全部扫描完毕,安全存活,不会被回收。

3.2.1 正常标记流程

  1. 初始状态:堆内所有对象标记为白色。

  2. 初始标记:从 GC Roots 出发,将直接关联的第一层对象标记为灰色。

  3. 并发标记:遍历灰色对象引用,将白色子对象依次置灰;遍历完成的灰色对象转为黑色。

  4. 最终状态:所有灰色对象全部转正,剩余白色对象统一回收。

3.2.2 并发标记致命问题:对象漏标

单 GC 线程标记时三色标记无问题,但用户线程与 GC 线程并发执行时,会出现漏标问题,导致存活对象被误回收。

漏标场景还原

初始状态:黑色对象 A 引用灰色对象 B,灰色对象 B 引用白色对象 C。

用户线程同时执行两步操作:

  1. B 断开对 C 的引用(B.c = null);

  2. 黑色 A 新建引用指向白色 C(A.c = C)。

Plain 复制代码
【篡改前】          【篡改后】
   [ 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)

  • 回收范围:新生代、老年代、元空间,整堆全量回收。

  • 四大核心触发条件(必考)

    1. 老年代空间不足,对象晋升失败;

    2. 元空间/方法区加载类过多,内存耗尽;

    3. 代码主动调用 System.gc()(建议回收,大概率触发 Full GC);

    4. Minor GC 空间分配担保失败。

  • 核心特点:全局 STW,暂停所有业务线程,服务卡顿严重;线上调优核心目标:减少 Full GC 频次与耗时。

四、运行时数据区子系统

运行时数据区分为线程共享区 (全局共用)和线程私有区(线程隔离、随线程生死)两大模块。

4.1 线程共享内存区域

4.1.1 堆 Heap(GC 核心区域)

JVM 最大内存区域,所有对象实例、数组默认在堆内存分配,是 GC 主要管理和回收的区域。

对象完整内存结构
Plain 复制代码
[ 栈 ]                   [ 堆 (Heap) ]
 user ──(存储指针)──>  ┌──────────────────────────────┐
                      │ 1. 对象头 (Mark Word + Klass) │
                      ├──────────────────────────────┤
                      │ 2. 实例数据 (id, name, age)   │
                      ├──────────────────────────────┤
                      │ 3. 对齐填充 (补齐为8字节倍数)  │
                      └──────────────────────────────┘
对象创建完整流程
  1. 类加载检查:校验目标类是否已加载,未加载则优先执行类加载流程。

  2. 内存分配:堆内存规整采用「指针碰撞」,内存碎片多采用「空闲列表」。

  3. 并发安全保障(TLAB):为每个线程分配私有缓冲区,多线程创建对象互不抢占、避免并发冲突。

  4. 内存零值初始化:对象内存(不含对象头)默认赋 0、null,保证实例变量可直接访问。

  5. 设置对象头:写入哈希码、GC 分代年龄、锁状态、类指针等核心元数据。

  6. 构造方法初始化 :执行<init> 方法,按业务代码为属性赋值,完成对象创建。

堆内存分代结构
Plain 复制代码
┌────────────────────────────────────────────────────────────┐
│                       堆内存 (Heap)                        │
├─────────────────────────────────────┬──────────────────────┤
│            新生代 (Young)            │     老年代 (Old)     │
├──────────────────┬──────────┬───────┤                      │
│   Eden (伊甸园)   │ From (S0)│To (S1)│   长期存活的老对象   │
└──────────────────┴──────────┴───────┴──────────────────────┘
新生代(对象新手村)
  • Eden 区:绝大多数新对象的诞生、驻留区域。

  • Survivor S0/S1:存活对象中转站。

GC 流转规则:Eden 满触发 Minor GC,存活对象复制至 S0/S1,年龄+1;下次 GC 时,Eden + 上一轮 Survivor 存活对象转移至另一块 Survivor 区,来回流转、年龄递增。

老年代(对象养老院)

存储长期存活、生命周期稳定的对象,三种晋升机制:

  1. 高龄晋升 :对象年龄达到阈值(默认15,可通过 -XX:MaxTenuringThreshold 修改),晋升老年代。

  2. 大对象直接晋升:超大数组、长字符串等大对象,新生代无法容纳,直接分配至老年代,避免频繁复制损耗性能。

  3. 动态年龄判定: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 底层操作短板。

相关推荐
一个儒雅随和的男子1 小时前
MQTT常见的问题?
java
Mr.45671 小时前
Netty中实现设备消息串行处理:Semaphore + 线程池
java·后端
2601_961194021 小时前
考研资料电子版|下载|pdf
java·python·考研·eclipse·django·pdf·pygame
好家伙VCC2 小时前
Rust+Bioinfo:80ms极速SNP注释引擎
java·开发语言·算法·rust
ANnianStriver2 小时前
PetLumina-AI 驱动的宠物生活管理平台
java·生活·vue3·springboot·ai编程·宠物·全栈开发
好家伙VCC2 小时前
Delta Lake + Flink 实现近实时数据湖 Schema 演化
java·大数据·flink
hoho_122 小时前
如何替换jar包中依赖的其他jar
java·pycharm·jar
码语智行2 小时前
接口请求处理流程
java
布朗克1682 小时前
23 泛型——类型安全的参数化编程
java·泛型