1. JVM 整体架构
JVM 是什么
JVM(Java Virtual Machine)是一个运行 Java 字节码的虚拟计算机,它负责:加载 .class 文件、管理内存、执行字节码、进行垃圾回收,从而实现 Java 程序的跨平台运行。
重温一下JDK、JRE、JVM的关系:
JVM:负责运行 .class
JRE = JVM + Java 核心类库
JDK = JRE + 开发工具(javac、jps、jstack…)
JVM 核心架构图
下面对图中的四大组件逐一介绍:
类加载子系统(ClassLoader Subsystem)
作用是把 .class 文件加载进 JVM 内存
需加载的内容有:类信息、常量池、方法字节码、静态变量
三大类加载器:
运行时数据区
是 JVM 最核心的部分,包含有程序计数器(PC)、虚拟机栈、本地方法栈、堆、方法区(JDK8 以后是元空间)
线程私有的数据在PC与栈中,共享的数据在堆和方法区中。
执行引擎(Execution Engine)
作用是执行字节码
执行方式:
解释器:一行一行解释执行。启动快,执行慢。
JIT 编译器:热点代码编译成本地机器码。执行快。
JVM 是 解释 + 编译混合执行
GC 也是执行引擎的一部分,负责回收堆和方法区的垃圾
本地方法接口(JNI)
作用为调用非 Java 代码,例如C/C++代码和操作系统API
Java 无法直接操作底层硬件,或者一些性能敏感场景会用到,比如Thread.sleep、文件 IO、网络 IO 都可能用到 Native 方法
JVM 程序执行流程
编译:javac Hello.java → Hello.class
类加载:ClassLoader 加载 class
分配内存:类信息进方法区,对象进堆,栈帧进虚拟机栈
执行:解释器 / JIT 执行字节码
回收:GC 自动回收无用对象
2. JVM 内存结构
JVM 内存结构图

程序计数器
记录 当前线程 正在执行的 字节码指令地址,保证线程切换后能恢复到正确位置
是线程私有的,是唯一一个 不会 OOM 的区域,执行执行 Native 方法时 PC为空
虚拟机栈
是所有方法调用的“现场”,栈中有栈帧,每个栈帧包含局部变量表、操作数栈、动态链接、方法返回地址。栈帧生命周期短,当方法调用时入栈,当方法结束时出栈。
两个经典异常:
本地方法栈
作用为执行 native 方法,是线程私有的,与虚拟机栈类似
在 HotSpot JVM 中,本地方法栈和虚拟机栈实现上可能合并
堆(Heap)
是绝大多数情况下 Java 对象的唯一归宿,是线程共享的,也是JVM 内存最大的一块,由 GC 管理
通过逃逸分析,对象可以分配在栈上(JIT 优化)
JVM 在 JIT 编译阶段分析对象是否被返回,是否被赋给成员变量,是否被其他线程访问。如果确认对象不会逃逸当前方法 / 线程,JVM 可以将对象分配到栈上
堆的结构从图中拆分出来:
堆(Heap)
├── 新生代(Young)
│ ├── Eden
│ ├── Survivor S0
│ └── Survivor S1
└── 老年代(Old)对于新对象,会分配到Eden,当Minor GC 后会放至Survivor,达到阈值后迁移至老年代
为什么要分代?
因为大多数对象朝生夕死,新生代频繁回收使用复制算法,而老年代存活久就标记整理。如果不分代的话会使GC效率低下。
常见堆相关参数:
-Xms 初始堆
-Xmx 最大堆
-Xmn 新生代方法区
方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。在不同的虚拟机实现上,方法区的实现是不同的。
存放类元信息、运行时常量池、静态变量、JIT 编译代码
在JDK8以前堆中会有“永久代”,而在JDK8后由方法区的元空间实现,使用本地内存减少OOM风险
运行时常量池也属于方法区的一部分,存放字面量和常量引用
3. 对象创建 & 内存分配
对象创建的流程
五步流程:
1. 类是否已加载
2. 为对象分配内存
3. 初始化零值
4. 设置对象头
5. 执行 <init> 构造方法类是否已加载:
Object obj = new Object();JVM 会先检查Object 类是否被加载,是否完成加载 → 验证 → 准备 → 解析 → 初始化
为对象分配内存:
分配位置:
大多数分配到新生代Eden区
少数情况直接分配到老年代(大对象)
分配方式:
指针碰撞:内存连续,只需移动指针,速度快。适用于Serial / ParNew,或压缩整理后的内存。
空闲列表:内存不连续,维护可用块列表。适用于CMS或内存碎片场景。
初始化零值:
JVM 会把对象实例字段int赋为0,boolean赋为false,引用赋为null。保证 Java 层“未赋值变量也有默认值”。
设置对象头:
对象头是 JVM 的“身份证”,在HotSpot中由Mark Word(哈希、锁状态、GC 年龄)与Class Pointer(指向类元数据)组成。
执行构造方法 <init> :
显式初始化
构造器代码
父类构造先执行
对象内存布局
| 对象头 | 实例数据 | 对齐填充 |
为什么要对齐:
提高 CPU 访问效率
HotSpot 默认 8 字节对齐
对象的内存分配优化(TLAB)
为什么要TLAB?堆是线程共享;对象创建非常频繁;加锁会严重影响性能。
TLAB是指Thread Local Allocation Buffer,每个线程在 Eden 区拥有一小块私有内存
好处是无锁分配且极快
若TLAB用完可以重新申请或直接走公共 Eden 区
对象什么时候进入老年代
四种情况:
年龄到达阈值:默认 15 次 Minor GC
大对象:超过
-XX:PretenureSizeThreshold动态年龄判断:Survivor 区空间不足
Minor GC 后 Survivor 放不下
逃逸分析、栈上分配
是JVM在JIT阶段做的优化
4. 垃圾回收
为什么需要垃圾回收
Java承诺开发者不需要手动释放内存,JVM自动管理对象的生命周期。
GC的本质就是:自动回收“不再被使用的对象”,防止内存泄漏和 OOM
如何判断对象是否是垃圾
引用计数法
原理:
对象被引用一次,计数 +1
引用失效,计数 -1
计数 = 0 → 回收
这种方法存在致命缺陷:
a.ref = b;
b.ref = a;存在循环引用时永远无法回收
可达性分析(GC Roots)
这种方式是JVM采用的方式,从GC Roots出发,看对象是否可达
常见的GC Roots:
虚拟机栈中的引用
方法区中的静态变量
方法区中的常量
JNI 引用
Java的四种引用类型
决定对象什么时候被回收
垃圾回收算法
标记-清除(Mark-Sweep)
过程:
标记存活对象
清除垃圾
缺点是内存碎片且效率不稳定
标记-复制(Mark-Copy)
过程:将存活对象复制到另一块区域
特点是无碎片但浪费空间
新生代常用此算法
标记-整理(Mark-Compact)
过程:
标记存活对象
向另一端移动
老年代常用此算法
分代收集理论(GC的灵魂)
两个经典假设:
大多数对象朝生夕死
存活越久的对象越难死
所以:
新生代:频繁GC,复制算法
老年代:低频GC,整理算法
GC类型
垃圾收集器
Serial/ParNew
单线程/多线程
STW(Stop The World)
CMS(经典低停顿)
特点:
并发标记、并发清除
停顿时间短
缺点:
内存碎片
CPU资源占用高
可能 Concurrent Mode Failure
CMS已逐步淘汰
G1
核心思想:化整为零;可预测停顿时间
关键概念:
Region
Remembered Set
Mixed GC
为什么G1适合大堆?
不再严格分代
局部回收
停顿可控
ZGC
亚毫秒停顿
大内存
JDK11+
GC触发条件
Minor GC
Eden 满
Full GC 常见原因
老年代空间不足
Metaspace OOM
显式调用 System.gc()
GC overhead limit exceeded
GC 日志 & 线上排查
常用参数:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xlog:gc频繁 Full GC 怎么排查?
看 GC 日志 →
看堆使用情况 →
是否大对象 / 内存泄漏 →
调整参数 / 代码
5. 类加载机制
什么是类加载机制
类加载机制是 JVM 把 .class 文件加载进内存,并转换成可以被 JVM 使用的 Class 对象的全过程。
核心目的:将 字节码 → 内存中的 Class 结构,为后续 对象创建、方法调用 做准备
Class的来源
Class 文件可能来自:
.class文件jar / war
网络(远程加载)
动态生成(ASM、CGLIB)
JSP 编译结果
类加载机制是开放的,不局限于磁盘文件
类加载的生命周期
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
加载
通过类的全限定名获取字节码
将字节码转为方法区中的数据结构
在堆中生成 java.lang.Class 对象
验证
保证字节码是“安全、合法的”,这是 JVM 防止恶意代码的重要屏障
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
为类变量(static)分配内存并赋“默认值”
解析
把符号引用转为直接引用
符号引用:字符串描述
直接引用:内存地址 / 偏移量
初始化
真正执行 Java 代码的阶段
执行
<clinit>方法给 static 变量赋“程序员写的值”
初始化触发条件(类的“主动使用”才会初始化):
new 对象
访问 static 变量(非 final)
调用 static 方法
反射
启动类(main)
类加载器
类加载器负责加载,不负责验证/初始化
双亲委派模型
类加载请求,先交给父加载器
Application
↑
Extension
↑
Bootstrap为什么需要双亲委派模型?
防止核心类被篡改
避免类的重复加载
如何打破双亲委派?
常见场景:
SPI(ServiceLoader)
Tomcat
OSGi
热部署
方式:
自定义 ClassLoader
重写 loadClass()
Tomcat类加载机制
Tomcat为什么能部署多个Web应用?
核心原因:每个 WebApp 一个 ClassLoader
好处
类隔离
支持热部署
不冲突依赖
类卸载
条件:
Class 对象无引用
ClassLoader 被回收
无实例对象
类卸载非常苛刻,很少发生
6. JVM 性能调优 & 工具
JVM调优流程:
现象 → 定位 → 原因 → 解决 → 验证
什么时候需要 JVM 调优?
不是所有系统都需要 JVM 调优
典型场景:
频繁 Full GC
接口 RT 波动大
吞吐量低
CPU 100%
OOM
系统重启后很快又出问题
JVM 核心性能指标(调优目标)
JVM 常用参数体系
堆相关参数(最重要)
-Xms 初始堆
-Xmx 最大堆
-Xmn 新生代大小GC 相关参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200元空间
-XX:MetaspaceSize
-XX:MaxMetaspaceSize诊断参数(必会)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/pathJVM工具体系
jps —— 看JVM进程
jps -l找进程号
jstat —— GC & 内存实时状态
jstat -gc pid 1000YGC / FGC 次数
Eden / Old 使用率
jmap —— 堆快照(OOM 核心)
jmap -dump:format=b,file=heap.hprof pid分析内存泄漏
找大对象
jstack —— 线程分析(CPU 飙高)
jstack pid死锁
死循环
线程阻塞
Arthas —— 不重启线上服务排查问题
常用命令
dashboardthreadheapdumpwatchtrace
典型线上问题 & 调优思路
JVM 调优通常从 GC 日志和监控入手,结合 jstat、jmap、jstack 等工具定位问题,分析是内存泄漏、大对象还是参数不合理,最终通过代码优化或参数调整解决。
频繁Full GC
看 GC 日志
看老年代占用
是否大对象 / 内存泄漏
调整堆 / 代码
OOM
Dump 堆
MAT / VisualVM 分析
定位引用链
CPU 100%
top找高 CPU 线程转成 16 进制
jstack定位代码
7. 资料
《深入理解 Java 虚拟机》
《Java 性能权威指南》
Arthas
JVisualVM