【JVM深入】一、基本内容

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 完整流程

  1. 【初始标记】

    暂停所有应用线程(极短时间)。

    扫描 当前所有的 GC Roots(如栈变量、静态字段等)。

    把这些根直接引用的对象标记为存活。

  2. 【并发标记开始 + 快照建立】

    恢复应用线程,同时 GC 线程开始并发遍历对象图。

    从这一刻起,"快照"生效:所有在快照建立前已存在的对象,如果在本轮被断开引用,不能立刻当垃圾。

  3. 【写屏障保护历史引用】

    应用线程在修改引用时(比如 obj.ref = null),JVM 自动插入写屏障。

    写屏障会把 "被覆盖的旧引用"(比如原来指向 B)存入 SATB 缓冲区。

    这样即使 B 后来没人引用了,GC 也会把它当作"可能存活"的对象重新扫描一遍。

  4. 【标记结束 + 清理】

    所有标记完成(包括处理完 SATB 缓冲区)。

    没有被标记的对象 = 垃圾 → 被回收。

    SATB 缓冲区被丢弃,本轮快照生命周期结束。

  5. 【关键后果】

    如果一个对象(如 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
相关推荐
vKd0Ff21L6 小时前
如何在Dev-C++中设置TDM-GCC为默认编译器第九十一篇
java·jvm·c++
Dicky-_-zhang9 小时前
Elasticsearch聚合查询优化实战
java·jvm
AI人工智能+电脑小能手9 小时前
【大白话说Java面试题 第64题】【JVM篇】第24题:强引用、软引用、弱引用、虚引用分别是什么?
java·开发语言·jvm·面试
一生了无挂10 小时前
深入解析JVM、JRE与JDK:Java技术体系的核心基石
java·开发语言·jvm
hef28810 小时前
C语言循环语句详解:实现1到10的打印输出
jvm
Dicky-_-zhang10 小时前
云原生数据库实战:TiDB与CockroachDB对比选型与落地实践
java·jvm
一直不明飞行20 小时前
Java的equals(),hashCode()应该在什么时候重写
java·开发语言·jvm
2301_8039346120 小时前
Go语言如何做网络爬虫_Go语言爬虫开发教程【指南】
jvm·数据库·python
2301_803934611 天前
MySQL 字段类型选择规范指南
jvm·数据库·python