双亲委派层级 自下而上
能
否
是
否
是
否
父
父
父
类加载请求
双亲委派模型
父加载器能否加载?
父加载器完成加载
当前加载器执行加载
验证:字节码合法性检查
准备:静态变量分配内存+默认值
解析:符号引用→直接引用
是否主动引用?
初始化:执行
类就绪(未初始化)
使用:实例化/方法调用
类可卸载?
卸载:Class对象回收
启动类加载器
扩展类加载器
应用类加载器
自定义加载器
类加载子系统是 JVM "加载代码" 的核心,内存结构(运行时数据区)是 JVM "运行代码" 的载体 ------ 前者负责把.class文件搬进 JVM,后者负责存放和执行,二者是 JVM 运行的 "左膀右臂"。
一、 类加载子系统
核心:把.class 文件变成 JVM 可执行的类
1. 核心职责
将外部.class二进制字节流加载到 JVM,经校验 / 转换 / 初始化后,生成Class对象作为访问入口,最终形成可执行的 Java 类型。
2. 类加载完整生命周期(7 阶段)
核心流程为:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载,其中前 5 阶段是类加载子系统的核心
| 阶段 | 核心工作 | 新手必记关键细节(易混点) |
|---|---|---|
| 加载 | 1. 按全限定名获取.class 字节流 2. 转成方法区运行时数据结构 3. 堆中生成Class对象(访问入口) |
✅ Class对象存在堆 ,类元数据存在方法区 / 元空间 ✅ 字节流来源:本地文件、jar、网络、动态代理等 |
| 验证 | 确保.class 文件符合 JVM 规范,无安全风险 | ✅ 4 层验证:文件格式(魔数 0xCAFEBABE)→ 元数据 → 字节码 → 符号引用 ✅ 核心目的:防止恶意字节码破坏 JVM |
| 准备 | 为静态变量分配内存 + 设置默认初始值 | ✅ 仅处理静态变量,实例变量在对象实例化时分配(堆) ✅ final 静态变量:直接赋值常量值(而非默认值) ✅ 示例:static int a=10 → 准备阶段 a=0,初始化阶段 a=10 |
| 解析 | 常量池符号引用 → 直接引用(内存地址 / 偏移量) | ✅ 符号引用:字符串描述(如 "java.lang.String") ✅ 直接引用:指向目标的实际内存地址 |
| 初始化 | 执行类初始化方法<clinit>() |
✅ <clinit>()由编译器生成,包含静态变量赋值 + 静态代码块 ✅ 执行顺序:父类<clinit>()先于子类 ✅ 触发场景(主动引用):new 实例、调静态方法 / 变量、反射、主类启动、子类初始化触发父类 |
| 使用 | 实例化对象、调用方法 / 字段 | - |
| 卸载 | 回收 Class 对象,释放类元数据 | ✅ 仅自定义类加载器加载的类可卸载,核心类(如 java.lang.String)不会卸载 |
📌 核心记忆口诀(类加载 5 核心阶段):
加(加载)验(验证)准(准备)解(解析)初(初始化),
堆存 Class 元存区,静态变量先默认,final 直接赋常量。
3. 类加载器与双亲委派模型
(1)类加载器分类(4 类)
| 类加载器 | 负责加载范围 | 实现方式 | 核心特征 |
|---|---|---|---|
| 启动类加载器(Bootstrap) | JAVA_HOME/lib 下核心类库(如 rt.jar) | C++ 实现(无 Java 对应类) | 最顶层,无法通过代码直接获取 |
| 扩展类加载器(Extension) | JAVA_HOME/lib/ext 下扩展类库 | Java 实现(继承 ClassLoader) | |
| 应用类加载器(Application) | classpath 下用户自定义类 / 第三方 jar | Java 实现(默认类加载器) | |
| 自定义类加载器 | 开发者自定义路径的类(如加密.class) | 继承 ClassLoader,重写 findClass () | 灵活控制加载逻辑 |
(2)双亲委派模型(核心:向上委派,向下加载)
核心规则(3 步)
- 收到加载请求 → 先委派给父加载器,不自己加载;
- 父加载器继续向上委派,直到启动类加载器;
- 父加载器加载失败 → 子加载器才自己尝试加载。
核心优势
✅ 避免类重复加载(同一个类只会被一个加载器加载);
✅ 防止核心类被篡改(如自定义java.lang.String无法加载,启动类加载器已加载核心版本)。
📌 双亲委派记忆口诀:
父上子下,先委后加;核心不篡,唯一不杂。
二、 JVM 内存结构(运行时数据区:类和对象的 "运行容器")
1. 核心划分(按线程归属)
tex
JVM运行时数据区
├─ 线程共享区(所有线程共用,易出OOM)
│ ├─ 堆(Heap):存对象实例,GC核心区域
│ └─ 方法区(Method Area):JDK8后为元空间,存类元数据
└─ 线程私有区(每个线程独立,随线程生灭)
├─ 程序计数器:记录字节码执行地址
├─ 虚拟机栈:服务Java方法,存栈帧
└─ 本地方法栈:服务native方法(C/C++实现)
2. 各区域详细说明
(1)线程私有区
| 区域 | 核心作用 | 关键特性 | 异常类型 |
|---|---|---|---|
| 程序计数器 | 记录当前线程执行的字节码指令地址 | ✅ 唯一不会 OOM 的区域 ✅ native 方法时值为 undefined | 无 |
| 虚拟机栈 | 描述 Java 方法执行,每个方法对应一个栈帧 | ✅ 栈深度由 - Xss 参数设置(默认 1M) ✅ 栈帧含:局部变量表 + 操作数栈 + 动态链接 + 返回地址 | StackOverflowError(栈太深)OutOfMemoryError(扩展失败) |
| 本地方法栈 | 服务 native 方法 | ✅ HotSpot 中与虚拟机栈合二为一 | 同虚拟机栈 |
📌 线程私有区记忆口诀:
计数器记地址,唯一无 OOM;
虚拟机栈存方法,栈帧进出控执行;
本地方法栈辅 native,异常同栈不特殊。
(2)线程共享区
① 堆(Heap):JVM 最大内存区域
核心作用
唯一目的是存放对象实例和数组,几乎所有对象都在这里分配内存。
关键特性
✅ 线程共享,GC(垃圾回收)的主要区域("GC 堆");
✅ 内存参数:-Xms(初始堆)、-Xmx(最大堆);
✅ 分代划分(GC 优化核心):
tex
堆
├─ 新生代(Young):存新对象,GC频率高
│ ├─ Eden区(伊甸园):新对象优先分配
│ └─ Survivor区(幸存者):From/To区,存Eden存活对象
└─ 老年代(Old):存新生代多次GC存活的对象,GC频率低
✅ 异常:OutOfMemoryError: Java heap space(堆内存不足)。
📌 堆记忆口诀:
堆存对象和数组,GC 主要忙这块;
新生伊甸加幸存,老年代里存长寿。
② 方法区(JDK8 后为元空间):存类的 "说明书"
核心作用
存储类元数据(版本、字段、方法)、运行时常量池、静态变量、即时编译代码缓存。
JDK7 vs JDK8 核心变化(易考点)
| 版本 | 实现方式 | 内存来源 | 限制参数 | 异常类型 |
|---|---|---|---|---|
| JDK7 及之前 | 永久代(PermGen) | JVM 堆内存 | -XX:MaxPermSize | OutOfMemoryError: PermGen space |
| JDK8 及之后 | 元空间(Metaspace) | 本地系统内存 | -XX:MaxMetaspaceSize | OutOfMemoryError: Metaspace |
📌 方法区记忆口诀:
方法区存元数据,JDK8 前叫永久;
永久代在堆里限,元空间本地无上限。
三、 类加载子系统与内存结构的关联(核心逻辑)
类加载子系统加载.class
类元数据→方法区/元空间
Class对象→堆
准备阶段
静态变量→方法区/元空间(分配内存+默认值)
初始化阶段
静态变量赋值→方法区/元空间
实例化对象
对象实例→堆
对象引用→虚拟机栈局部变量表
执行方法
创建栈帧→虚拟机栈
核心关联点
- 类加载的最终产物:类元数据存方法区 / 元空间 ,
Class对象存堆; - 静态变量:准备阶段在方法区 / 元空间分配内存,初始化阶段完成赋值;
- 对象实例:分配在堆 中,对象引用存储在虚拟机栈的局部变量表;
- 方法执行:虚拟机栈中创建栈帧,局部变量、操作数等都存于栈帧内。
总结
- 类加载核心:加验准解初5 阶段,双亲委派 "向上委派、向下加载",核心类不被篡改;
- 内存结构核心:线程私有区(计数器无 OOM、虚拟机栈存方法),线程共享区(堆存对象、方法区存元数据);
- 版本差异核心:JDK8 移除永久代,方法区变为元空间(使用本地内存);
- 关联核心:类元数据存方法区,对象存堆,方法执行依赖虚拟机栈。
JVM 类加载子系统 & 内存结构 核心记忆口诀
一、 类加载子系统 记忆口诀
1. 类加载五阶段(核心流程)
口诀:加验准解初,五步按序走
解释
- 加:加载(获取字节码,生成 Class 对象)
- 验:验证(校验字节码合法性,防恶意篡改)
- 准:准备(类变量赋默认值,分配内存)
- 解:解析(符号引用转直接引用)
- 初:初始化(执行
<clinit>(),赋程序员指定值)
2. 双亲委派模型(核心规则)
口诀:父先子后找,安全防重复
解释
- 父先子后:类加载请求先委托父加载器,父加载失败子才加载
- 安全防重复:避免核心类(如
java.lang.String)被篡改,防止类重复加载 - 层级补充:启动→扩展→应用→自定义(自顶向下委托)
3. 类初始化触发场景(主动使用)
口诀:new 调静,反射用,子类初,主类跑,枚举动
解释
- new:new 关键字创建对象
- 调静:调用类的静态方法 / 非 final 静态变量
- 反射用:通过反射操作类的属性 / 方法
- 子类初:初始化子类时,父类先初始化
- 主类跑:执行 main 方法的程序入口类
- 枚举动:JDK8 + 枚举类 / 动态语言支持(MethodHandle)
4. 打破双亲委派典型场景
口诀:Tomcat 隔离,JDBC 驱动,热部署替换
解释
- Tomcat 隔离:不同 Web 应用加载同名类,需反向委派
- JDBC 驱动:启动类加载器无法加载,靠线程上下文类加载器
- 热部署替换:自定义类加载器实现类的重新加载
二、 内存结构 记忆口诀
1. JDK8 运行时数据区(区域划分)
口诀:私有三区域,共享堆与元,无一 OOM 除计数
解释
- 私有三区域:程序计数器、虚拟机栈、本地方法栈(线程私有)
- 共享堆与元:堆(存对象)、元空间(存类元信息,替代永久代)
- 无一 OOM 除计数:只有程序计数器不会 OOM,其余区域均可能出现 OOM
2. 堆内存分代模型(核心设计)
口诀:堆分新老代,新生 811,Minor 勤 Major 稀
解释
- 新老代:新生代(存新生对象)+ 老年代(存存活久的对象)
- 新生 811:新生代 = Eden(8): Survivor0(1): Survivor1(1)
- Minor 勤 Major 稀:新生代 GC(Minor GC)频率高,老年代 GC(Major GC)频率低
3. 永久代→元空间(核心变化)
口诀:永久代退场,元空间登场;本地内存用,大小可扩容
解释
- 永久代退场:JDK8 移除永久代,解决固定大小易 OOM 问题
- 元空间登场:存储类元信息,直接使用本地内存
- 大小可扩容:通过参数
-XX:MaxMetaspaceSize限制,默认无上限
4. 虚拟机栈栈帧组成
口诀:局变操作数,动态链接址,附加信息补
解释
- 局变:局部变量表(存方法参数、局部变量)
- 操作数:操作数栈(字节码指令执行的临时数据区)
- 动态链接址:动态链接(指向常量池的方法引用)+ 方法返回地址
- 附加信息补:虚拟机实现相关的附加信息
三、 综合记忆口诀
类加载五步走,双亲委派父先求;
内存五区分私共,堆分代来 GC 优;
永久退场元空间,OOM 排查按区究。