安派尔

JVM学习杂记

2022/10/13
2
0

JVM学习杂记

概述

JVM(Java Virtual Machine)是 Java“一次编写,到处运行” 的核心,其底层本质是将 Java 字节码(.class 文件)转换为操作系统能识别的机器码,并提供内存管理、线程调度等核心能力。

JVM的核心组成

JVM 的底层架构可以分为 5 个核心模块,它们协同工作完成 Java 程序的执行,整体架构如下:

architecture.png

1. 类加载子系统(Class Loader Subsystem)

底层作用:将磁盘上的.class文件(字节码)加载到 JVM 的内存中,并完成验证、准备、解析,最终生成可以被执行的 Class 对象。

  • 加载:通过类的全限定名(如java.lang.String)找到.class文件,读取字节码到内存;
  • 链接:
    • 验证:检查字节码是否符合 JVM 规范(防止恶意字节码);
    • 准备:为类的静态变量分配内存并设置默认值(如int a默认值是 0,而非代码中赋值的 10);
    • 解析:将符号引用(如代码中的Object obj)转换为直接引用(内存地址);
  • 初始化:执行静态代码块、为静态变量赋实际值(如a = 10)。

2. 运行时数据区(Runtime Data Area)

这是 JVM 底层最核心的内存管理区域,所有运行时的数据都存在这里,它被划分为 5 个区域,每个区域有明确的底层职责:

区域名称底层作用线程是否私有核心特点
程序计数器记录当前线程执行的字节码行号,线程切换时恢复执行位置唯一不会 OOM 的区域
虚拟机栈存储每个方法的栈帧(局部变量、方法参数、返回地址等),方法调用 = 入栈,执行完 = 出栈栈深度过大会抛StackOverflowError
本地方法栈作用和虚拟机栈一致,但服务于本地方法(如native修饰的方法)同样会抛 StackOverflowError
堆(Heap)存储所有对象实例和数组,是 GC(垃圾回收)的核心区域否(共享)内存不足会抛OutOfMemoryError(OOM)
方法区(元空间)存储类的元数据(类名、方法名、字段、常量池等),JDK8 后替换为 “元空间”(直接使用本地内存)否(共享)元数据过多会抛 OOM

3. 执行引擎(Execution Engine)

底层作用:将加载到内存的字节码转换为 CPU 能执行的机器码,是 JVM “执行代码” 的核心。它有两种执行方式:

  • 解释器:逐行解释字节码为机器码并执行,优点是启动快,缺点是执行慢(适合短时间运行的程序);
  • JIT 编译器(即时编译器):识别频繁执行的 “热点代码”(如循环、高频调用的方法),一次性编译为机器码缓存,后续直接执行缓存的机器码,优点是执行快,缺点是编译需要时间(适合长时间运行的程序)。

底层逻辑:JVM 默认是 “解释器 + JIT” 混合模式 —— 程序启动时用解释器快速执行,运行中 JIT 编译热点代码,兼顾启动速度和执行效率。

4. 本地方法接口(JNI)+ 本地方法库

底层作用:JVM 是用 C/C++ 实现的,当 Java 代码调用native方法(如System.currentTimeMillis())时,JNI 作为 “桥梁”,让 Java 代码能调用底层操作系统的 C/C++ 函数(本地方法库),实现和硬件 / 操作系统的交互。

5. 垃圾回收器(GC)

底层作用:自动回收堆中不再被引用的对象内存(避免内存泄漏),是 JVM “内存管家”。

  • 核心算法:标记 - 清除、标记 - 复制、标记 - 整理(不同 GC 器采用不同组合);
  • 常见 GC 器:Serial GC(单线程)、Parallel GC(多线程)、G1 GC(分区回收)、ZGC/Shenandoah(低延迟);
  • 底层流程:暂停业务线程(STW)→ 标记存活对象 → 回收死亡对象 → 整理内存(可选)→ 恢复业务线程。

类加载子系统

核心工作流程

类加载子系统的工作分为 3 个核心阶段:加载(Loading)→ 链接(Linking)→ 初始化(Initialization),这三个阶段按顺序执行(解析阶段可能在初始化后执行,为了支持动态绑定)。

第一阶段:加载——“找文件,读内存”

类加载的的第一步,核心是“找到.class文件并加载到内存”。

  • 定位.class文件:通过类的全限定名(如java.lang.String,com.example.user),类加载器会从不同的位置查找字节码文件:
    • 系统类(如String):从JDK中的rt.jar中查找;
    • 应用类:从项目的classpath中查找;
    • 第三方类:从maven依赖的jar中查找
  • 读取字节码:将找到的.class文件的字节流读取到内存中;
  • 生成Class对象:在内存中创建该类的java.lang.Class对象,这个对象是后续访问类元数据的唯一入口。

第二阶段:链接(Linking)——“校验+准备+解析”

链接是类加载的核心校验和准备阶段,分为 3 个子步骤,确保加载的类符合 JVM 规范且能正常运行:

​ 1、验证(Verification)——“安检”

核心是检查字节码是否合法、安全,避免恶意或错误的字节码导致JVM崩溃,主要做4类校验:

  • 文件格式校验:检查.class文件的魔数(Magic Number,固定是0xCAFEBABE,如下图)、版本号(如JDK8编译的类不能在JDK7的JVM中运行)是否合法。

  • 元数据校验:检查类的继承关系、字段 / 方法的语法是否符合 Java 规范(比如不能继承final类,不能重写final方法);

  • 字节码校验:检查字节码指令的逻辑是否合法(比如不能跳转到不存在的行号,不能操作非法类型的变量);

  • 符号引用校验:检查类引用的其他类 / 方法是否存在(比如User类引用了Order类,要确认Order类能被找到)。

    编译与执行版本错误示例

关键作用:如果验证失败,会抛出VerifyError,比如用修改工具篡改.class文件的魔数,JVM 在验证阶段就会报错。

​ 2、准备(Preparation)——“分配内存,设默认值”

核心是为类的静态变量(类变量) 分配内存(存储在方法区),并设置默认初始值(不是代码中赋值的实际值)。

数据类型默认初始值代码中赋值的实际值
int0比如static int a = 10;中的 10
booleanfalse比如static boolean flag = true;中的 true
对象引用null比如static User u = new User();中的 new User ()

注意

  • 只处理静态变量,实例变量(非 static)的内存分配在创建对象时(堆中),不在这个阶段;
  • 静态常量(static final)是特例:准备阶段会直接赋值为代码中的常量值(比如static final int b = 20;,准备阶段 b 就会被赋值为 20,而非 0)。

​ 3、解析(Resolution)——“符号引用→直接引用”

核心是将常量池中的符号引用(比如代码中写的User u = new User();里的User)转换为直接引用(内存地址,比如方法区中User.class对象的内存地址)。

  • 符号引用:用字符串描述的引用(比如 “com.example.User”),不依赖具体内存地址;
  • 直接引用:指向内存中具体对象的指针 / 偏移量,是 JVM 能直接访问的地址。

关键时机:解析阶段不一定在准备后立即执行,JVM 支持 “懒解析”—— 只有在实际使用该引用时才解析(比如调用User类的方法时,才解析User的符号引用),目的是提升类加载效率。

第三阶段:初始化(Initialization)——“执行赋值和静态代码块”

这是类加载的最后一步,核心是执行类的静态代码块(static {}),并为静态变量赋代码中定义的实际值

执行顺序:

  1. 先执行父类的初始化(如果父类未初始化);
  2. 再执行当前类的静态变量赋值语句;
  3. 最后执行当前类的静态代码块(static {})。
public class Parent {
    static int parentAge = 50;
    static {
        System.out.println("Parent静态代码块执行");
    }
}

public class Child extends Parent {
    static int childAge = 20;
    static {
        System.out.println("Child静态代码块执行");
    }
}

// 测试代码
public class Test {
    public static void main(String[] args) {
        System.out.println(Child.childAge);
    }
}

执行结果:

Parent静态代码块执行
Child静态代码块执行
20

过程逻辑:访问Child.childAge触发Child类的初始化,JVM 先初始化父类Parent(执行parentAge=50+ 父类静态代码块),再初始化Child(执行childAge=20+ 子类静态代码块)。

触发初始化的场景(主动使用):

只有以下 6 种 “主动使用” 场景会触发类的初始化,其他情况(比如仅引用静态常量)不会触发:

  1. 创建类的实例(new Child());
  2. 访问类的静态变量(非 final);
  3. 调用类的静态方法;
  4. 反射调用(Class.forName("com.example.Child"));
  5. 初始化子类时,父类会先初始化;
  6. 启动类(包含main方法的类)会被优先初始化。

类加载子系统的核心机制:类加载器与双亲委派模型

类加载子系统的核心是 “类加载器”,它负责执行上述的加载阶段,JVM 内置了 3 类加载器,且遵循 “双亲委派模型”,这是类加载的核心规则。

1. 类加载器的分类

JVM 的类加载器分为 4 类(3 个内置 + 1 个自定义),各自负责加载不同范围的类:

类加载器类型核心作用实现方式加载路径示例
启动类加载器(Bootstrap)加载 JVM 核心类(java.lang.*java.util.*等)C/C++ 实现(无 Java 对象)JDK 安装目录 /jre/lib/rt.jar
扩展类加载器(Extension)加载 JVM 扩展类Java 实现(ExtClassLoaderJDK 安装目录 /jre/lib/ext/*.jar
应用程序类加载器(Application)加载应用程序的类(classpath 下的类)Java 实现(AppClassLoader项目 target/classes、Maven 依赖的 jar 包
自定义类加载器加载自定义路径的类(比如加载磁盘外的类)继承ClassLoader实现比如加载网络上的.class 文件、加密的.class 文件

2.双亲委派模型 ——“先找父,再找自己”

这是类加载器的核心工作规则,目的是保证核心类的唯一性和安全性(比如避免自定义的java.lang.String覆盖 JDK 的核心String类)。

​ 1.当一个类加载器收到加载请求时,首先委托给父加载器加载,而非自己直接加载;

​ 2.父加载器收到请求后,继续委托给它的父加载器,直到启动类加载器;

​ 3.启动类加载器检查是否能加载该类:能加载则直接加载,不能则向下返回给扩展类加载器;

​ 4.扩展类加载器检查:能加载则加载,不能则向下返回给应用程序类加载器;

​ 5.应用程序类加载器检查:能加载则加载,不能则返回给自定义类加载器;

​ 6.自定义类加载器仍无法加载,则抛出ClassNotFoundException

核心作用:

  • 安全:避免自定义的核心类(如java.lang.String)替换 JDK 的核心类,因为启动类加载器会优先加载rt.jar中的String类,自定义的String类永远不会被加载;
  • 避免重复加载:同一个类只会被一个类加载器加载,保证内存中只有一个Class对象。

执行引擎

执行引擎是 JVM 的核心执行组件,也是连接 “字节码” 和 “硬件 CPU” 的桥梁,其核心作用是:将类加载子系统加载到方法区的字节码指令,转换为 CPU 能识别的机器码并执行,同时协调运行时数据区(栈帧、堆、程序计数器)完成方法调用、变量操作等逻辑。

执行引擎的核心组成

组件名称核心职责
解释器(interpreter)逐行将字节码指令解释为机器码并执行,是执行引擎的“基础执行器”
JIT编译器识别“热点代码”,一次性将其编译为优化后的机器码并缓存,后续直接执行缓存
热点代码检测器(Profiler)实时监控字节码的执行频率,判断哪些代码是“热点代码”,触发JIT编译
栈帧管理器处理方法调用的栈帧入栈/出栈,管理局部变量表、操作数栈等栈帧内容
本地方法调用器调用JNI接口,衔接本地方法库,执行native方法

其中,解释器和JIT编译器是执行引擎的核心,JVM默认采用“解释器+JIT”的混合执行模式,兼顾程序启动速度和运行效率。

执行引擎的的3种执行方式

JVM支持解释执行、编译执行、混合执行三种方式,其中混合执行是默认且最优的方式。

1、解释执行

  • 工作原理

    解释器以逐行解释、逐行执行的方式处理字节码:每次读取一条字节码指令,将其转换为对应平台的机器码,交给CPU执行,执行完再读取下一条,全程无缓存

    JVM内置的解释器是模板解释器,为每一条字节码指令预先编写好对应的机器码模板,解释时直接复用模板,提升解释效率

  • 优缺点

    • 优点:启动速度极快,无需编译时间,适合短时间运行的程序
    • 缺点:执行效率低,相同字节码被多次执行时,会重复解释,产生大量冗余操作。

2、编译执行

  • 工作原理

    由JIT编译器将整段字节码一次性编译为优化后的机器码,并存入内存缓存;后续执行该段代码时,直接调用缓存中的机器码,无需再次解释/编译。

  • 核心前提:识别“热点代码”

    JIT编译器不会对所有代码都编译,只会编译热点代码(即频繁执行的代码),热点代码的判断由热点检测器完成,判断依据有两种:

    • 方法调用热点:某个方法被调用的次数达到阈值(默认一万次,通过-xx:CompileThreshold调整);
    • 循环执行热点:方法内的循环体执行次数达到阈值(循环回边计数器,默认一万次)。
  • JIT编译器的分级

    HotSpot VM 提供了C1 编译器(客户端编译器)C2 编译器(服务端编译器),JDK8 及以后默认开启分层编译,结合两者的优势:

    • C1 编译器:编译速度快,优化程度低,适合客户端程序(如桌面应用),优先保证启动速度;
    • C2 编译器:编译速度慢,优化程度高(深度优化),适合服务端程序(如微服务),优先保证运行效率;
    • 分层编译:代码先被 C1 编译为 “一级优化机器码”,若后续执行频率继续升高,再由 C2 重新编译为 “二级深度优化机器码”。

    优缺点

    • 优点执行效率极高,编译后的机器码经过深度优化,且缓存后可重复使用;
    • 缺点启动速度慢,编译需要时间,若程序运行时间短,编译开销甚至超过执行收益。

3、混合执行(Mixed Mode)——JVM默认方式

  • 工作原理

    结合解释执行和编译执行的优势:

    1.程序启动阶段:用解释器逐行执行字节码,快速启动程序,避免编译等待;

    2.程序运行阶段:热点检测器实时监控,将频繁执行的 “热点代码” 交给 JIT 编译器编译为机器码并缓存;

    3.后续执行热点代码时,直接调用 JIT 缓存的机器码;执行非热点代码时,仍用解释器逐行执行。

  • 核心优势

    这是 JVM 的默认执行模式(可通过-Xint强制解释执行,-Xcomp强制编译执行),完美解决了 “启动速度” 和 “执行效率” 的矛盾,也是 Java 程序既能快速启动,又能在长期运行中保持高性能的关键。

执行引擎的底层执行流程

public class ExecutionEngineTest {
    public static void main(String[] args) {
        int result = add(1, 2); // 调用add方法
        System.out.println(result);
    }

    public static int add(int a, int b) {
        return a + b; // 核心字节码:iadd
    }
}

底层执行步骤

  1. 字节码准备:类加载子系统将ExecutionEngineTest.class加载到方法区,生成Class对象,其中add方法的字节码(iconst_1iconst_2iaddireturn)存储在方法区的方法元数据中。

  2. 解释执行启动:执行引擎调用main方法,栈帧管理器为main方法创建栈帧并推入虚拟机栈;解释器从方法区读取main方法的字节码,逐行解释执行。

  3. 调用add方法:解释器执行到add(1,2)时,栈帧管理器为add方法创建新栈帧(包含局部变量表a=1b=2,操作数栈),推入虚拟机栈;程序计数器记录add方法的字节码执行位置。

  4. 解释执行add方法:解释器读取add方法的字节码:

    • iconst_1/iconst_2:将 1、2 压入操作数栈;

    • iadd:从操作数栈弹出 1 和 2,执行加法运算,将结果 3 压回操作数栈;

    • ireturn:将结果 3 返回给main方法的栈帧,add方法的栈帧出栈。

  5. 热点检测与 JIT 编译:若add方法被频繁调用(达到热点阈值),热点检测器会标记其为 “热点代码”,并通知 JIT 编译器。

  6. JIT 编译与缓存:JIT 编译器(C1/C2)从方法区读取add方法的字节码,进行深度优化(如方法内联),编译为 CPU 可直接执行的机器码,并存入内存的 “代码缓存” 中。

  7. 编译执行:后续再次调用add方法时,执行引擎不再调用解释器,而是直接从代码缓存中读取编译后的机器码执行,执行效率大幅提升。

  8. 程序结束main方法执行完毕,栈帧出栈;执行引擎通知 JVM 回收资源,程序退出。

JIT编译器的核心优化技术

JIT 编译器的执行效率优势,核心来自对字节码的深度优化,重点有一下4种方式:

  1. 方法内联(Method Inlining):将被调用的小方法的字节码,直接嵌入到调用方的方法中,消除 “方法调用的栈帧创建 / 销毁开销”,同时为后续优化创造条件。

    例子:

    public static int add(int a, int b) { return a + b; }
    public static void main(String[] args) {
        int sum = add(1,2) + add(3,4);
    }
    

    优化后:

    public static void main(String[] args) {
        int sum = (1+2) + (3+4); // 直接消除add方法的调用开销
    }
    
  2. 逃逸分析(Escape Analysis):分析对象的作用域,判断对象是否 “逃逸” 出方法 / 线程:

    • 未逃逸:对象仅在方法内创建和使用,未被返回或传递给其他方法 / 线程;
    • 逃逸:对象被返回、赋值给全局变量,或传递给其他方法 / 线程。

    基于逃逸分析的结果,JIT 会进行后续优化(标量替换、栈上分配)。

  3. 标量替换(Scalar Replacement):将未逃逸的对象拆解为多个基本数据类型(标量),直接存储在虚拟机栈的局部变量表中,避免在堆中创建对象,减少 GC 开销。

    例子:

    class Point { int x; int y; }
    public static void printPoint() {
        Point p = new Point(); // 对象未逃逸(仅在方法内使用)
        p.x = 1;
        p.y = 2;
        System.out.println(p.x + p.y);
    }
    

    标量替换后:

    public static void printPoint() {
        int x = 1; // 拆解为标量,存储在局部变量表
        int y = 2;
        System.out.println(x + y);
    }
    
  4. 死代码消除(Dead Code Elimination):移除程序中永远不会执行的代码(如条件恒为 false 的分支、未使用的变量赋值),减少执行指令数。

    例子:

    public static int calculate(int a) {
        int b = a + 1;
        if (false) { // 条件恒为false,分支内是死代码
            b = a * 2;
        }
        return b;
    }
    

    死代码消除后:

    public static int calculate(int a) {
        int b = a + 1;
        return b;
    }
    

垃圾回收器

垃圾回收器(Garbage Collector)是 JVM 的 “内存管家”,核心作用是自动识别并回收堆中不再被引用的 “垃圾对象” 内存,避免内存泄漏和OutOfMemoryError(OOM),同时尽可能降低对业务线程的暂停影响(STW,Stop-The-World)。

GC 的核心基础

在讲具体回收器前,先明确 GC 的核心前提:

1. 什么是 “垃圾”?

堆中无法被任何可达引用链指向的对象就是垃圾(比如局部变量执行完出栈后,指向的对象无其他引用)。

JVM 判断对象是否为垃圾的核心方式:

  • 引用计数法(已淘汰):给对象加引用计数器,引用 + 1,引用失效 - 1,计数器 = 0 则为垃圾(缺点:无法解决循环引用);
  • 可达性分析算法(主流):以GC Roots(根节点,如虚拟机栈的局部变量、静态变量、本地方法栈的引用)为起点,遍历对象引用链,无法到达的对象即为垃圾。

2. GC 的核心目标

  • 内存回收:释放垃圾对象占用的内存,避免 OOM;
  • 低暂停:尽可能减少 STW(业务线程暂停)时间,保证程序响应性;
  • 高吞吐量:单位时间内业务代码执行时间占比(吞吐量 = 业务执行时间 /(业务执行时间 + GC 时间))。

3. GC 的核心回收区域

GC 主要回收堆内存(分代模型:新生代 + 老年代),方法区(元空间)也会回收但非核心:

  • 新生代:存储新创建的对象,对象生命周期短,回收频率高(采用标记 - 复制算法,效率高);

    新生代又分为:Eden 区(80%)+ Survivor0 区(10%)+ Survivor1 区(10%)。

  • 老年代:存储从新生代存活下来的 “长寿对象”,回收频率低(采用标记 - 清除 / 标记 - 整理算法)。

GC 的核心算法(回收器的底层基础)

所有垃圾回收器都基于以下 3 种核心算法实现,不同算法适配不同区域:

算法名称核心步骤优点缺点适用区域
标记 - 清除1.标记所有垃圾对象;2. 直接清除垃圾对象实现简单,无需移动对象1.产生内存碎片;2. STW 时间长老年代(CMS)
标记 - 复制1.标记存活对象;2. 复制到新内存区域;3. 清空原区域无内存碎片,回收效率高浪费 50% 内存(需预留空区域)新生代
标记 - 整理1.标记存活对象;2. 移动存活对象到内存一端;3. 清除另一端无内存碎片,不浪费内存移动对象开销大,STW 时间较长老年代

JVM 经典垃圾回收器(分类 + 核心特性)

JVM 提供了多种垃圾回收器,按 “分代” 和 “适用场景” 可分为两类:分代回收器(适配新生代 + 老年代)、不分代回收器(G1/ZGC/Shenandoah)。

1.分代回收器(JDK8 及之前主流)

这类回收器分工明确:新生代回收器负责新生代 GC(YGC,Young GC),老年代回收器负责老年代 GC(Full GC)。

(1)新生代回收器

回收器名称核心特点优点缺点适用场景
Serial GC(串行)单线程执行 YGC,标记 - 复制算法,全程 STW占用 CPU 少,实现简单STW 时间长,效率低单核 CPU、小内存(客户端程序)
ParNew GCSerial 的多线程版本,标记 - 复制算法,全程 STW多线程提速 YGC占用多核 CPU配合 CMS 老年代回收器使用
Parallel Scavenge多线程执行 YGC,标记 - 复制算法,关注吞吐量(可设置吞吐量目标)吞吐量高,适合批量处理STW 时间比 ParNew 略长后台服务、批处理程序(JDK8 默认新生代回收器)

(2)老年代回收器

回收器名称核心特点优点缺点适用场景
Serial OldSerial 的老年代版本,单线程,标记 - 整理算法,全程 STW占用 CPU 少STW 时间极长配合 Serial GC(客户端)
Parallel OldParallel Scavenge 的老年代版本,多线程,标记 - 整理算法,关注吞吐量吞吐量高STW 时间较长配合 Parallel Scavenge(JDK8 默认老年代回收器)
CMS(并发标记清除)多线程,标记 - 清除算法,关注低延迟,分 4 步:
1. 初始标记(STW,标记 GC Roots 直接引用)
2. 并发标记(不 STW,遍历引用链)
3. 重新标记(STW,修正并发标记的遗漏)
4. 并发清除(不 STW,清除垃圾)
STW 时间极短,响应性高1.产生内存碎片;2. 占用 CPU 高;3. 有浮动垃圾(并发清除时产生的新垃圾)对延迟敏感的场景(如 Web 服务)

2.不分代回收器(JDK9 + 主流,适配大内存)

这类回收器打破分代模型,将堆划分为多个大小相等的 Region(区域),兼顾吞吐量和低延迟,适合大内存场景(4G 以上)。

回收器名称核心特点优点缺点适用场景
G1 GC(Garbage-First)1. 堆划分为多个 Region(新生代 / 老年代 Region 混合分布);
2. 优先回收垃圾多的 Region(Garbage-First);
3. 标记 - 复制 + 标记 - 整理结合,分 4 步:初始标记→并发标记→最终标记→筛选回收(STW)
兼顾吞吐量和低延迟;2. 可设置最大 STW 时间(-XX:MaxGCPauseMillis);3. 无内存碎片STW 时间比 CMS 略长(但可控制)大内存(4G+)、混合场景(JDK9 + 默认)
ZGC基于 Region,采用 “染色指针” 技术,几乎全程并发执行,STW 时间 < 10ms极低延迟(STW<10ms),支持 TB 级内存吞吐量略低于 G1超大内存、超低延迟场景(如金融、电商核心服务)
Shenandoah与 ZGC 类似,全程并发,STW 时间 < 10ms,开源免费极低延迟,适配更多平台吞吐量略低超低延迟、大内存场景

3.回收器的组合规则(JDK 默认)

不同回收器不能随意组合,JVM 有固定搭配:

  • JDK7/8 默认:Parallel Scavenge(新生代) + Parallel Old(老年代)(吞吐量优先);
  • JDK9 + 默认:G1 GC(不分代,兼顾吞吐量和延迟);
  • 常用自定义组合:ParNew(新生代) + CMS(老年代)(低延迟)。

运行时数据区

运行时数据区是 JVM 在执行 Java 程序时,专门用于存储运行时数据和执行上下文的内存区域,是 JVM 核心组成部分之一,它的设计直接决定了 JVM 的内存管理效率、线程安全特性,以及 GC 的回收范围。

一、运行时数据区的整体架构(核心分类),如上图

  • 线程私有区域:每个线程独立拥有,生命周期与线程一致,线程销毁时区域释放,无线程安全问题
  • 线程共享区域:所有线程共用,生命周期与 JVM 一致,是 GC 的主要回收区域,也是线程安全问题的高发区。

二、线程私有区域

1. 程序计数器(Program Counter Register)

核心作用:记录当前线程执行的字节码指令的行号指示器,相当于线程的 “执行书签”:

  • 执行 Java 方法时,存储当前字节码指令的地址索引(对应javap -v查看的字节码行号);
  • 执行本地方法(native)时,计数器值为undefined(本地方法由 C/C++ 执行,JVM 无需跟踪)。

核心特点

  • 线程私有:每个线程有独立的程序计数器,避免线程切换时指令执行位置混乱;
  • 唯一不抛出 OOM 的区域:JVM 规范规定,该区域不会发生OutOfMemoryError(内存溢出),因为它的内存大小固定(仅存储一个地址 / 行号);
  • 执行引擎的 “导航仪”:执行引擎通过程序计数器的指示,依次读取字节码指令执行。

2.虚拟机栈(Java Virtual Machine Stack)

核心作用:存储每个Java 方法执行的上下文,即栈帧(Stack Frame)。方法的调用过程对应栈帧的入栈,方法的执行完成对应栈帧的出栈

  • 局部变量表:存放编译期可知的各种基本数据类型(int, boolean, char 等)、对象引用(reference 类型,不等同于对象本身,可能是指向对象起始地址的指针或句柄)和 returnAddress 类型(指向一条字节码指令的地址)。
  • 操作数栈:方法执行过程中,进行算术运算传递方法调用参数的临时工作区。比如执行 iadd 指令时,就从操作数栈弹出两个整数,相加后再压入。
  • 动态链接:指向运行时常量池中该栈帧所属方法的引用。将符号引用(如方法名)转换为直接调用地址(内存地址)的过程就发生在这里。
  • 方法返回地址:方法退出后,需要返回到调用它的位置,这个地址就保存在这里。

核心特点

  • 线程私有:每个线程的虚拟机栈独立,栈帧的入栈 / 出栈仅影响当前线程;
  • 栈深度有限:虚拟机栈的最大深度由 JVM 参数-Xss(如-Xss1M)指定,超出则抛出 StackOverflowError(栈溢出);
  • 可动态扩容:部分 JVM 实现支持虚拟机栈动态扩容,若扩容时内存不足,会抛出 OutOfMemoryError
  • 无 GC 回收:栈帧随方法调用 / 执行完成自动入栈 / 出栈,内存随线程销毁释放,无需 GC 干预。

三、线程共享区域

1. 堆(Heap)——GC 的核心回收区域

堆是 JVM 中内存最大、最重要的共享区域,也是 GC 的唯一核心回收目标(方法区回收为辅助)。

核心作用:存储所有对象实例和数组(几乎所有new出来的东西都存在这里),比如new User()int[] arr = new int[10]

核心特点

  • 线程共享:所有线程可访问堆中的对象,因此对象的线程安全需要通过synchronizedvolatile等机制保证;
  • 内存可动态调整:通过 JVM 参数-Xms(堆初始大小)和-Xmx(堆最大大小)配置,如-Xms2G -Xmx4G
  • GC 的主战场:堆中不再被引用的对象会被 GC 回收,若堆内存不足,抛出 OutOfMemoryError: Java heap space
  • 分代模型:为了提升 GC 效率,堆被划分为新生代老年代

堆的分代模型

新生代:存储新创建的对象,对象生命周期短,回收频率高(每次 YGC 都回收),采用标记 - 复制算法(效率高);

  • Eden 区:新对象优先分配到这里,Eden 区满则触发YGC(新生代 GC)
  • Survivor0/Survivor1:用于存储 YGC 后存活的对象,两个区始终有一个为空(作为复制的目标区域),对象在两个区之间移动,达到年龄阈值(默认 15)则进入老年代;

老年代:存储从新生代存活下来的长寿对象,回收频率低(Full GC 时回收),采用标记 - 清除 / 标记 - 整理算法

  • 老年代满则触发Full GC(整堆回收),Full GC 的 STW 时间远长于 YGC,是 GC 调优的重点优化对象。

2.方法区(Method Area)

方法区是 JVM 规范中的概念,不同 JVM 实现有不同的落地方式:JDK7 及之前为永久代(PermGen),JDK8 及以后替换为元空间(Metaspace)

核心作用:存储类的元数据信息,包括:

  • 类的全限定名、访问修饰符(public/final等);
  • 类的字段(属性)、方法的元数据(参数、返回值、字节码指令);
  • 运行时常量池;
  • 静态变量(static修饰的变量)、常量(final修饰的常量)。

JDK7 vs JDK8:永久代 → 元空间(核心变化)

特性永久代(JDK7 及之前)元空间(JDK8 及以后)
内存位置属于堆内存(堆的永久代分区)属于本地内存(JVM 进程外的系统内存)
内存限制-XX:PermSize/-XX:MaxPermSize限制,默认大小小受系统物理内存限制,默认无上限(可通过-XX:MetaspaceSize/-XX:MaxMetaspaceSize限制)
异常类型内存不足抛出OutOfMemoryError: PermGen space内存不足抛出OutOfMemoryError: Metaspace
GC 回收随 Full GC 回收,效率低独立的元空间 GC,触发条件更宽松,回收效率高

3.运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的重要组成部分,是从.class文件的常量池加载而来的运行时版本。

核心作用:存储编译期生成的常量运行期动态生成的常量,包括:

  • 字面量:字符串常量(如"hello")、基本类型常量(如123);
  • 符号引用:类的全限定名、方法的名称和参数类型、字段的名称和类型;
  • 运行时常量:通过String.intern()动态加入的字符串常量(运行时生成)。

核心特点

  • 动态性:常量池不仅存储编译期常量,还能在运行时动态添加(如String.intern());
  • 内存分配:JDK7 及之前存储在永久代,JDK8 + 存储在中(这是面试高频考点);
  • 异常类型:常量池内存不足,抛出OutOfMemoryError(永久代 / 元空间 / 堆,取决于 JDK 版本)。

在最后,附上整体完整的图