目录
[二、JVM 整体运行流程](#二、JVM 整体运行流程)
[三、JVM 内存结构](#三、JVM 内存结构)
[五、Java 虚拟机栈](#五、Java 虚拟机栈)
[九、GC 垃圾回收机制](#九、GC 垃圾回收机制)
[十一、为什么 JVM 要分代?](#十一、为什么 JVM 要分代?)
[十三、常见 OOM 问题](#十三、常见 OOM 问题)
一、JVM、JRE、JDK关系
在我们进行JAVA开发的时候,第一步就是安装JDK、JDK里面又包含了JRE,三者关系如下:

二、JVM 整体运行流程
众所周知,Java的特点是一次编译,到处运行,而这个到处运行的关键就是JVM虚拟机,项目跑在JVM虚拟机上,而虚拟机适配各种系统,如windows、linux、mac
我们写的 Java 文件,并不能直接运行。Java 程序真正运行流程如下:
.java
↓
javac 编译
↓
.class 字节码文件
↓
ClassLoader 类加载
↓
JVM 内存区域
↓
Execution Engine 执行引擎
JVM 本质上:就是一个运行 Java 字节码的虚拟机

三、JVM 内存结构
JVM 在运行过程中,会把内存划分成多个区域


核心结构如下:
线程共享:
堆
方法区(元空间)
线程私有:
程序计数器
虚拟机栈
本地方法栈
四、程序计数器
程序计数器(Program Counter Register):
本质上是:当前线程正在执行的字节码行号指示器
作用
因为 JVM 支持多线程并发执行,例如
就会导致CPU 会不断线程切换
切换回来后,必须知道:线程执行到哪了
程序计数器就是干这个的。
生命周期随线程
线程的时候创建程序计数器
线程销毁的时候程序计数器也销毁
为什么程序计数器不会 OOM?
因为它占用内存极小,也就整个JVM中唯一一个不会发生 OOM 的区域
五、Java 虚拟机栈
虚拟机栈:Java 方法执行的内存模型
每次方法调用都会创建:栈帧(Stack Frame)
栈帧中包含什么?
局部变量表
操作数栈
动态链接
方法出口
示例
java
public void test(){
int a = 1;
int b = 2;
int c = a + b;
}
其中:a、b、c
都存储在:局部变量表中
为什么递归容易 StackOverflow?
例如:
java
public void test(){
test();
}
每次递归:都会创建新栈帧
最终 栈空间耗尽,抛出StackOverflowError
栈大小如何调整?
-Xss
例如:-Xss1m
表示:每个线程栈大小 1MB
六、堆
堆是 JVM 中最大的一块内存区域 ,所有对象实例、数组都在堆中分配内存,也是垃圾回收(GC)的核心区域 堆分为两大块:
- 新生代(Young Generation):存放刚创建、生命周期短的对象
- 老年代(Old Generation / Tenured):存放长期存活、多次 GC 未被回收的对象
默认占比(HotSpot 虚拟机,JDK8)
1. 新生代:老年代 默认比例
默认 1 : 2
- 新生代占堆总大小的 1/3
- 老年代占堆总大小的 2/3
2. 新生代内部结构
新生代又分为:Eden 区 + Survivor0(S0) + Survivor1(S1) 默认比例:8 : 1 : 1
- Eden:80%(大部分新对象在这里创建)
- S0:10%
- S1:10%(始终一块空闲,做复制存活对象用)
3. 举个直观例子
假设堆总大小 -Xms -Xmx = 300M
- 新生代:100M
- Eden:80M
- S0:10M
- S1:10M
- 老年代:200M
堆结构
为什么对象优先进入 Eden?
因为绝大多数对象"朝生夕死",也就是说的用完就没用了该回收了,创建和回收较频繁
例如:
- 方法中的临时对象
- 查询结果
- DTO
- JSON 对象
因此 JVM 专门设计:新生代 来高效回收
对象什么时候进入老年代?
老年代是什么?老年代存储的是存活时间较长的,也就好几轮没回收的,认为该对象活的时间较长,放在老年代就避免频繁判断是否回收。也可能当新生代内存不够了会直接将对象放在老年代中
主要有几个条件:
1. 年龄达到阈值
对象每熬过一次 Minor GC(新生代垃圾回收),就会 年龄 +1
默认:15 岁 进入老年代,也就是说如果这个对象在 新生代 发生垃圾回收15次,这个对象还没被回收
2. 大对象直接进入老年代
例如:超大数组 、 超大字符串
避免:新生代频繁复制
常用堆JVM参数
-Xms:初始堆内存大小-Xmx:最大堆内存大小 线上通常设置-Xms=-Xmx,固定堆大小,避免扩容抖动-Xmn:新生代总内存大小 (Eden 区 + Survivor0 + Survivor1 的总大小)-XX:NewRatio=2:新生代:老年代 = 1:2-XX:SurvivorRatio=8:Eden:S0:S1=8:1:1-XX:MaxTenuringThreshold=15:对象晋升老年代的年龄阈值
七、对象创建全过程、4种引用
对象创建过程
java
User user = new User();
到底发生了什么?
1. 类加载检查
JVM 首先检查:User 类是否已经加载
如果没有:先执行类加载
2. 分配内存
JVM 在堆中:划分对象空间
3. 初始化零值
例如:
java
int -> 0
boolean -> false
4. 设置对象头
对象头中包含:
- hashcode
- GC 分代年龄
- 锁状态
5. 执行 init 方法
构造方法 真正执行
Java 4 种引用
- 强引用:默认,不回收
- 软引用:内存不足回收
- 弱引用:下次 GC 必回收
- 虚引用:仅做回收通知
八、对象什么时候会被回收?根据垃圾回收算法判断
目前常见的:引用计数法、GC Root可达性分析法,但是目前基本都是GCRoot
引用计数法为什么不用?
因为:循环引用问题
例如:
A -> B
B -> A
即使没人引用它们:引用计数也不为 0
JVM 使用什么
把整个内存里的对象,想象成一棵倒立的大树
- GC Roots = 树根(起点)
- 普通对象 = 树枝、叶子
可达性分析逻辑: 从 GC Roots(根) 出发,顺着引用链往下遍历:
- 能走到的对象 → 可达,存活,不回收
- 走不到的对象 → 不可达,垃圾,要回收
只要树根能顺着树枝到达这个叶子 → 活着 树根摸不到这个叶子 → 垃圾,回收
GC Root 包括
- 虚拟机栈(本地变量表)引用的对象
- 方法区中静态变量引用的对象
- 本地方法栈(JNI)引用的对象
- 运行中的活跃线程
- Class 对象、锁对象等
九、GC 垃圾回收机制
Minor GC
发生在:新生代空间不时,Eden 满出发
特点:频率高、速度快、STW 时间短
Full GC
回收:整个堆
包括:
- 新生代
- 老年代
Full GC触发条件
- 老年代空间不足
- 元空间不足
- 手动调用
System.gc()(注:只是"建议 JVM 进行 Full GC",不一定立即执行)
为什么 FullGC 可怕
因为 STW(Stop The World) , 会暂停所有用户线程
线上现象
如果出现频繁 FullGC 或单次 FullGC 时间过长:
- 接口响应超时
- 服务明显卡顿
- TPS、QPS 暴跌
- 严重时引发服务雪崩
单次 FullGC 停顿太久(几秒)→ 直接超时、雪崩
频繁 FullGC(几秒一次)→ CPU 打满、服务持续卡顿
十、垃圾回收算法
1. 标记清除
过程:
标记垃圾
直接删除
问题:会产生内存碎片
图有点问题,标记的是存活对象,未标记的是要回收的对象,并且回收结束后不会移动存活对象

2. 复制算法
把存活对象 ,复制到另一块区域,这样就可以内存连续
优点:无碎片
缺点:浪费空间,要再单独搞出一块内存放 内存连续 没有被回收的对象

3. 标记整理
整理内存:避免碎片

十一、为什么 JVM 要分代?
不同对象的生命周期不一样,分代后用不同的垃圾回收算法,提升 GC 效率,减少 STW 停顿
JVM 发现对象有非常明显的特点:
- 大部分对象朝生夕死:方法内临时变量、接口临时数据、DTO、JSON 对象,用完马上就回收;
- 少部分对象长期存活:全局缓存、单例对象、静态变量,长期不回收。
如果不分代,所有对象混在一起,每次 GC 都要扫描整个堆,效率极低、停顿极长。
2. 新生代、老年代用不同 GC 算法
新生代(对象死亡率极高)
- 特点:存活对象少、死亡对象多
- 适合:复制算法
- 优势:复制少量存活对象,没有内存碎片,回收速度极快,Minor GC 毫秒级
老年代(对象存活率极高)
- 特点:存活对象多、死亡对象少
- 适合:标记 - 清除 / 标记 - 整理算法
- 优势:不用大量复制,避免浪费内存
3. 分代带来的好处
- GC 频率分开:新生代频繁快速回收,老年代很少回收
- STW 时间变短:大部分垃圾在新生代就被清理,不用频繁扫描整个堆
- 内存利用率更高:针对不同生命周期优化内存布局
4. 不分代的缺点
不分代时,不管新对象老对象,每次 GC 都要全部扫描,GC 慢、卡顿久、性能差
十二、双亲委派机制
双亲委派机制是 Java 类加载器的加载规则: 当一个类收到加载请求时,先交给父类加载器;父类加载不了,自己再加载。 向上委派,向下查找
四层类加载器(从上到下)
- 启动类加载器(Bootstrap ClassLoader) :加载 JDK 核心类,如
java.lang.*,C++ 实现 - 扩展类加载器(Extension ClassLoader):加载 jre/lib/ext 扩展包
- 应用类加载器(App ClassLoader):加载我们自己写的项目代码
- 自定义类加载器:自己继承 ClassLoader 实现
注意:双亲不是指两个父类,是向上层层委托
完整加载流程
- 我自己的类 → 先给应用类加载器
- 应用类加载器 → 委托给扩展类加载器
- 扩展类加载器 → 委托给启动类加载器
- 启动类加载器找不到 → 退回给扩展
- 扩展找不到 → 退回给应用加载器自己加载
原则:向上委托,向下查找
为什么要双亲委派?
-
安全,防止核心类被篡改 比如你自己写一个
java.lang.String类, 因为启动类加载器已经加载过系统的 String,不会加载你写的,避免安全漏洞 -
类全局唯一,避免重复加载 保证一个类在内存中只加载一次,节省内存
十三、常见 OOM 问题
1. Java heap space
堆内存不足。
常见原因:
- 内存泄漏
- 大缓存
- 集合无限增长
2. StackOverflowError
递归过深,例如我们写的错误递归算法
3. Metaspace OOM
元空间不足
常见:动态生成大量类
(部分图示来自AI生成)
