深入理解 JVM:从核心原理到实战应用
文章目录
- [深入理解 JVM:从核心原理到实战应用](#深入理解 JVM:从核心原理到实战应用)
-
- [一、JVM 是什么?核心定位与价值](#一、JVM 是什么?核心定位与价值)
-
- [JVM 的核心价值](#JVM 的核心价值)
- [二、JVM 核心架构:五大核心模块](#二、JVM 核心架构:五大核心模块)
-
- [1. 类加载子系统](#1. 类加载子系统)
- [2. 运行时数据区(JVM 内存模型)](#2. 运行时数据区(JVM 内存模型))
-
- [(1) JVM 内存区域的核心划分](#(1) JVM 内存区域的核心划分)
- [(2) 线程私有区域(每个线程独有)](#(2) 线程私有区域(每个线程独有))
-
- [1. 程序计数器(Program Counter Register)](#1. 程序计数器(Program Counter Register))
- [2. 虚拟机栈(Java Virtual Machine Stack)](#2. 虚拟机栈(Java Virtual Machine Stack))
- [3. 本地方法栈(Native Method Stack)](#3. 本地方法栈(Native Method Stack))
- [(3) 线程共享区域(所有线程共用)](#(3) 线程共享区域(所有线程共用))
-
- [1. 堆(Heap)](#1. 堆(Heap))
- [2. 方法区(Method Area)](#2. 方法区(Method Area))
- [3. 执行引擎](#3. 执行引擎)
- [4. 本地方法接口(JNI)](#4. 本地方法接口(JNI))
- [5. 垃圾回收器(GC)](#5. 垃圾回收器(GC))
-
- 1)垃圾判定标准
- [2)常见 GC 收集器(JDK 8 默认 Parallel Scavenge + Parallel Old)](#2)常见 GC 收集器(JDK 8 默认 Parallel Scavenge + Parallel Old))
- 6.各个模块之间的关联:
- [7.JVM 内存区域在程序运行时的**执行流程**](#7.JVM 内存区域在程序运行时的执行流程)
-
- 核心前提
- [(1) 完整执行流程(结合代码示例)](#(1) 完整执行流程(结合代码示例))
-
- [步骤 1:JVM 启动,类加载阶段(内存区域:元空间 + 堆)](#步骤 1:JVM 启动,类加载阶段(内存区域:元空间 + 堆))
- [步骤 2:主线程执行 main 方法(内存区域:PC + 虚拟机栈)](#步骤 2:主线程执行 main 方法(内存区域:PC + 虚拟机栈))
- [步骤 3:创建对象(内存区域:堆 + 虚拟机栈)](#步骤 3:创建对象(内存区域:堆 + 虚拟机栈))
- [步骤 4:调用 concat 方法(内存区域:PC + 虚拟机栈 + 堆)](#步骤 4:调用 concat 方法(内存区域:PC + 虚拟机栈 + 堆))
- [步骤 5:调用 System.out.println(内存区域:本地方法栈 + JNI)](#步骤 5:调用 System.out.println(内存区域:本地方法栈 + JNI))
- [步骤 6:GC 后台工作(内存区域:堆 + 元空间)](#步骤 6:GC 后台工作(内存区域:堆 + 元空间))
- [步骤 7:程序结束(内存区域释放)](#步骤 7:程序结束(内存区域释放))
- [(2) 流程核心可视化(Mermaid 时序图)](#(2) 流程核心可视化(Mermaid 时序图))
- [(3) 关键细节补充](#(3) 关键细节补充)
- [三、JVM 实战:核心调优参数与工具](#三、JVM 实战:核心调优参数与工具)
-
- [1. 核心调优参数(JDK 8+)](#1. 核心调优参数(JDK 8+))
- [2. 常用调优工具](#2. 常用调优工具)
- [四、JVM 常见问题与解决方案](#四、JVM 常见问题与解决方案)
- 五、总结
作为 Java 开发者,JVM(Java Virtual Machine,Java 虚拟机)是我们日常开发中绕不开的核心基础。它不仅是 Java"一次编写,到处运行" 跨平台特性的基石,更是影响程序性能、稳定性的关键所在。本文将从 JVM 的核心概念、架构组成、核心机制到实战调优,全方位拆解 JVM,帮助大家真正理解并用好 JVM。
一、JVM 是什么?核心定位与价值
你可以把 JVM 理解为一个 "虚拟的计算机"------ 它是运行在操作系统之上的软件层,专门负责执行 Java 字节码(.class 文件)。Java 源码编译后生成的不是机器码,而是字节码,JVM 负责将字节码解释 / 编译为对应操作系统的机器指令,这也是 Java 跨平台的核心原因:不同操作系统有对应的 JVM 实现,而字节码无需修改。
JVM 的核心价值
- 跨平台性:一套字节码,Windows/Linux/Mac 等系统的 JVM 均可执行;
- 自动内存管理:无需手动分配 / 释放内存(GC 垃圾回收),降低内存泄漏风险;
- 安全性:通过类加载验证、沙箱机制等保障程序运行安全;
- 性能优化:JIT(即时编译器)将热点代码编译为机器码,提升执行效率。
二、JVM 核心架构:五大核心模块
JVM 的整体架构可拆解为 5 个核心部分,下图清晰展示了各模块的关系:
JVM架构
类加载子系统
运行时数据区
执行引擎
本地方法接口JNI
垃圾回收器GC
程序计数器
虚拟机栈
本地方法栈
堆
方法区
1. 类加载子系统
负责将.class 文件加载到 JVM 中,核心流程分为加载、验证、准备、解析、初始化 5 步:
- 加载:通过类的全限定名读取字节码文件,生成 Class 对象;
- 验证:校验字节码合法性(如文件格式、语义、字节码指令等),防止恶意代码;
- 准备:为类的静态变量分配内存并设置默认初始值(如 int 默认 0,引用默认 null);
- 解析:将符号引用替换为直接引用(如将类名替换为内存地址);
- 初始化:执行静态代码块、为静态变量赋值,是类加载的最后一步。
类加载的核心原则:双亲委派模型(避免类重复加载、保证核心类安全),即加载类时先委托父类加载器加载,父类加载器无法加载时才由子类加载器自行加载。
2. 运行时数据区(JVM 内存模型)
(1) JVM 内存区域的核心划分
JVM 在运行 Java 程序时,会把内存划分为不同的区域,核心目的是隔离不同类型的数据,方便管理和回收。这些区域整体分为两大类:
- 线程私有区域:每个线程独立拥有,线程结束后自动释放,无需 GC;
- 线程共享区域:所有线程共用,只有 JVM 退出或 GC 时才会释放,是内存泄漏 / 溢出的高发区。
下面逐一拆解每个区域:
(2) 线程私有区域(每个线程独有)
1. 程序计数器(Program Counter Register)
核心作用:记录当前线程正在执行的字节码指令的行号(可以理解为 "线程的执行进度条")。
- 线程切换时,JVM 会记录每个线程的程序计数器值,切换回来后能精准恢复执行位置;
- 如果线程执行的是Java 方法 ,计数器存字节码行号;如果是Native 方法 (如 JNI 调用的 C/C++ 方法),计数器值为
undefined。
特点:
- 是 JVM 中唯一不会抛出 OutOfMemoryError(OOM) 的区域;
- 占用内存极小,几乎可以忽略。
补: Java 中的 OutOfMemoryError(OOM),简单来说这是 Java 程序运行时因内存不足而抛出的严重错误,而非普通异常,会直接导致程序崩溃。
2. 虚拟机栈(Java Virtual Machine Stack)
核心作用:存储方法调用的 "栈帧"(Stack Frame),每个方法从调用到执行完成,对应一个栈帧的入栈和出栈。
- 栈帧包含:局部变量表(存储方法内的局部变量)、操作数栈(方法执行的临时数据区)、方法返回地址(执行完回到哪里)等。
关键特点:
- 栈的大小是固定的(可通过
-Xss参数设置,如-Xss1M); - 方法嵌套调用过深(如无限递归)会导致栈帧过多,抛出
StackOverflowError(栈溢出错误); - 若 JVM 无法为新线程分配栈空间,会抛出
OutOfMemoryError(极少出现)。
示例:无限递归导致栈溢出
StackOverflowError最常见的原因:方法调用的层级无限加深,导致虚拟机栈中不断压入新的栈帧,最终超出 JVM 对虚拟机栈的内存限制(默认栈深度约 1000-2000 层)。
其中,无限递归调用 是触发这个错误的最典型场景。
java
public class StackOverflowDemo {
public static void recursive() {
recursive(); // 无限递归调用
}
public static void main(String[] args) {
recursive(); // 执行后会抛出 StackOverflowError
}
}
补:栈帧是 JVM 为每一次方法调用分配的独立内存单元,也是 Java 虚拟机栈(JVM Stack)的最小组成单位。你可以把它理解为:每个方法在执行时的''专属工作台'' ------ 方法执行需要的所有数据、计算空间、执行记录都存在这个 "工作台" 里,方法执行完,这个 "工作台" 就会被销毁。
Java 中的 StackOverflowError(栈溢出错误),这是和栈帧、虚拟机栈直接相关的典型错误,简单来说:它是当 JVM 虚拟机栈中栈帧的数量超出了栈的最大容量限制时抛出的错误,会直接导致方法调用链中断、程序崩溃。
3. 本地方法栈(Native Method Stack)
核心作用 :和虚拟机栈功能类似,但专门为Native 方法 (用 C/C++ 编写、通过 JNI 调用的方法,如System.currentTimeMillis()底层)提供栈空间。
特点:
- 同样会抛出
StackOverflowError和OutOfMemoryError; - 不同 JVM 实现(如 HotSpot)会把虚拟机栈和本地方法栈合并实现。
补: Java 中的 System.currentTimeMillis() 方法,简单来说,这个方法的核心作用是返回当前时间与 1970 年 1 月 1 日 00:00:00 UTC(格林威治标准时间)之间的毫秒数,是 Java 中获取时间戳最常用、最高效的方式。
什么是HotSpot?
简单来说,HotSpot 是 Oracle/Sun 官方默认的 Java 虚拟机(JVM)实现,也是目前使用最广泛的 JVM,我们日常开发、运行 Java 程序时,底层默认用的就是它。

(3) 线程共享区域(所有线程共用)
1. 堆(Heap)
核心作用:
JVM 中最大的内存区域,专门存储对象实例和数组(几乎所有 new 出来的东西都存在这里)。例:new String("hello"),字符串对象存在堆中,引用(变量)存在虚拟机栈中。
关键特点:
- 是垃圾回收(GC)的核心区域(平时说的 GC 主要就是回收堆里的无用对象);
- 堆的大小可通过 JVM 参数调整:
-Xms(初始堆大小,如-Xms512M)、-Xmx(最大堆大小,如-Xmx1G); - 堆内存不足时,会抛出
OutOfMemoryError: Java heap space。
堆的细分(便于 GC):
为了提高 GC 效率,堆又分为 "新生代" 和 "老年代"(JDK8 及以后):
- 新生代:存储新创建的对象,GC 频率高(Minor GC),又分为 Eden 区、Survivor0 区、Survivor1 区;
- 老年代:存储存活时间长的对象,GC 频率低(Major GC/Full GC);
- 元空间(Metaspace):JDK8 替代了原来的 "方法区",不在堆内,而是使用本地内存。
2. 方法区(Method Area)
核心作用:存储类的元数据(类的全限定名、字段、方法、常量池等)、静态变量、即时编译器(JIT)编译后的代码。
关键特点:
- JDK7 及以前叫 "永久代"(PermGen),属于堆的一部分;JDK8 及以后改为 "元空间(Metaspace)",使用操作系统的本地内存;
- 方法区 / 元空间不足时,抛出
OutOfMemoryError: Metaspace(JDK8)或OutOfMemoryError: PermGen space(JDK7); - 常量池(如字符串常量池)在 JDK7 从方法区移到堆中,JDK8 后元空间仅存类元数据。
示例:元空间溢出(JDK8)
java
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
// 需要引入cglib依赖:<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version></dependency>
public class MetaspaceOOMDemo {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMClass.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create(); // 动态生成大量类,耗尽元空间
}
}
static class OOMClass {}
}
运行时添加参数-XX:MaxMetaspaceSize=10M,会快速抛出OutOfMemoryError: Metaspace。

这是 JVM 最核心的部分,也是面试 / 调优的重点,JVM 运行时会将内存划分为以下区域:
| 区域 | 作用 | 线程私有 / 共享 | 常见问题 |
|---|---|---|---|
| 程序计数器 | 记录当前线程执行的字节码行号,线程切换时恢复执行位置 | 私有 | 无(唯一不会 OOM 的区域) |
| 虚拟机栈 | 存储方法调用的栈帧(局部变量、操作数栈、方法返回地址等) | 私有 | StackOverflowError(栈溢出) |
| 本地方法栈 | 为 native 方法(本地方法,如调用 C/C++ 代码)提供栈空间 | 私有 | StackOverflowError |
| 堆 | 存储对象实例和数组,是 GC 垃圾回收的主要区域 | 共享 | OutOfMemoryError(OOM) |
| 方法区 | 存储类信息、常量、静态变量、即时编译器编译后的代码等 | 共享 | OutOfMemoryError |
3. 执行引擎
JVM 的 "CPU" ,负责将运行时数据区中加载的Java 字节码解释 / 编译为**底层操作系统能识别的机器码 **,并执行最终的机器指令。即负责执行运行时数据区中的字节码,核心执行方式有两种:
解释执行:逐行解释字节码为机器码并执行,启动快、执行慢;
JIT 编译执行:识别 "热点代码"(频繁执行的代码),一次性编译为机器码缓存,后续直接执行,执行效率高。
对应的子组件介绍:
解释器(Interpreter):
- 工作方式:逐行解释字节码为机器码并执行,无需编译整段代码;
- 特点:启动快、执行慢,适合程序启动初期(无热点代码时),保证 Java 的跨平台性(不同系统的解释器适配不同机器码);
- 不足:逐行解释存在重复编译,执行效率低。
即时编译器(JIT Compiler):
- 工作方式:通过热点探测 识别频繁执行的代码(热点代码,如循环、高频调用的方法) ,将整段热点字节码一次性编译为机器码并缓存,后续执行直接调用缓存的机器码;
- 特点:启动慢、执行快,弥补解释器的效率短板,是 JVM 性能优化的核心;
- 分类(HotSpot):C1 编译器(客户端编译器,轻量编译,追求启动速度)、C2 编译器(服务端编译器,深度编译,追求执行效率)、Graal 编译器(JDK10+,新一代即时编译器)。
HotSpot 采用混合执行模式 :程序启动时用解释器快速执行,JIT 后台编译热点代码,后续自动切换为编译执行,兼顾启动速度 和运行效率。
4. 本地方法接口(JNI)
作为 JVM 与本地操作系统 / 本地方法库(如 C/C++ 库)的桥梁,让 Java 程序能调用非 Java 代码,比如 Java IO、网络编程底层都会通过 JNI 调用系统接口。
工作流程
- Java 代码中通过
native关键字声明本地方法(如public native long currentTimeMillis();); - JVM 通过 JNI 找到该本地方法对应的 C/C++ 实现库(.so/.dll 文件);
- 执行引擎通过本地方法栈为本地方法分配执行空间,调用本地库的机器码;
- 本地方法执行完成后,通过 JNI 将结果返回给 Java 程序。
关键特点
- 弥补 Java 的底层操作能力不足,实现与操作系统的交互;
- 本地方法的执行不受 JVM 管理(如内存分配),需开发者手动管理,容易引发内存泄漏;
- 核心类库(如 java.io、java.net)大量使用 JNI,实现跨平台的底层功能封装。
5. 垃圾回收器(GC)
JVM 的 "内存清洁工" ,自动检测并回收运行时数据区中**不再被引用的对象 **,释放堆和方法区(元空间)的无用内存,避免内存溢出,无需程序员手动管理内存(对比 C/C++ 的 malloc/free)。
1)垃圾判定标准
- 引用计数法:给对象加引用计数器,引用 + 1,引用释放 - 1,计数器为 0 则为垃圾(缺点:无法解决循环引用);
- 可达性分析算法:以 GC Roots(如虚拟机栈引用的对象、类静态变量引用的对象等)为起点,遍历对象引用链,不可达的对象为垃圾(JVM 默认使用)。
2)常见 GC 收集器(JDK 8 默认 Parallel Scavenge + Parallel Old)
| 收集器 | 适用区域 | 特点 | 适用场景 |
|---|---|---|---|
| Serial GC | 新生代 | 单线程 GC,暂停时间长 | 单核 CPU、小型应用 |
| ParNew GC | 新生代 | 多线程 GC,配合 CMS 使用 | 多核 CPU、追求低延迟 |
| Parallel GC | 新生代 | 多线程 GC,追求高吞吐量 | 后台运算、批处理程序 |
| CMS GC | 老年代 | 并发 GC,暂停时间短(低延迟) | 响应时间敏感的应用(如 Web) |
| G1 GC | 堆整体 | 分区回收,兼顾吞吐量和低延迟 | JDK 9 + 默认、大内存应用 |
| ZGC/Shenandoah | 堆整体 | 几乎无暂停,超高吞吐 | 超大内存(TB 级)、低延迟 |
6.各个模块之间的关联:
Java 程序的完整执行流程如下,清晰体现各模块的协同关系:
- 编写与编译:开发者编写.java 源文件,通过 javac 编译器编译为与平台无关的.class 字节码文件;
- 类加载:JVM 启动后,类加载子系统按双亲委派模型加载.class 文件,完成验证、准备、解析、初始化,将类元数据存入方法区,堆中生成 Class 对象;
- 内存分配:程序执行时,通过 new 创建对象 / 数组,JVM 在堆中为其分配内存,虚拟机栈中生成对象引用;
- 字节码执行:执行引擎先通过解释器逐行解释字节码为机器码执行,同时后台探测热点代码,由 JIT 编译器编译为机器码并缓存;
- 本地方法调用:若执行 native 方法,通过 JNI 调用底层 C/C++ 本地库,本地方法栈为其分配执行空间;
- 内存回收:GC 后台持续运行,通过可达性分析标记堆中的垃圾对象,在合适时机执行回收,释放无用内存;
- 程序结束:Java 程序执行完毕,JVM 退出,释放所有内存资源。
模块关联图:
管理
类加载子系统
Class Loader
运行时数据区
Runtime Data Area
执行引擎
Execution Engine
本地方法接口
JNI
垃圾回收器
GC
7.JVM 内存区域在程序运行时的执行流程
核心前提
先明确两个基础概念,帮你理解流程:
- 内存区域分工 :线程私有区域(PC、虚拟机栈、本地方法栈)负责方法执行的上下文 ,线程共享区域(堆、元空间)负责数据存储;
- 核心流程逻辑:代码执行 = 方法调用(栈帧入栈 / 出栈) + 数据存储(堆存对象、元空间存类信息) + 执行引擎解释 / 编译字节码 + GC 后台回收无用内存。
(1) 完整执行流程(结合代码示例)
以一段简单的 Java 代码为例,拆解每一步内存区域的行为:
java
public class JvmMemoryDemo {
// 静态变量(存元空间)
private static String STATIC_STR = "静态字符串";
// 成员变量(存堆中对象实例)
private String memberStr;
public JvmMemoryDemo(String memberStr) {
this.memberStr = memberStr; // 构造方法赋值
}
// 普通方法
public String concat(String param) {
String localStr = "局部字符串"; // 局部变量(存虚拟机栈)
return localStr + param + this.memberStr;
}
public static void main(String[] args) {
// 主线程执行入口
JvmMemoryDemo demo = new JvmMemoryDemo("成员字符串"); // 创建对象
String result = demo.concat("参数字符串"); // 调用方法
System.out.println(result); // 调用native方法(本地方法栈+JNI)
}
}
步骤 1:JVM 启动,类加载阶段(内存区域:元空间 + 堆)
- 执行
java JvmMemoryDemo,JVM 启动并创建主线程; - 类加载子系统按双亲委派模型加载JvmMemoryDemo.class:
- 加载:读取
.class字节码,将类元数据(类名、方法、字段、常量池等)存入元空间(方法区); - 链接:验证字节码合法性 → 准备阶段为静态变量
STATIC_STR分配内存并赋默认值null→ 解析阶段将符号引用(如System.out)转为直接引用; - 初始化:执行
<clinit>()方法,为STATIC_STR赋初始值"静态字符串"(字符串常量存堆的字符串常量池);
- 加载:读取
- 同时,堆中生成
JvmMemoryDemo的Class对象(作为元空间类元数据的引用)。
步骤 2:主线程执行 main 方法(内存区域:PC + 虚拟机栈)
- 程序计数器(PC):主线程的 PC 寄存器记录
main方法的第一条字节码指令行号; - 虚拟机栈:为main方法创建栈帧并入栈,栈帧包含:
- 局部变量表:存储
args(方法参数)、demo(对象引用)、result(字符串引用)等局部变量; - 操作数栈:方法执行时的临时数据计算区;
- 方法返回地址:记录执行完
main后回到哪里(JVM 退出)。
- 局部变量表:存储
步骤 3:创建对象(内存区域:堆 + 虚拟机栈)
执行new JvmMemoryDemo("成员字符串"):
- 执行引擎在堆 中 为 JvmMemoryDemo 对象分配内存:
- 存储成员变量
memberStr(值为"成员字符串",字符串常量存堆的常量池); - 对象头(存储哈希值、GC 分代年龄等);
- 存储成员变量
- 虚拟机栈的
main方法栈帧中,demo变量存储该对象在堆中的内存地址(引用); - 调用构造方法:为构造方法创建栈帧入栈,执行赋值逻辑后栈帧出栈(构造方法执行完毕)。
步骤 4:调用 concat 方法(内存区域:PC + 虚拟机栈 + 堆)
执行demo.concat("参数字符串"):
- PC 寄存器:更新为
concat方法的第一条字节码指令行号; - 虚拟机栈:为concat方法创建栈帧入栈,栈帧中:
- 局部变量表:存储
this(当前对象引用)、param(参数引用)、localStr(局部变量引用); - 操作数栈:执行字符串拼接的临时计算(
localStr + param + this.memberStr);
- 局部变量表:存储
- 拼接后的新字符串对象在堆 中创建,
concat方法返回该对象的引用; concat方法执行完毕,其栈帧出栈,PC 寄存器回到main方法的下一条指令行号;main方法的result变量存储拼接后字符串的堆内存引用。
步骤 5:调用 System.out.println(内存区域:本地方法栈 + JNI)
执行System.out.println(result):
println方法底层调用native 方法 (如write系统调用);- JVM 为该 native 方法在本地方法栈分配栈帧;
- 通过JNI(本地方法接口) 加载底层 C/C++ 本地库,调用操作系统的 IO 接口;
- 执行完毕后,本地方法栈帧出栈,回到
main方法。
步骤 6:GC 后台工作(内存区域:堆 + 元空间)
整个执行过程中,GC 始终在后台运行:
- 可达性分析:以 GC Roots(如虚拟机栈中的
demo、result引用,元空间的Class对象)为起点,标记堆中存活的对象; - 垃圾回收:若有临时创建的无用对象(如拼接过程中产生的临时字符串),在合适时机(如新生代内存不足触发 Minor GC)回收,释放堆内存;
- 元空间仅在类被判定为 "无用类" 时(极少发生)才会回收类元数据。
步骤 7:程序结束(内存区域释放)
main方法执行完毕,其栈帧出栈,主线程的虚拟机栈、PC 寄存器、本地方法栈随线程销毁自动释放;- JVM 退出,堆和元空间的所有内存被操作系统回收。
(2) 流程核心可视化(Mermaid 时序图)
用时序图清晰展示各内存区域的交互顺序:
堆/元空间 JVM GC 本地方法栈/JNI 元空间 堆 虚拟机栈 PC寄存器 主线程 堆/元空间 JVM GC 本地方法栈/JNI 元空间 堆 虚拟机栈 PC寄存器 主线程 JVM启动,加载JvmMemoryDemo类 执行main方法 创建JvmMemoryDemo对象 调用concat方法 调用println(native方法) 后台持续运行 main方法执行完毕 存储类元数据、静态变量STATIC_STR 创建JvmMemoryDemo的Class对象 记录main方法字节码行号 main方法栈帧入栈 分配对象内存,存储memberStr 返回对象引用(demo变量) 更新为concat方法行号 concat方法栈帧入栈 创建拼接后的字符串对象 返回字符串引用(result变量) concat栈帧出栈 分配native方法栈帧,调用底层IO native方法执行完毕,栈帧出栈 可达性分析,标记存活对象 回收无用临时对象,释放内存 main栈帧出栈 线程销毁,寄存器释放 退出,释放所有共享内存
(3) 关键细节补充
- 栈帧的核心作用:每个方法对应一个栈帧,栈帧的入栈 / 出栈是方法调用 / 结束的本质,栈帧中存储的局部变量表是方法内变量的 "临时存储区";
- 引用与对象的关系 :虚拟机栈中存储的是对象引用(内存地址),真正的对象数据在堆中,这是 Java "值传递" 的核心(传递的是引用的值);
- GC 的触发时机:
- Minor GC:新生代(Eden 区)内存不足时触发,回收新生代无用对象,速度快;
- Major GC/Full GC:老年代内存不足时触发,回收老年代 + 新生代,STW 时间长;
- 元空间的特殊性 :JDK8 后元空间使用本地内存,默认无上限(可通过
-XX:MaxMetaspaceSize限制),溢出时抛出OutOfMemoryError: Metaspace。
三、JVM 实战:核心调优参数与工具
1. 核心调优参数(JDK 8+)
| 参数 | 作用 | 示例 |
|---|---|---|
| -Xms | 堆初始内存(建议与 - Xmx 相同,避免内存扩容) | -Xms2G |
| -Xmx | 堆最大内存 | -Xmx4G |
| -Xmn | 新生代内存大小 | -Xmn1G |
| -XX:SurvivorRatio | 新生代 Eden/Survivor 比例 | -XX:SurvivorRatio=8 |
| -XX:+UseG1GC | 使用 G1 收集器 | -XX:+UseG1GC |
| -XX:+PrintGCDetails | 打印 GC 详细日志 | -XX:+PrintGCDetails |
| -XX:MetaspaceSize | 元空间初始大小 | -XX:MetaspaceSize=256M |
| -XX:MaxMetaspaceSize | 元空间最大大小 | -XX:MaxMetaspaceSize=512M |
2. 常用调优工具
jps:查看运行中的 Java 进程;
jstat :监控 JVM 内存和 GC 状态(如jstat -gc 12345 1000,每 1 秒打印进程 12345 的 GC 信息);
jmap :导出堆快照、查看内存使用(如jmap -dump:format=b,file=heap.hprof 12345);
jhat:分析堆快照文件;
jvisualvm/jconsole:图形化监控工具,可视化查看内存、GC、线程状态;
Arthas:阿里开源的 JVM 诊断工具,支持实时监控、反编译、调优,上手简单。
四、JVM 常见问题与解决方案
1.OOM(OutOfMemoryError):
- 堆 OOM:检查是否内存泄漏(如未关闭连接、集合缓存过大),调大 - Xmx,优化 GC;
- 元空间 OOM:调大 MaxMetaspaceSize,检查是否频繁加载类(如动态代理未释放);
- 栈 OOM:调大 - Xss(栈大小),检查是否递归调用过深。
2.GC 频繁 / Full GC 过多:
- 新生代过小:调大 - Xmn,减少 Minor GC 频率;
- 老年代内存泄漏:排查大对象、长期存活对象;
- 收集器选择不当:替换为 G1GC,优化 GC 参数。
3.程序启动慢:
- 关闭 JIT 预热(-XX:-TieredCompilation),或调整 JIT 编译阈值;
- 优化类加载(减少无用依赖、懒加载类)。
五、总结
JVM 是 Java 的核心基石,理解 JVM 不仅能帮助我们解决性能问题,更能提升代码编写的质量。核心要点总结:
-
JVM 的核心价值是跨平台、自动内存管理,核心架构包含类加载子系统、运行时数据区、执行引擎、JNI、GC 五大模块;
-
运行时数据区是调优核心,堆分为新生代和老年代,GC 主要回收堆中垃圾,不同收集器适用于不同场景;
-
JVM 调优的核心思路是:先通过工具定位问题(内存泄漏 / GC 频繁),再调整内存参数 / 更换收集器,最后验证效果。
-
JVM 内存区域的执行流程核心是:类加载(元空间存类信息)→ 方法调用(栈帧入栈 / 出栈)→ 对象创建(堆存数据)→ 本地方法调用(本地方法栈 / JNI)→ GC 回收(堆内存)→ 线程销毁(私有区域释放)→ JVM 退出(共享区域释放);
-
线程私有区域(PC、虚拟机栈、本地方法栈)随线程生命周期销毁,无需 GC;共享区域(堆、元空间)由 GC 管理,是内存问题的核心区域;
-
代码执行的本质是 "栈帧的流转"+"堆数据的读写",理解这一点就能掌握 JVM 内存执行的核心逻辑。