JVM 核心知识点总结
一、JVM的核心定位与概述
JVM的全称
JVM = Java Virtual Machine ,Java虚拟机,是运行所有Java程序的核心运行环境,是Java语言的基石。JVM本质是一个软件模拟的计算机,有自己的指令集、内存区域、寄存器等,能识别并执行Java编译后的字节码文件(.class)
JVM 核心使命(Java的核心优势)
实现 Java 的 一次编写,到处运行 (Write Once, Run Anywhere) 跨平台特性:
-
Java源码(.java)编译为与平台无关的字节码(.class),不直接编译为操作系统的机器码;
-
不同操作系统(Windows/Linux/Mac)安装对应版本的JVM,由JVM负责将字节码解析/编译为当前系统的机器码执行;
-
程序员无需关注底层系统差异,JVM屏蔽了所有平台相关的细节。
JVM 核心地位
Java 程序运行链路:Java源码(.java) → 编译器 → 字节码(.class) → JVM → 机器码 → CPU执行
✅ 结论:JVM是连接Java字节码与操作系统的桥梁,没有JVM,Java程序无法运行
JVM核心特点
-
跨平台:一套字节码,所有平台的JVM均可执行;
-
自动内存管理:核心的垃圾回收(GC) 机制,无需程序员手动申请/释放内存;
-
面向对象支持:完美实现Java的封装、继承、多态等特性;
-
安全可靠:提供类加载校验、内存沙箱等安全机制,防止恶意代码执行
二、JVM的整体架构
JVM是一个完整的执行体系,整体分为三大核心模块,执行流程自上而下,缺一不可:

模块一:类加载子系统
负责将磁盘中的 .class 字节码文件,加载到JVM的内存中(方法区),并完成加载、验证、准备、解析、初始化 5个核心步骤,最终生成可执行的Java类对象
模块二:运行时数据区
JVM的核心内存划分,所有加载的类信息、程序运行的变量、线程数据都存储在这里。
《Java虚拟机 规范》强制划分的内存区域,下文单独详细讲解
模块三:执行引擎
负责执行加载后的字节码指令,核心包含3个组件:
解释器:逐行解析字节码执行,优点是启动快,缺点是执行效率低;
JIT编译器(即时编译器):将热点代码(频繁执行的代码) 一次性编译为机器码,缓存后重复执行,大幅提升执行效率;
本地方法接口(Native Method Interface):调用C/C++编写的本地方法库(.dll/.so),执行native修饰的方法
补充:JVM的执行策略是「解释执行 + 编译执行」混合模式,兼顾启动速度和运行效率。
三、JVM 运行时数据区
JVM的运行时数据区,按线程归属分为两大核心类别:
线程私有区域:每个线程独立拥有一份,生命周期与线程一致,线程创建时分配,线程销毁时释放,无GC回收;
线程共享区域:所有线程共用一份,生命周期与JVM一致,只有在JVM停止时才释放,是垃圾回收(GC)的核心区域。

第一类:线程私有内存区域(3个)
1. 程序计数器 (Program Counter Register)
核心定义:一块极小的内存空间,也叫PC寄存器,JVM规范中唯一不会抛出内存溢出(OutOfMemoryError)的区域
作用:
-
存储当前线程执行Java方法时,下一条要执行的字节码指令地址/行号索引;
-
执行native本地方法时,值为 Undefined(未定义);
-
支撑多线程抢占式调度:线程切换时保存执行位置,恢复时继续执行,实现「断点续跑」;
-
记录方法调用的返回地址,保证方法嵌套调用后正确返回
特性:
线程私有 无GC 无 OOM ,读写效率极致(寄存器级别)
示例说明:
public void example() {
int a = 10; // PC指向这条指令
int b = 20; // 执行后PC指向下一条
int c = a + b; // 继续指向这条指令
return; // 最后指向return指令
}
线程1的程序计数器 线程2的程序计数器
┌─────────────────┐ ┌─────────────────┐
│ PC = 0x1234 │ │ PC = 0x5678 │
│ (执行main方法) │ │ (执行其他方法) │
└─────────────────┘ └─────────────────┘
字节码地址对应:
0x1234: aload_0 // 加载this
0x1235: bipush 10 // 压入10
0x1237: istore_1 // 存储到a
0x1238: bipush 20 // 压入20
0x123A: istore_2 // 存储到b
0x123B: iload_1 // 加载a
0x123C: iload_2 // 加载b
0x123D: iadd // a + b
0x123E: ireturn // 返回
2. 虚拟机栈 (Java Virtual Machine Stack)
核心定义:每个线程创建时都会分配一个虚拟机栈,栈内存储的核心单元是 栈帧(Stack Frame) ,一个栈帧对应一个Java方法的一次调用过程。
方法调用时,栈帧入栈;方法执行完毕,栈帧出栈,栈帧的生命周期与方法调用一致
栈帧核心组成 :每个栈帧包含 局部变量表、操作数栈、动态链接、方法返回地址,存储了方法执行的所有上下文信息
特性:
-
线程私有,栈的深度由JVM参数控制
-
无GC回收,栈帧随方法调用自动创建/销毁
示例代码:
public class StackDemo {
public static void main(String[] args) {
int a = 10;
int b = 20;
int result = add(a, b);
System.out.println(result);
}
private static int add(int x, int y) {
int sum = x + y;
return sum;
}
}
内存示意图:
虚拟机栈 (当前线程)
┌────────────────────────────────┐
│ ┌──────────────────────────┐ │
│ │ main方法栈帧 │ │
│ │ │ │
│ │ 局部变量表 │ │
│ │ ┌────────────┐ │ │
│ │ │ args = {} │ │ │
│ │ │ a = 10 │ │ │
│ │ │ b = 20 │ │ │
│ │ │ result = ? │ │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ 操作数栈 │ │
│ │ ┌────────────┐ │ │
│ │ │ [ ] │ │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ 返回地址: add方法 │ │
│ └──────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ add方法栈帧 │ │
│ │ │ │
│ │ 局部变量表 │ │
│ │ ┌────────────┐ │ │
│ │ │ x = 10 │ │ │
│ │ │ y = 20 │ │ │
│ │ │ sum = 30 │ │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ 操作数栈 │ │
│ │ ┌────────────┐ │ │
│ │ │ 30 │ │ │ (x + y的结果)
│ │ └────────────┘ │ │
│ │ │ │
│ │ 返回地址: main方法 │ │
│ └──────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────┐ │
│ │ System.out.println栈帧 │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
常见异常:
-
StackOverflowError:栈深度溢出,比如无限递归调用方法,栈帧不断入栈,超出栈的最大深度;
-
OutOfMemoryError:虚拟机栈可以动态扩展,当扩展时申请不到足够内存,抛出该异常
3. 本地方法栈 (Native Method Stack)
核心定义:
与虚拟机栈功能完全一致,唯一区别是:虚拟机栈服务于Java方法,本地方法栈服务于native本地方法
特性
-
线程私有,存储native方法的执行上下文;
-
同样会抛出
StackOverflowError和OutOfMemoryError异常; -
不同JVM对本地方法栈的实现不同,比如HotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一
示例代码:
public class NativeDemo {
// 本地方法,调用C/C++代码
private native void nativeMethod();
// Object类中的native方法示例
// public native int hashCode();
// public final native Class<?> getClass();
public static void main(String[] args) {
NativeDemo demo = new NativeDemo();
demo.nativeMethod(); // 在本地方法栈中执行
}
}
内存示意图:
虚拟机栈 本地方法栈
┌──────────────────┐ ┌──────────────────┐
│ Java方法栈帧 │ │ Native方法栈帧 │
│ │ │ │
│ main() │ │ nativeMethod() │ ← 调用C代码
│ └─ nativeMethod()├────────┤ │
│ │ │ C函数调用栈 │
└──────────────────┘ │ - func1() │
│ - func2() │
└──────────────────┘
第二类:线程共享内存区域(2个)是垃圾回收核心区
1. 堆 (Heap)
核心定义:
JVM启动时创建,是JVM中内存占比最大、最核心的区域,也是垃圾回收(GC)的主战场,几乎所有的对象实例和数组都在堆中分配内存。
核心地位:
-
堆是Java程序运行的核心内存区,所有线程共享,也是内存溢出最常见的区域;
-
堆的大小可以通过JVM参数动态调整,是JVM调优的核心目标
堆的内存细分:
JVM的堆内存采用 分代收集思想 划分,核心依据是:对象的存活周期不同,垃圾回收的策略不同,分为两大区域,细分3块:

新生代 (Young Generation) :占堆内存的1/3左右,存储新创建的对象、存活周期短的对象
新生代内部再分 Eden区(伊甸园) + 两个Survivor区(S0区、S1区,也叫From区、To区),默认比例 8:1:1。核心特点:对象创建和销毁频繁,垃圾回收频率高,回收速度快,采用「复制算法」
老年代 (Old Generation) :占堆内存的2/3左右,存储存活周期长的对象
核心特点:对象经过新生代多次GC后仍存活,会被晋升到老年代;回收频率低,单次回收耗时久,采用「标记-整理算法」
代码示例:
public class HeapDemo {
public static void main(String[] args) {
// 对象1:存储在堆中
User user1 = new User("张三", 25);
// 对象2:存储在堆中
User user2 = new User("李四", 30);
// 数组:存储在堆中
int[] numbers = {1, 2, 3, 4, 5};
// user1, user2, numbers 引用存储在栈中,指向堆中的对象
}
}
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name; // "张三"存储在堆中
this.age = age; // 25存储在堆中
}
}
内存示意:
栈内存 堆内存
┌─────────────────────┐ ┌────────────────────────┐
│ main方法栈帧 │ │ │
│ │ │ ┌──────────────────┐ │
│ user1 ──────────────┼───────▶│ │ User对象1 │ │
│ [0x1234] │ │ │ name: "张三" │ │
│ │ │ │ age: 25 │ │
│ user2 ──────────────┼───────▶│ └──────────────────┘ │
│ [0x5678] │ │ [0x1234] │
│ │ │ │
│ numbers ────────────┼───────▶│ ┌──────────────────┐ │
│ [0x9ABC] │ │ │ User对象2 │ │
│ │ │ │ name: "李四" │ │
└─────────────────────┘ │ │ age: 30 │ │
│ └──────────────────┘ │
│ [0x5678] │
│ │
│ ┌──────────────────┐ │
│ │ int[]数组 │ │
│ │ [1, 2, 3, 4, 5] │ │
│ └──────────────────┘ │
│ [0x9ABC] │
└────────────────────────┘
堆的核心异常:
OutOfMemoryError: Java heap space → 堆内存溢出,创建的对象过多,内存不足,GC无法回收足够内存。
堆内存中还有一个 永久代(PermGen) ,JDK8之前归属于堆,JDK8及以后被 元空间(Metaspace) 替代,永久代彻底移除
2. 方法区 (Method Area)
核心定义:
线程共享的内存区域,也叫「永久代」(JDK8前),存储JVM加载的类的元数据信息,是Java类的「说明书」
存储内容:
-
类的全限定名、访问修饰符、字段信息、方法信息;
-
常量池、静态变量、即时编译器编译后的代码缓存;
-
父类和接口的引用、方法字节码等
核心特性:
-
线程共享,生命周期与JVM一致;
-
方法区的内存也会被GC回收,回收内容是:无用的类信息、常量池中的废弃常量
示例代码:
public class MethodAreaDemo {
// 静态变量:存储在方法区
private static String STATIC_CONFIG = "config_value";
private static final int MAX_SIZE = 100;
private static final Map<String, String> CACHE = new HashMap<>();
// 类信息:存储在方法区
// - 类名、父类、接口
// - 方法的字节码
// - 字段信息
public static void main(String[] args) {
// 字符串常量:存储在方法区的字符串常量池
String s1 = "hello";
String s2 = "hello"; // s2和s1指向同一个字符串常量
System.out.println(s1 == s2); // true,指向同一对象
}
}
内存示意图:
方法区/元空间
┌─────────────────────────────────────────┐
│ 类信息 (Class Metadata) │
│ ┌─────────────────────────────────┐ │
│ │ MethodAreaDemo类 │ │
│ │ - 类名: MethodAreaDemo │ │
│ │ - 方法: main() │ │
│ │ - 字段: STATIC_CONFIG │ │
│ └─────────────────────────────────┘ │
│ │
│ 静态变量 │
│ ┌─────────────────────────────────┐ │
│ │ STATIC_CONFIG = "config_value" │ │
│ │ MAX_SIZE = 100 │ │
│ │ CACHE = HashMap对象引用 │ │
│ └─────────────────────────────────┘ │
│ │
│ 字符串常量池 │
│ ┌─────────────────────────────────┐ │
│ │ "hello" ←──── s1 ─────┐ │ │
│ │ │ │ │
│ │ "config_value" │ │ │
│ │ │ │ │
│ │ "Java" │ │ │
│ └────────────────────────┘ │ │
│ │ │
└───────────────────────────────────┼────┘
│
栈内存 │
┌─────────▼────────┐
│ main方法栈帧 │
│ │
│ s1 ──────────────┘
│ │
│ s2 ──────────────┘
│ │
└──────────────────┘
永久代 vs 元空间(JDK8重大变更)
DK7及之前:方法区的实现是 永久代(PermGen),属于堆内存的一部分,大小固定,容易溢出;
JDK8及之后:永久代被 元空间(Metaspace) 彻底替代,元空间不再属于堆内存,而是直接使用操作系统的本地内存;
变更原因:永久代大小固定,容易触发 OutOfMemoryError: PermGen space,元空间使用本地内存,内存上限更高,溢出概率大幅降低。
延伸:运行时常量池 (Runtime Constant Pool)
属于方法区的一部分,是.class文件中「常量池」的运行时版本,存储字符串常量、字面量、符号引用等,是String的intern()方法核心依赖,也是方法区的重要组成部分
方法区核心异常
JDK7及之前:OutOfMemoryError: PermGen space 永久代溢出;
JDK8及之后:OutOfMemoryError: Metaspace 元空间溢出。
3. 直接内存(Direct Memory)
作用:
避免在Java堆和Native堆之间复制数据
特点:
-
不受JVM堆内存限制
-
用于NIO操作
示例代码:
import java.nio.ByteBuffer;
public class DirectMemoryDemo {
public static void main(String[] args) {
// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB
// 这1MB不在Java堆中,而是在直接内存中
// 优点:减少Java堆和Native堆之间的数据复制
// 缺点:分配和释放成本较高
}
}
内存示意图:
JVM堆内存 直接内存
┌──────────────────┐ ┌──────────────────┐
│ Java对象 │ │ DirectBuffer │
│ │ │ │
│ ┌────────────┐ │ │ ┌────────────┐ │
│ │HeapBuffer │ │ │ │ 1MB数据 │ │
│ │(堆内缓冲) │ │ │ │ │ │
│ └────────────┘ │ │ └────────────┘ │
│ │ │ │
│ 需要复制数据 │ │ 直接访问 │
│ 到Native堆 │ │ 无需复制 │
└──────────────────┘ └──────────────────┘
↓ ↑
复制操作 零拷贝
完整示例(多线程场景)
示例代码:
public class CompleteMemoryDemo {
// 静态变量:存储在方法区
private static final ConcurrentHashMap<String, String> CACHE =
new ConcurrentHashMap<>();
// 实例方法
public void processData(String input) {
// 局部变量:存储在栈中
String processed = input.toUpperCase();
// 创建对象:存储在堆中
User user = new User(processed);
// 对象引用存储在栈中,对象在堆中
CACHE.put(input, processed);
}
public static void main(String[] args) {
// 主线程
CompleteMemoryDemo demo = new CompleteMemoryDemo();
// 创建多个线程
for (int i = 0; i < 3; i++) {
final int index = i;
new Thread(() -> {
demo.processData("data-" + index);
}).start();
}
}
}
class User {
private String name;
public User(String name) {
this.name = name;
}
}
内存分布图:
JVM运行时数据区
┌─────────────────────────────────────────────────────┐
│ 方法区 │
│ ┌──────────────────────────────────────────────┐ │
│ │ CompleteMemoryDemo类信息 │ │
│ │ - 方法: processData(), main() │ │
│ │ - 字段: CACHE │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ CACHE = ConcurrentHashMap引用 │ │
│ │ (实际对象在堆中) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 堆内存 │
│ ┌──────────────────────────────────────────────┐ │
│ │ ConcurrentHashMap实例 │ │
│ │ - "data-0" → "DATA-0" │ │
│ │ - "data-1" → "DATA-1" │ │
│ │ - "data-2" → "DATA-2" │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ User对象1 │ │ User对象2 │ │ User对象3 │ │
│ │ name="DATA │ │ name="DATA │ │ name="DATA │ │
│ │ -0" │ │ -1" │ │ -2" │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────┘
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ 主线程的虚拟机栈 │ │ 线程1的虚拟机栈 │ │ 线程2的虚拟机栈 │
│ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ main()栈帧 │ │ │ │ run()栈帧 │ │ │ │ run()栈帧 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ demo ────────┼──┼──┼──▶│ processed │ │ │ │ processed │ │
│ │ │ │ │ │ index = 0 │ │ │ │ index = 1 │ │
│ │ for循环变量 │ │ │ │ user ────────┼──┼──┼──▶│ user ────────┼──┼────
│ │ i = 0,1,2 │ │ │ │ │ │ │ │ │ │
│ └──────────────┘ │ │ └──────────────┘ │ │ └──────────────┘ │
└────────────────────┘ └────────────────────┘ └────────────────────┘
┌────────────────────┐
│ 线程3的虚拟机栈 │
│ ┌──────────────┐ │
│ │ run()栈帧 │ │
│ │ │ │
│ │ processed │ │
│ │ index = 2 │ │
│ │ user ────────┼────┘
│ │ │ │
│ └──────────────┘ │
└────────────────────┘
每个线程都有独立的:
- 虚拟机栈
- 程序计数器
- 本地方法栈
但共享:
- 堆内存
- 方法区/元空间
四、JVM 类加载机制
1. 类加载的核心定义
类加载子系统将磁盘中的 .class 字节码文件,加载到JVM的方法区,并在堆中生成对应的java.lang.Class对象,这个过程就是类加载,只加载一次,复用永久。
2. 类的完整生命周期(7步)
一个Java类从被加载到JVM内存,到最终被卸载,完整生命周期分为7个步骤,顺序固定,核心是前5步的类加载过程

核心步骤详解
加载:通过类的全限定名,读取.class文件到方法区,生成Class对象;
验证:校验.class文件的合法性,防止恶意字节码、文件损坏,保证安全;
准备:为类的静态变量分配内存,并赋上默认初始值(如int=0、String=null);
解析:将常量池中的符号引用替换为直接引用(内存地址);
初始化:执行静态代码块、为静态变量赋程序员指定的初始值,是类加载的核心步骤;
使用:程序调用类的方法、访问字段,正常使用类;
卸载:类的Class对象被GC回收,类的元数据信息从方法区移除,只有极少场景会触发
3. 类加器的分类(4类)

类加载的执行者是类加载器,JVM内置3类核心类加载器,开发者可自定义第4类,按加载优先级从高到低排序:
启动类加载器(Bootstrap ClassLoader):最顶层,C/C++编写,加载JDK的核心类库(rt.jar),无父类加载器;
扩展类加载器(Extension ClassLoader):加载JDK扩展类库(jre/lib/ext),父类是启动类加载器;
应用程序类加载器(Application ClassLoader):加载项目的classpath下的类,是程序默认的类加载器,父类是扩展类加载器;
自定义类加载器:继承ClassLoader,开发者自定义加载规则,比如加载磁盘/网络中的.class文件。
4. 双亲委派模型
核心定义
JVM类加载的核心机制:一个类加载器收到类加载请求时,首先将请求委托给父类加载器,只有父类加载器无法加载时,子类加载器才会自己加载
核心优点
-
保证类的唯一性:同一个类只会被加载一次,避免重复加载;
-
保证核心类的安全:核心Java类(如java.lang.String)只能被启动类加载器加载,防止恶意代码篡改核心类;
-
实现类的分层加载:核心类库由顶层加载,业务类由应用加载,职责清晰
沙箱机制
JVM沙箱是JVM提供的安全隔离与权限控制系统,将Java代码限制在特定执行范围内,严格管控其对本地系统资源(文件、网络、系统命令等)的访问,防止恶意或错误代码破坏宿主环境,是Java安全模型的核心。
双亲委派确保核心类优先由启动类加载器加载,自定义类无法替换核心类;不同类加载器加载的类命名空间隔离,避免相互干扰,在类加载器加载的类只能看到自己类加载器或者父类类加载器加载的所有类
五、JVM 垃圾回收 GC(Garbage Collection) 核心全解
GC是JVM的灵魂特性,也是Java的核心优势:自动回收不再使用的对象内存,无需程序员手动释放,彻底解决了C/C++的内存泄漏问题。
GC的核心回收区域:堆内存 + 方法区,线程私有区域无GC回收;GC的核心对象:堆中的对象实例
1. 垃圾回收的核心前提:如何判断对象是「垃圾」?
识别堆中哪些对象是「存活」的,哪些是「死亡(垃圾)」的,JVM有2个核心算法,目前主流是第2个
引用计数法:
引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该 对象被引用的次数(Reference Count)。如果该对象被引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0 时,那么该对象就会被回收
-
原理:给每个对象加一个引用计数器,被引用时+1,引用失效时-1,计数器为0则是垃圾;
-
缺点:无法解决循环引用问题(A引用B,B引用A,计数器都不为0,永远无法回收),JVM已弃用
代码示例:
String m = new String("jack");
// 引用次数:RC =1
m = null;
// 引用次数 RC = 0;
// 这时候 "jack" 即将被回收
先创建一个字符串,这时候"jack"有一个引用,就是 m。然后将 m 设置为 null,这时候"jack"的引用次数就等于0了,在引用计数算法中,意 味着这块内容就需要被回收了。
可达性分析算法:
原理:以 GC Roots(GC根节点) 为起点,向下遍历对象的引用链,没有被引用链连接的对象,就是垃圾对象;
GC Roots 包含哪些对象:虚拟机栈的栈帧中的局部变量、方法区的静态变量、方法区的常量、本地方法栈的native引用。
优点:完美解决循环引用问题,是所有JVM的默认垃圾判断算法
public class GCRootExample {
Object instanceVariable; // 静态字段引用
static Object staticVariable; // 静态字段引用
public void useLocalVariable() {
Object localVariable = new Object(); // 虚拟机栈中的引用
doSomething(localVariable); // 传递给本地方法
}
private native void doSomething(Object obj); // JNI本地方法
public static void main(String[] args) {
GCRootExample example = new GCRootExample(); // 虚拟机栈中的引用
example.instanceVariable = new Object(); // 实例变量
staticVariable = new Object(); // 静态变量
// 使example对象保持活跃状态,避免被GC回收
Runtime.getRuntime().gc(); // 强制执行垃圾收集
example.useLocalVariable(); // 调用本地方法,传递局部变量引用
}
static {
System.loadLibrary("example"); // 加载本地库,确保JNI引用有效
}
}
2. 引用类型(按优先级从高到低 4种)
Java的引用类型决定了对象的回收优先级,是GC的核心基础:
强引用:最普通的引用(Object obj = new Object()),只要强引用存在,对象永远不会被GC回收,是内存溢出的主要原因;
软引用:内存充足时不回收,内存不足时才回收,适合做缓存(SoftReference);
弱引用:只要触发GC,无论内存是否充足,都会回收,适合做临时缓存(WeakReference);
虚引用:最弱的引用,仅用于监听对象被GC回收的事件,无实际引用意义(PhantomReference)
3. 经典垃圾回收算法(4种)
GC的核心算法,JVM的垃圾回收器都是基于这些算法实现的,按使用场景分类,分代收集的核心依据
① 标记-清除算法 (Mark-Sweep)
-
原理:分两步 → 标记:标记所有存活对象;清除:回收未标记的垃圾对象;
-
优点:实现简单;
-
缺点:产生内存碎片,内存碎片过多会导致无法分配大对象,触发频繁GC

② 标记-复制算法 (Mark-Copy)
-
原理:将内存分为大小相等的两块,只使用一块;GC时标记存活对象,复制到另一块内存,然后清空当前块;
-
优点:无内存碎片,回收效率高;
-
缺点:内存利用率低(仅50%);
-
适用场景:新生代(对象存活率低,复制成本低),也是新生代的默认算法

③ 标记-整理算法 (Mark-Compact)
-
原理:分三步 → 标记:标记存活对象;整理:将存活对象向内存一端移动;清除清空另一端的垃圾对象;
-
优点:无内存碎片,内存利用率100%;
-
缺点:整理阶段需要移动对象,耗时较长;
-
适用场景:老年代(对象存活率高,整理成本低于复制成本),也是老年代的默认算法

④ 分代收集算法 (Generational Collection)
-
原理:不是独立算法,是组合策略 → 新生代用「标记-复制」,老年代用「标记-整理/标记-清除」;
-
核心依据:新生代对象存活短、回收频繁,复制算法效率高;老年代对象存活长、回收少,整理算法更合适;
-
地位:所有JVM的默认GC策略,是目前最优的垃圾回收思路。

Minor GC 、 Major GC 、 Full GC ?
新生代内存不够用时候发生Minor GC 也叫 Yong GC ,老年代内存不够的时候发生 Major GC, Minor GC 相比 Major GC 更频繁,回收速度也更快。 还有一种GC 负责整个新生代 + 老年代的回收称为 Full GC.
触发Full GC执行的情况有如下五种:
1. 调用System.gc()时,系统建议执行Full GC,但是不必然执行;
2. 老年代空间不足;
3. 方法区空间不足;
4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
5. 由Eden区、Survivor space(From Space)区向Survivor space(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
4. 常用垃圾回收器(5种)
垃圾回收器是GC算法的具体实现,不同回收器有不同的特性、适用场景,JVM支持组合使用,核心分类:新生代回收器 + 老年代回收器,以下是主流的5种,按版本排序
新生代回收器(2种)
1.Serial GC(串行回收器):单线程回收,暂停所有用户线程(STW),效率低,适合单核、小内存应用;
2. ParNew GC(并行回收器):Serial的多线程版本,多核环境下效率提升,是CMS回收器的默认新生代搭档。
1.Eden 区:
业务处理中,绝大多数对象是朝生夕死,这种对象会在新生代 Eden 区中进行内存分 配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC。
通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些 无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)
2.Survivor 区:
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。 Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会 将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进 入 Old 区)
老年代回收器(5种)
1.Parallel GC(并行回收器):JDK8 默认的垃圾回收器,新生代+老年代都支持并行回收,追求最大吞吐量,适合后台计算、批量处理等无界面应用;
2.CMS GC(并发标记清除):以最短停顿时间为目标,并发执行,减少STW时间,适合响应时间敏感的应用(如电商、金融),缺点是产生内存碎片、CPU占用高;
3.G1 GC(Garbage First):JDK9 默认的垃圾回收器,JVM的终极回收器之一,兼顾吞吐量和停顿时间,将堆划分为多个区域,优先回收垃圾多的区域,无内存碎片,适合大内存、高并发应用;
4.ZGC(Z垃圾回收器):JDK15正式转正,以极致低延迟(≤10ms) 为核心目标,支持TB级超大内存,通过染色指针、读屏障和内存多重映射技术实现并发回收,无内存碎片,适合超大内存、核心低延迟业务(如金融高频交易);
5.Shenandoah GC(神龙回收器):JDK17正式转正,以极致低延迟为核心目标,对标ZGC,无着色指针设计兼容性更好,CPU开销更低,支持TB级内存,无内存碎片,适合超大内存低延迟业务(ZGC的优秀替代方案)
Old 区:
老年代占据着2/3的堆内存空间,只有在Major GC或者(Full GC) 的时候才会进行清理, 每次 GC 都会触发“ Stop-The-World ”。内存越大,STW 的时间也越长,所以内存 也不仅仅是越大就越好由于复制算法在对象存活率较高的老年代会进行很多次的复 制操作,效率很低,所以老年代这里采用的是 标记-整理 算法。
Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的 其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许 GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。
GC 举例
我们编写一个OOM的异常:
public class OOMTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "mogu blog";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Exception e) {
e.getStackTrace();
}
}
}
然后设置JVM启动参数指定堆最大为10m:

打印出的日志如下:
[0.189s][info ][gc,metaspace] GC(6) Metaspace: 438K(640K)->438K(640K) NonClass: 410K(512K)->410K(512K) Class: 27K(128K)->27K(128K)
[0.189s][info ][gc ] GC(6) Pause Young (Normal) (G1 Humongous Allocation) 4M->4M(6M) 0.384ms
[0.189s][info ][gc,cpu ] GC(6) User=0.00s Sys=0.00s Real=0.00s
[0.189s][info ][gc,ergo ] Attempting full compaction
[0.189s][info ][gc,task ] GC(7) Using 1 workers of 8 for full compaction
[0.189s][info ][gc,start ] GC(7) Pause Full (G1 Compaction Pause)
[0.190s][info ][gc,phases,start] GC(7) Phase 1: Mark live objects
[0.191s][info ][gc,phases ] GC(7) Phase 1: Mark live objects 1.362ms
[0.191s][info ][gc,phases,start] GC(7) Phase 2: Prepare for compaction
[0.191s][info ][gc,phases ] GC(7) Phase 2: Prepare for compaction 0.054ms
[0.191s][info ][gc,phases,start] GC(7) Phase 3: Adjust pointers
[0.192s][info ][gc,phases ] GC(7) Phase 3: Adjust pointers 0.598ms
[0.192s][info ][gc,phases,start] GC(7) Phase 4: Compact heap
[0.192s][info ][gc,phases ] GC(7) Phase 4: Compact heap 0.098ms
[0.192s][info ][gc,heap ] GC(7) Eden regions: 0->0(1)
[0.192s][info ][gc,heap ] GC(7) Survivor regions: 0->0(0)
[0.192s][info ][gc,heap ] GC(7) Old regions: 2->2
[0.192s][info ][gc,heap ] GC(7) Archive regions: 0->0
[0.192s][info ][gc,heap ] GC(7) Humongous regions: 3->3
[0.192s][info ][gc,metaspace ] GC(7) Metaspace: 438K(640K)->438K(640K) NonClass: 410K(512K)->410K(512K) Class: 27K(128K)->27K(128K)
[0.192s][info ][gc ] GC(7) Pause Full (G1 Compaction Pause) 4M->4M(6M) 2.553ms
[0.192s][info ][gc,cpu ] GC(7) User=0.00s Sys=0.00s Real=0.00s
[0.192s][info ][gc,ergo ] Attempting maximum full compaction clearing soft references
[0.192s][info ][gc,task ] GC(8) Using 1 workers of 8 for full compaction
[0.192s][info ][gc,start ] GC(8) Pause Full (G1 Compaction Pause)
[0.192s][info ][gc,phases,start] GC(8) Phase 1: Mark live objects
[0.193s][info ][gc,phases ] GC(8) Phase 1: Mark live objects 1.352ms
[0.194s][info ][gc,phases,start] GC(8) Phase 2: Prepare for compaction
[0.194s][info ][gc,phases ] GC(8) Phase 2: Prepare for compaction 0.181ms
[0.194s][info ][gc,phases,start] GC(8) Phase 3: Adjust pointers
[0.194s][info ][gc,phases ] GC(8) Phase 3: Adjust pointers 0.449ms
[0.194s][info ][gc,phases,start] GC(8) Phase 4: Compact heap
[0.195s][info ][gc,phases ] GC(8) Phase 4: Compact heap 0.374ms
[0.195s][info ][gc,heap ] GC(8) Eden regions: 0->0(1)
[0.195s][info ][gc,heap ] GC(8) Survivor regions: 0->0(0)
[0.195s][info ][gc,heap ] GC(8) Old regions: 2->2
[0.195s][info ][gc,heap ] GC(8) Archive regions: 0->0
[0.195s][info ][gc,heap ] GC(8) Humongous regions: 3->3
[0.195s][info ][gc,metaspace ] GC(8) Metaspace: 438K(640K)->438K(640K) NonClass: 410K(512K)->410K(512K) Class: 27K(128K)->27K(128K)
[0.195s][info ][gc ] GC(8) Pause Full (G1 Compaction Pause) 4M->4M(6M) 2.593ms
[0.195s][info ][gc,cpu ] GC(8) User=0.00s Sys=0.00s Real=0.00s
[0.195s][info ][gc,marking ] GC(3) Concurrent Mark From Roots 7.448ms
[0.195s][info ][gc,marking ] GC(3) Concurrent Mark Abort
[0.195s][info ][gc ] GC(3) Concurrent Mark Cycle 7.586ms
[0.196s][info ][gc,heap,exit ] Heap
[0.196s][info ][gc,heap,exit ] garbage-first heap total 6144K, used 4553K [0x00000000ffa00000, 0x0000000100000000)
[0.196s][info ][gc,heap,exit ] region size 1024K, 1 young (1024K), 0 survivors (0K)
[0.196s][info ][gc,heap,exit ] Metaspace used 457K, committed 640K, reserved 1056768K
[0.196s][info ][gc,heap,exit ] class space used 30K, committed 128K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray0(Unsafe.java:1382)
at java.base/jdk.internal.misc.Unsafe.allocateUninitializedArray(Unsafe.java:1375)
at java.base/java.lang.StringConcatHelper.newArray(StringConcatHelper.java:494)
at java.base/java.lang.StringConcatHelper.simpleConcat(StringConcatHelper.java:421)
at java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder)
at java.base/java.lang.invoke.DelegatingMethodHandle$Holder.reinvoke_L(DelegatingMethodHandle$Holder)
at java.base/java.lang.invoke.Invokers$Holder.linkToTargetMethod(Invokers$Holder)
at java8.gc.TestGC.main(TestGC.java:19)
Process finished with exit code 1
我们看到,触发OOM的时候,一定是进行了一次Full GC,因为只有在老年代空间不足时候,才会爆出OOM异常。
六、JVM 性能调优核心
1. 调优核心原则
-
调优是最后手段:先优化代码、解决内存泄漏、减少大对象,再进行JVM调优;
-
调优核心目标:减少GC频率、缩短STW时间、提升吞吐量/响应时间;
-
分代调优:新生代调优为主,老年代调优为辅,新生代的GC频率远高于老年代
2. 常用核心JVM启动参数

堆内存参数(最核心)
-Xms2G :设置堆的初始内存为2G,建议与-Xmx一致,避免内存扩容的开销;
-Xmx4G :设置堆的最大内存为4G;
-XX:NewRatio=2 :新生代:老年代 = 1:2,默认比例;
-XX:SurvivorRatio=8 :新生代中Eden:S0:S1 = 8:1:1,默认比例
垃圾回收器参数
-XX:+UseParallelGC :使用Parallel并行回收器(JDK8默认);
-XX:+UseG1GC :使用G1回收器(JDK9默认,推荐);
-XX:+UseZGC :使用ZGC回收器(JDK15+支持,需手动开启);
-XX:+UseShenandoahGC :使用Shenandoah回收器(JDK17+支持,需手动开启);
-XX:+PrintGCDetails :打印详细GC日志,排查GC问题必备;
-XX:+HeapDumpOnOutOfMemoryError :开启内存溢出时自动保留dump文件(核心参数);
-XX:HeapDumpPath=/path/to/dumpfile.hprof :指定内存溢出dump文件的保存路径(可选,默认生成在JVM启动目录)
其他核心参数
-Xss1M :设置每个线程的虚拟机栈内存为1M;
-XX:MetaspaceSize=256M :设置元空间初始内存
3. 调优核心步骤
-
开启GC日志,记录GC的频率、STW时间、内存占用;
-
分析日志,判断是内存不足还是GC策略不合理;
-
调整堆内存大小、新生代比例、垃圾回收器;
-
压测验证,对比调优前后的吞吐量、响应时间;
-
迭代优化,直到达到业务要求
七、JVM工具介绍
7.1 这里先前置介绍一个命令:jps(Java Process Status)是一个用来列出Java进程的工具。它是一个Java命令行工具(JDK携带),用于显示当前运行的Java进程的详细信息,包括进程ID(PID)、类路径(Classpath)、主类(Main Class)以及传递给Java虚拟机(JVM)的启动参数。
比如使用 jps -lm
jps -lm
展示信息如下,第一列为java程序的id,第二列是启动类程序,第三列是传入参数:

如果想要看jvm的参数,可以增加参数v的传入:
jps -v

7.2 .jmap
是 Java 虚拟机(JVM)的一个诊断工具,它允许用户查询 Java 进程的内存映射信息,以及获取 Java 堆(Heap)的使用情况。
1.1 使用jmap查看内存中的对象信息及其大小:jmap -histo
# 进程号传入上方jps查看的进程号
jmap -histo 进程号
# 可以将信息输出到文件中
jmap -histo 12345 > ./obj.log
展示信息如下:

-
- num:序号
- instances:实例数量
- bytes:占用空间大小
- class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
1.2 使用jmap查看堆信息: jmap -heap
直接使用命令如下:
# 12345 是使用jps查看到的进程号
jmap -heap 12345

从上面的信息可以看到,堆中信息的各种配置,以及目前使用的垃圾收集器以及他们的各种参数等,这些信息对于线上信息的实时排查还是很有用的
1.3 jmap导出堆内存dump: jmap -dump
直接使用如下命令即可
# format=b 二进制形式导出,file 声明dump文件 最后加进程号
jmap -dump:format=b,file=file.dump 25928
# 还可以如下写,没有任何区别,注意hprof与dump后缀都是可以的
jmap -dump:format=b,file=file.hprof 25928

然后使用dump分析工具将导出的文件进行分析即可,就可以查看到各个对象的情况了,我比较常用的是在线的heaphero:
https://heaphero.io/,这是一个免费高效的在线分析dump文件工具,如果访问不了网络还可以使用JDK自带的jvisualvm来进行分析
7.3.Jstack
jstack 是 Java 虚拟机(JVM)的一个诊断工具,它允许用户查看 Java 进程中线程的堆栈跟踪信息。
2.1 jstack 查看线程信息,可用于查看线程死锁
使用如下命令:
# 查看进程12345的线程信息
jstack 12345
# 也可以将其导入到文件,方便查看
jstack 12345 > ./stack.log

如果两个线程死锁了,通过这个信息也是可以观察出来的(无需增加任何参数),如下图,可以看到两个线程互相等待锁了,这个信息可以打印到文件方便查看:

2.2 jstack排查CPU异常升高的问题
1.使用jps查看到java进程号,然后使用top命令查看占用的内存信息:
top -p 28771

下面是对top命令的展示解释:
Tasks: 显示当前展示的任务数
%Cpu: 显示CPU的使用情况,包括用户态和系统态的CPU使用百分比。
KiB Mem: 显示进程使用的物理内存大小(以KiB为单位)。
KiB Swap: 显示进程使用的交换空间大小(如果有的话,以KiB为单位)。
PID: 进程ID。
User: 运行该进程的用户名。
PR: 进程优先级,数字越小,优先级越高。
NI: 进程的nice值,用于调整进程的优先级。
VIRT: 进程使用的虚拟内存大小(以KiB为单位)。
RES: 进程使用的物理内存大小(不包括交换空间)。
SHR: 进程使用的共享内存大小(以KiB为单位)。
S: 进程状态。通常有R(运行)、S(睡眠)、T(跟踪或停止)、Z(僵尸进程)等。
%CPU: 进程CPU使用百分比。
%MEM: 进程使用的物理内存百分比。
TIME+: 进程自启动以来的CPU使用时间。
Command: 运行的命令或程序名
2. 按H,获取每个线程的cpu使用情况
如下图,可以看到CPU占用最高的线程id是28882,注意此时最左侧一列展示的是线程id了,同时也包括top查询的进程id

3.将上面拿到的线程id转为十六进制,然后使用jstack查看线程信息
上面已经拿到了线程id是28882,将其转化为16进制是:70d2,
# 十进制转十六进制- 小写
printf "%x\n" 13
# 十进制转十六进制- 大写
printf "%X\n" 13
使用如下命令,查看对应进程中的线程信息
# 找出进程28771中 线程id为70d2的信息,-A 10 表示展示匹配行开始的10行信息,如果行数不够可以自行增加
jstack 28771|grep -A 10 70d2
展示信息如下,这里是一个sentinel的守护线程:

到这里我们就已经定位到CPU异常的线程了,如果这个线程有问题,通过这里是可以看到代码的情况的,也可以找到真正的java类和对应的代码
3.Jinfo
用于实时查看jvm或者系统参数,使用起来也很简单
3.1 使用jinfo查看jvm参数
# 根据进程号查看jvm参数
jinfo -flags 31963

这里可以看到展示了两块区域:
Non-default VM flags: 非默认的JVM参数配置,注意这里是和下面的Common line对应的信息,可以在这里观看我们配置的结果,这个信息不是默认,而是我们更改后的非默认值(Non-default VM flags名字写的很清楚了,有的人非要说是默认值)。
Command line: 命令行携带的JVM参数,也就是我们设置的信息
3.2 使用jinfo查看系统变量
jinfo -sysprops 31963
执行以后会列出当前的系统变量,有时项目的配置文件会使用系统变量,使用此命令可以一下看到系统变量的值。

4.Jstat
该命令可以实时观测JVM各个区域的情况以及GC的执行情况等,针对JVM的监控工具其实底层就是使用的jstat
jstat [-命令选项] [进程号] [间隔时间(毫秒)] [查询次数]
4.1 jstat查看gc的使用情况与内存情况
使用如下命令,就可以观测到实时的堆内存的使用情况与GC的执行情况,这样可以很好的判断我们参数设置的是否准确
jstat -gc 31963
S0C:第一个幸存区的大小,单位KB
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小(元空间)
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间,单位s
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间,单位s
GCT:垃圾回收消耗总时间,单位s
4.2 使用jstat查看堆使用情况
jstat -gccapacity 31963

NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0C:第一个幸存区大小
S1C:第二个幸存区的大小
EC:伊甸园区的大小
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:当前老年代大小
MCMN:最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代gc次数
FGC:老年代GC次数
4.3 新生代垃圾回收统计
jstat -gcnew 31963

S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
TT:对象在新生代存活的次数
MTT:对象在新生代存活的最大次数
DSS:期望的幸存区大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
4.4 新生代内存统计
jstat -gcnewcapacity 31963

NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0CMX:最大幸存1区大小
S0C:当前幸存1区大小
S1CMX:最大幸存2区大小
S1C:当前幸存2区大小
ECMX:最大伊甸园区大小
EC:当前伊甸园区大小
YGC:年轻代垃圾回收次数
FGC:老年代回收次数
4.5 老年代垃圾回收统计
jstat -gcold 31963

MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
OC:老年代大小
OU:老年代使用大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
4.6 老年代内存统计
jstat -gcoldcapacity 31963

OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:老年代大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
4.7 元数据空间统计
jstat -gcmetacapacity 31963

MCMN:最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
4.8 内存分布使用比例展示(快速查看概况)
jstat -gcutil 31963

S0:幸存1区当前使用比例
S1:幸存2区当前使用比例
E:伊甸园区使用比例
O:老年代使用比例
M:元数据区使用比例
CCS:压缩使用比例
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
注意:以上所有的命令都是可以支持持续观测的,这里以观测整体使用比例为例,假设间隔5000ms观测一次,总共观测10次,则命令如下:
jstat -gcutil 31963 5000 10
信息展示如下,这样就可以做到动态的监控了。

八、核心总结(JVM知识图谱)
1.JVM是Java的运行核心,核心价值是跨平台+自动内存管理;
2.运行时数据区是核心,线程私有无GC,线程共享是GC主战场;
3.类加载机制是基础,双亲委派模型保证类的安全和唯一性;
4.GC是灵魂,可达性分析+分代收集是核心,G1是目前最优的回收器;
5.调优是手段,先优化代码再调参数,核心目标是减少GC开销
更多推荐




所有评论(0)