1. JVM 整体架构

JVM 是什么

JVM(Java Virtual Machine)是一个运行 Java 字节码的虚拟计算机,它负责:加载 .class 文件、管理内存、执行字节码、进行垃圾回收,从而实现 Java 程序的跨平台运行。

重温一下JDK、JRE、JVM的关系:

JVM:负责运行 .class

JRE = JVM + Java 核心类库

JDK = JRE + 开发工具(javac、jps、jstack…)

JVM 核心架构图

一文搞懂 JVM 架构和运行时数据区 (内存区域)-CSDN博客

下面对图中的四大组件逐一介绍:

类加载子系统(ClassLoader Subsystem)

作用是把 .class 文件加载进 JVM 内存

需加载的内容有:类信息、常量池、方法字节码、静态变量

三大类加载器:

加载器

作用

Bootstrap

加载核心类(rt.jar)

Extension

扩展类

Application

用户类

运行时数据区

是 JVM 最核心的部分,包含有程序计数器(PC)、虚拟机栈、本地方法栈、堆、方法区(JDK8 以后是元空间)

线程私有的数据在PC与栈中,共享的数据在堆和方法区中。

执行引擎(Execution Engine)

作用是执行字节码

执行方式:

  1. 解释器:一行一行解释执行。启动快,执行慢。

  2. JIT 编译器:热点代码编译成本地机器码。执行快。

JVM 是 解释 + 编译混合执行

GC 也是执行引擎的一部分,负责回收堆和方法区的垃圾

本地方法接口(JNI)

作用为调用非 Java 代码,例如C/C++代码和操作系统API

Java 无法直接操作底层硬件,或者一些性能敏感场景会用到,比如Thread.sleep、文件 IO、网络 IO 都可能用到 Native 方法

JVM 程序执行流程

  1. 编译:javac Hello.java → Hello.class

  2. 类加载:ClassLoader 加载 class

  3. 分配内存:类信息进方法区,对象进堆,栈帧进虚拟机栈

  4. 执行:解释器 / JIT 执行字节码

  5. 回收:GC 自动回收无用对象

2. JVM 内存结构

JVM 内存结构图

程序计数器

记录 当前线程 正在执行的 字节码指令地址,保证线程切换后能恢复到正确位置

是线程私有的,是唯一一个 不会 OOM 的区域,执行执行 Native 方法时 PC为空

虚拟机栈

是所有方法调用的“现场”,栈中有栈帧,每个栈帧包含局部变量表、操作数栈、动态链接、方法返回地址。栈帧生命周期短,当方法调用时入栈,当方法结束时出栈。

两个经典异常:

异常

场景

StackOverflowError

递归太深

OutOfMemoryError

栈内存不足

本地方法栈

作用为执行 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 区

对象什么时候进入老年代

四种情况:

  1. 年龄到达阈值:默认 15 次 Minor GC

  2. 大对象:超过 -XX:PretenureSizeThreshold

  3. 动态年龄判断:Survivor 区空间不足

  4. 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的四种引用类型

决定对象什么时候被回收

引用类型

回收时机

强引用

永不(OOM 才回收)

软引用

内存不足

弱引用

下次 GC

虚引用

随时(用于通知)

垃圾回收算法

标记-清除(Mark-Sweep)

过程:

标记存活对象

清除垃圾

缺点是内存碎片且效率不稳定

标记-复制(Mark-Copy)

过程:将存活对象复制到另一块区域

特点是无碎片但浪费空间

新生代常用此算法

标记-整理(Mark-Compact)

过程:

标记存活对象

向另一端移动

老年代常用此算法

分代收集理论(GC的灵魂)

两个经典假设:

大多数对象朝生夕死

存活越久的对象越难死

所以:

新生代:频繁GC,复制算法

老年代:低频GC,整理算法

GC类型

类型

说明

Minor GC

新生代 GC

Major GC

老年代 GC

Full 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)

类加载器

类加载器负责加载,不负责验证/初始化

类加载器

说明

Bootstrap

C++ 实现,加载核心类

Extension / Platform

扩展类

Application

应用类

双亲委派模型

类加载请求,先交给父加载器

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 核心性能指标(调优目标)

指标

说明

吞吐量

程序有效工作时间占比

延迟

STW 停顿时间

内存占用

堆 / 元空间

稳定性

Full GC 频率

JVM 常用参数体系

堆相关参数(最重要)

-Xms   初始堆
-Xmx   最大堆
-Xmn   新生代大小

GC 相关参数

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200

元空间

-XX:MetaspaceSize
-XX:MaxMetaspaceSize

诊断参数(必会)

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path

JVM工具体系

jps —— 看JVM进程

jps -l

找进程号

jstat —— GC & 内存实时状态

jstat -gc pid 1000
  • YGC / FGC 次数

  • Eden / Old 使用率

jmap —— 堆快照(OOM 核心)

jmap -dump:format=b,file=heap.hprof pid
  • 分析内存泄漏

  • 找大对象

jstack —— 线程分析(CPU 飙高)

jstack pid
  • 死锁

  • 死循环

  • 线程阻塞

Arthas —— 不重启线上服务排查问题

常用命令

  • dashboard

  • thread

  • heapdump

  • watch

  • trace

典型线上问题 & 调优思路

JVM 调优通常从 GC 日志和监控入手,结合 jstat、jmap、jstack 等工具定位问题,分析是内存泄漏、大对象还是参数不合理,最终通过代码优化或参数调整解决。

频繁Full GC

  • 看 GC 日志

  • 看老年代占用

  • 是否大对象 / 内存泄漏

  • 调整堆 / 代码

OOM

OOM 类型

可能原因

Heap Space

内存泄漏 / 堆小

Metaspace

动态类过多

GC overhead

GC 效率极低

  • Dump 堆

  • MAT / VisualVM 分析

  • 定位引用链

CPU 100%

  • top 找高 CPU 线程

  • 转成 16 进制

  • jstack 定位代码

7. 资料

《深入理解 Java 虚拟机》

《Java 性能权威指南》

Arthas

JVisualVM