1、双亲委派
1.1 什么是双亲委派
- 其实就是遇到一个类,该由谁加载它的问题
- 当一个类加载器遇到加载一个类的时候,首先交由自己的父级类加载器处理,如果直到最顶级类加载器都无法处理,再交由下级加载器逐级处理
- Bootstrap ClassLoader (启动类加载器)负责加载最核心的java.lang.*类,就是jre/lib下面的jar
- Extension ClassLoader(扩展类加载器)负责加载扩展类,就是jre/lib/ext的jar
- Application ClassLoader(应用类加载器)负责加载用户类路径上的类
- Custom ClassLoader(自定义类加载器)开发者可继承Classload实现自己的加载逻辑
1.2 Application和Custom区别
| 需求 | Application ClassLoader 能否满足? | 是否需要 Custom ClassLoader? |
|---|---|---|
| 加载本地 classpath 下的类 | ✅ 能 | ❌ 不需要 |
| 从网络/数据库加载类 | ❌ 不能 | ✅ 需要 |
| 同一类多版本共存 | ❌ 不能(会冲突) | ✅ 需要 |
| 动态生成类并加载 | ❌ 不能 | ✅ 需要 |
| 实现插件化/模块化 | ❌ 不能(无隔离) | ✅ 需要 |
| 热部署 | ❌ 不能 | ✅ 需要 |
1.3 定义一个新的核心类会有问题吗
- 在自己的 classpath 中定义一个"核心类"(比如 java.lang.String、java.lang.Object 等)是无效的,而且通常不会生效
- 你的代码由 Application ClassLoader 加载;
它收到加载 java.lang.String 的请求;
但它不会自己加载,而是先委托给父加载器(Extension → Bootstrap);
Bootstrap ClassLoader 发现:java.lang.String 在它的管辖范围内(JAVA_HOME/jre/lib 或模块路径);
于是 直接加载官方的 String 类,根本不会看你的 classpath! - 用自定义类加载器,会被 SecurityManager 或 JVM 本身拦截
2、JVM调优
2.1 调优目标
- 高吞吐,单位时间内处理更多任务(请求,计算),后台批处理,大数据分析
- 低延迟,减少GC停顿时间,让响应更快,电商交易,支付系统,实时接口
- 低内存占用,节省内存,避免OOM,容器化部署,资源受限环境
2.2 调优手段
- 调整堆内存大小
- 调整新生代/老年代的占用比例
- 切换垃圾回收器
- 调整元空间大小
| GC 类型 | 特点 | 适用场景 |
|---|---|---|
| Serial GC | 单线程,简单 | 小型应用、客户端程序 |
| Parallel GC(默认) | 多线程,高吞吐 | 后台计算、批处理 |
| CMS(已废弃) | 并发回收,低延迟 | 老版本低延迟需求 |
| G1 GC | 分区域回收,可预测停顿 | 大堆(4GB+)、低延迟要求 |
| ZGC / Shenandoah | 超低停顿(<10ms),并发 | 超高实时性要求(Java 11+) |
2.3 新生代和老年代的区别
| 区域 | 存放对象 | 生命周期 | GC 频率 | GC 算法 |
|---|---|---|---|---|
| 新生代 (Young Generation) | 新创建的对象 | 极短(几毫秒到几秒) | 非常高频 | 复制算法 (Copying) |
| 老年代 (Old Generation) | 长寿的对象 | 很长(几分钟到几小时甚至永久) | 非常低频 | 标记-清除/整理 (Mark-Sweep/Compact) |
| 调整目标 | 本质目的 |
|---|---|
| 增大新生代 | 减少 Minor GC 频率,提升吞吐量 |
| 减小新生代 | 缩短单次 Minor GC 停顿时间(适用于超低延迟场景) |
| 增大老年代 | 避免因老年代空间不足而频繁 Full GC |
| 合理设置比例 | 让"朝生暮死"的对象尽量在新生代就被回收掉,不要污染老年代! |
2.4 复制算法
- GC的复制算法,就是Eden 区满了之后,触发Minor GC,此时会从Eden和Survivor(0/1)找到存活的对象,复制到另一个Survivor区,再清除Eden和原始Survivor区
- 在第一次 Minor GC 之后,如果 Eden 中的对象还活着,就会被复制到 Survivor 区
- 存活对象判定方法,从GC Roots处开始查询,根搜索算法,GC Roots 是 JVM 认为"绝对不能回收"的对象。所有能从 GC Roots 直接或间接访问到的对象,都是存活的;反之,就是垃圾。
2.4.1 GC Roots
2.4.1.1 虚拟机栈帧
- 每个 Java 线程都有自己的 虚拟机栈。
- 每调用一个方法,就会创建一个栈帧(Stack Frame)
- 栈帧里有局部变量表,存放方法参数,局部变量等
- 这些局部变量如果引用了堆中的对象,那么这些对象就是GC Roots的直接子节点,不会被回收
java
public class GcRootsExample {
public void method() {
// obj 是局部变量,存储在当前线程的栈帧中
byte[] obj = new byte[1024 * 1024]; // 1MB 对象,分配在堆上
// 此时,obj 引用了堆中的 byte[] 对象
// 这个 byte[] 对象 → 被 GC Root(局部变量 obj)引用 → 存活!
System.out.println("方法执行中...");
// 方法结束前,obj 一直存在,对象不会被回收
}
public static void main(String[] args) {
GcRootsExample example = new GcRootsExample();
example.method(); // 调用 method
// method 执行完毕后,其栈帧被销毁,obj 变量消失
// 此时,byte[] 对象失去所有引用 → 成为垃圾 → 下次 GC 被回收
}
}
2.4.1.2 方法区中类静态属性引用的对象
- 静态变量存储在方法区(Metaspace)
- 属于类本身,而不是某个实例
- 静态变量的生命周期=类的生命周期≈应用的生命周期
- 所以被视作GC Roots
java
public class StaticGcRoot {
// 静态变量,存储在方法区
public static List<String> cache = new ArrayList<>();
public static void main(String[] args) {
// 向静态列表添加对象
cache.add("Hello");
cache.add(new String("World"));
// 即使 main 方法结束,cache 依然存在(因为是 static)
// 所以 "Hello" 和 "World" 字符串对象 → 被 GC Root(静态变量 cache)引用 → 不会被回收!
System.out.println("程序运行中...");
// 程序不退出,这些对象就一直存活
}
}
2.4.1.3 方法区中常量引用的对象(如 static final String)
- 字符串常量池(String Pool)位于方法区
- 常量池的引用也是GC Roots
java
public class ConstantGcRoot {
// 编译期常量,存入方法区常量池
public static final String CONSTANT = "I am a constant";
public static void main(String[] args) {
String s1 = "I am a constant"; // 从常量池获取
String s2 = CONSTANT; // 也是同一个对象
// 字符串 "I am a constant" 被常量池引用 → 是 GC Root 的一部分 → 不会被回收!
System.out.println(s1 == s2); // true,同一个对象
}
}
2.4.1.4 本地方法栈中JNI(Native方法)引用的对象
- 当java调用本地方法native,会使用本地方法栈
- 在native代码中,可以通过JNI持有对java对象的引用
- 这些被native代码引用的java对象,也是GC Roots,不能被回收,否则native代码会访问到野指针
java
public class JNIGcRoot {
// 声明一个 native 方法
public native void processObject(byte[] data);
static {
System.loadLibrary("mylib"); // 加载 native 库
}
public static void main(String[] args) {
byte[] bigData = new byte[10 * 1024 * 1024]; // 10MB 数据
JNIGcRoot obj = new JNIGcRoot();
obj.processObject(bigData); // 传递给 native 方法
// 在 native 代码执行期间,
// bigData 被 JNI 引用 → 成为 GC Root 的一部分 → 即使 Java 代码不再引用它,也不会被回收!
// 只有当 native 方法返回后,JNI 引用解除,bigData 才可能被回收
}
}
| GC Root 类型 | 存储位置 | 生命周期 | 代码特征 | 是否可被回收 |
|---|---|---|---|---|
| 局部变量/参数 | 虚拟机栈 | 方法执行期间 | 方法内的变量 | 方法结束即消失 |
| 静态变量 | 方法区 | 类加载到卸载 | static 修饰 |
程序结束才消失 |
| 常量 | 方法区(常量池) | 类加载到卸载 | static final 字面量 |
几乎永不回收 |
| JNI 引用 | 本地方法栈 | native 方法执行期间 | native 方法参数 |
native 返回后可回收 |
2.5 标记整理/标记清除
- 标记其实都是从GC Roots出发,看哪些对象还存活,打上标记,
- 清除,就是没有标记的就全部清除,问题会产生大量的碎片,虽然总空闲内存足够,但没有一块连续的大空间分配给新对象(比如一个大数组)。会导致OOM
- 整理,就是同时清理和整理,将存活的对象统一向左移动,一次性清理右边的所有垃圾
2.6 G1的局部整理
- 不再区分新生代/老年代的物理边界,而是将整个堆划分为2048个左右的Region(默认1~32M)
- 每个Region 可以是Eden/Survivor/Old
- 局部整理,就是每次选取一部分Region进行整理,类似于复制算法,复制到新的Region,并清理原始数据
- 每次只选一部分 Region 进行整理(复制存活对象),而不是全堆整理!
2.7 ZGC的并发整理
- 利用着色指针操作,表明该对象是否已经重定向
- 读屏障,在实际获取对象时,读屏障检查指针颜色,如果已经移动,则自动将引用更新为新地址,应用线程永远看到的是最新的地址
2.7.1 完整流程
-
【初始标记】
暂停所有应用线程(极短时间)。
扫描 当前所有的 GC Roots(如栈变量、静态字段等)。
把这些根直接引用的对象标记为存活。
-
【并发标记开始 + 快照建立】
恢复应用线程,同时 GC 线程开始并发遍历对象图。
从这一刻起,"快照"生效:所有在快照建立前已存在的对象,如果在本轮被断开引用,不能立刻当垃圾。
-
【写屏障保护历史引用】
应用线程在修改引用时(比如 obj.ref = null),JVM 自动插入写屏障。
写屏障会把 "被覆盖的旧引用"(比如原来指向 B)存入 SATB 缓冲区。
这样即使 B 后来没人引用了,GC 也会把它当作"可能存活"的对象重新扫描一遍。
-
【标记结束 + 清理】
所有标记完成(包括处理完 SATB 缓冲区)。
没有被标记的对象 = 垃圾 → 被回收。
SATB 缓冲区被丢弃,本轮快照生命周期结束。
-
【关键后果】
如果一个对象(如 B)在本轮 GC 中被断开引用但被写屏障捕获,它会被保守地当作存活。
即使它实际上已经不可达,也不会在本轮被回收。
但它会在下一轮 GC 中被正常识别为垃圾并回收(因为那时没人引用它,也不会再进 SATB)。
2.7.2 人话版流程
- ZGC 每轮 GC 都说:"我相信快照开始时存在的对象都是好人;如果你在调查期间偷偷放走了一个'嫌疑人',我得先把他关起来审一审------哪怕他其实是清白的(垃圾)。等下一轮再放。"
3、JVM运行时区域
- 程序计数器,线程私有 ,记录当前线程正在执行的 字节码指令地址(行号指示器)
- java虚拟机栈,每个线程都是一个虚拟机栈,每当调用一个 Java 方法,就会在该栈中压入一个栈帧(Stack Frame)
- 本地方法栈,本地方法栈用于支持 native 方法(如 JNI 调用 C/C++ 代码)的执行,结构与 Java 虚拟机栈类似
- 堆,堆是 JVM 中最大的一块内存区域,几乎所有对象实例和数组都在这里分配
- 方法区,方法区存储类的元数据,包括:类结构信息、静态变量、运行时常量池(含字符串常量)等
- 直接内存,NIO使用的native内存
| 区域 | 线程私有/共享 | 作用 | 是否受 GC 管理 | 常见异常 |
|---|---|---|---|---|
| 程序计数器 | 私有 | 记录字节码行号 | 否 | 无 |
| Java 虚拟机栈 | 私有 | 存储方法栈帧(局部变量等) | 否(但栈帧随方法结束自动释放) | StackOverflowError, OOM |
| 本地方法栈 | 私有 | 支持 native 方法调用 | 否 | 同上 |
| 堆 | 共享 | 存放对象实例、数组 | ✅ 是(主要区域) | Heap OOM |
| 方法区 | 共享 | 存储类元数据、静态变量、常量池 | ✅(部分,如常量可回收) | Metaspace OOM(JDK8+) |
| 直接内存 | --- | NIO 等使用的 native 内存 | 否(但可被 Cleaner 回收) | 系统 OOM |