#前言
最近在啃JVM,抱着厚厚的PDF课件啃了整整一周,从虚拟机发展史、内存分区、类加载到垃圾回收、JMM内存模型全部梳理了一遍。以前总觉得JVM是面试天书,拆开一步步学才发现逻辑特别连贯,今天以学生视角整理一份完整学习笔记,零基础也能看懂,面试高频考点全部划重点!
一、什么是JVM?
各大虚拟机发展史盘点 JVM全称Java Virtual Machine,Java虚拟机,是一套软件模拟的、隔离环境下的虚拟计算机,也是实现一次编译,到处运行的核心。 很多人分不清JVM和VMware、VirtualBox。
- VMware/VirtualBox:模拟完整物理CPU指令集,完整复刻硬件;
- JVM:只模拟Java字节码指令集,只保留PC寄存器,是专门为Java设计的专用虚拟机。
主流JVM发展史
- Sun Classic VM 第一款商用Java虚拟机,JDK1.0诞生,只有解释器,JIT编译器需要外挂,二者无法同时工作,JDK1.4彻底淘汰,现在HotSpot内置了它。
- Exact VM JDK1.2推出,解决Classic的缺陷,支持热点探测、解释器+编译器混合执行,但只在Solaris系统短暂使用,很快被HotSpot替代。
- HotSpot VM(我们现在用的) 目前绝对主流,JDK1.3成为默认虚拟机,Oracle JDK、OpenJDK默认实现,核心是热点代码探测,解释器和JIT编译器协同工作,平衡启动速度和运行性能。
- JRockit 服务端专用虚拟机,无解释器,全靠JIT编译,性能极强,主打低延迟,后来Oracle收购BEA后,把它的优秀特性移植到HotSpot中。
- OpenJ9(IBM J9) IBM研发,多场景通用,2017年开源交给Eclipse,服务器、嵌入式都能用,IBM产品默认虚拟机。
- 阿里Taobao JVM(AJDK) 国产定制虚拟机,基于OpenJDK HotSpot改造,自研GCIH堆外共享、ZenGC垃圾收集器,解决高并发电商场景GC卡顿问题,淘宝、天猫全线替换官方JDK。
二、JVM完整执行流程,四大核心模块
从.class字节码文件到CPU执行,全程分四大组件:
- 类加载子系统 ClassLoader:读取class文件,加载类到内存;
- 运行时数据区 Runtime Data Area:内存布局,程序运行时存储数据;
- 执行引擎 Execution Engine:把字节码翻译成操作系统指令,含解释器、JIT编译器;
- 本地方法接口 Native Interface :调用C/C++写的本地方法,配套本地方法库。 流程简化:Java代码编译成class文件 → 类加载器载入运行时数据区 → 执行引擎解析字节码 → 本地方法接口调用底层库,交给CPU运行。

三、运行时数据区(JVM内存布局,重中之重)
很多新手会混淆运行时数据区(内存布局) 和JVM(Java内存模型) ,前者是内存物理分区,后者是并发内存访问规范,完全两个概念。 内存分为线程共享区 、线程私有区两大类:
(一)线程共享(多线程共用,GC重点回收区域)
1. 堆 Heap
所有对象实例、数组全部存放在堆,是GC最频繁的区域。
-
JVM参数:
-Xms堆初始最小内存,-Xmx堆最大内存; -
分区:新生代Young + 老年代Old

-
异常:内存放不下对象抛出
java.lang.OutOfMemoryError: Java heap space
2. 元空间 Metaspace(JDK8替代永久代PermGen)
对应规范里的方法区,存储类结构、静态变量、常量池、编译后的代码。
- JDK7及之前:永久代,属于JVM堆内存,容易OOM;
- JDK8改动:元空间使用本地物理内存,不再占用堆;字符串常量池从永久代移到堆;
- 异常:类加载过多抛出
java.lang.OutOfMemoryError: Metaspace。 - 运行时常量池:元空间的一部分,存字面量、符号引用。
(二)线程私有(每个线程独立一份,线程销毁自动释放,无需GC)
1. Java虚拟机栈
存储方法执行信息,每调用一个方法创建一块栈帧 ,方法结束栈帧出栈。 栈帧四部分:局部变量表、操作数栈、动态链接、方法返回地址 。

- 局部变量表:存基本类型、对象引用,编译时确定大小;
- 异常 :递归过深、栈容量太小抛出
StackOverflowError;多线程创建过多栈会OOM。 - 参数
-Xss设置单个栈的内存大小。
2. 本地方法栈
和虚拟机栈结构一致,专门给native本地C/C++方法使用。HotSpot将虚拟机栈、本地方法栈合并实现。
3. 程序计数器
PC寄存器 记录当前线程执行到哪一行字节码指令,唯一不会发生OOM 的内存区域; 执行Native本地方法时值为空,执行Java方法存字节码地址。

内存溢出实战小实验
- 堆OOM:循环创建对象,设置
-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError,溢出自动导出堆快照,用MAT工具分析区分内存泄漏 (对象无法被GC)和内存溢出(内存确实不足); - 栈溢出:递归无限调用方法,调小
-Xss128k,直接抛出StackOverflowError。
四、类加载机制:
类的完整生命周期 一个类从加载到卸载分为5大阶段:加载 → 验证 → 准备 → 解析 → 初始化 ,前5步统称类加载。

- 加载Loading :通过全限定名读取class二进制流,在元空间生成类结构,创建
Class对象作为访问入口; - 验证Verify:校验class文件格式、字节码合法性、符号引用安全,防止恶意代码破坏虚拟机;
- 准备Prepare :给static静态变量分配内存,赋默认初始值 (
static int a=123这里a先等于0); - 解析Resolve:把常量池里的符号引用转为直接内存引用;
- 初始化Init:执行静态代码块、给静态变量赋程序员写的真实值,真正执行Java代码。
双亲委派模型
如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆法完成这个加载请求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成加载。
四层类加载器
- 启动类加载器Bootstrap:C++实现,加载
jre/lib/rt.jar核心类; - 扩展类加载器Platform:加载
jre/lib/ext扩展包; - 应用程序加载器AppClassLoader:加载我们自己写的项目代码、ClassPath下jar;
- 自定义类加载器:用户自己继承ClassLoader实现。
双亲委派规则
收到加载请求,先交给父加载器,层层向上直到Bootstrap;父加载器找不到类,子类才自己加载。
两大好处
- 避免类重复加载;⽐如A类和B类都有⼀个⽗类C类,那么当A启动时就会将C类加载起来,那么在B类进⾏加载时就不需要在重复加载C类了。
- 安全防护:防止用户自定义
java.lang.Object篡改核心类。
破坏双亲委派:
JDBC SPI案例(核心考点) 双亲委派存在局限:核心包(rt.jar)里的代码需要加载用户自定义jar里的实现类,典型就是JDBC。
DriverManager在rt.jar,由Bootstrap加载;- MySQL驱动实现类在项目引入的mysql.jar,只能由AppClassLoader加载;
- Bootstrap加载器无法访问子类加载器,于是引入线程上下文类加载器,反向委派子类加载器加载驱动,打破了自上而下的双亲规则。
五、垃圾回收GC:
堆和元空间的回收逻辑 线程私有区随线程销毁自动释放,GC只关心堆(对象)和元空间(类)。
1. 如何判断对象是否可回收?
(1)引用计数算法(Java不采用)
对象加计数器,引用+1,失效-1,0则回收;缺陷:循环引用无法回收,
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
// 强制jvm进⾏垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
GC后内存明显释放,证明JVM不用这套算法。
(2)可达性分析算法(HotSpot主流)
以GC Roots为起点遍历引用链,没有引用链连通的对象判定为垃圾。 能作为GC Roots的对象:
- 虚拟机栈局部变量引用的对象;
- 元空间static静态变量引用;
- 常量引用;
- Native本地方法引用。
四种引用强度递减
- 强引用 :
Object o = new Object(),永远不回收; - 软引用SoftReference:内存即将溢出时回收,适合缓存;
- 弱引用WeakReference:下次GC必回收;
- 虚引用PhantomReference:仅用于回收时收到系统通知,无法获取对象。
2. 三大基础垃圾回收算法
- 标记-清除:先标记垃圾再统一清除;缺点:效率低、产生大量内存碎片;
- 复制算法:内存对半分,存活对象复制到另一半,清空原区域;无碎片,新生代默认使用(Eden+S0/S1);
- 标记-整理:标记垃圾后,存活对象向内存一端移动,清理边界外垃圾;老年代使用,解决碎片问题。
3. 分代收集(商用虚拟机统一方案)
根据对象存活周期分区,不同区域使用对应算法:
- 新生代:98%对象朝生夕死,复制算法;对象在S0/S1来回拷贝,默认15次GC后晋升老年代;
- 老年代:对象存活率高,标记清除/标记整理。
Minor GC vs Full GC - Minor GC:
新生代回收,频率极高,速度快; - Full GC:整堆(新生代+老年代+元空间)回收,STW停顿极长,尽量避免触发。
4. 七大垃圾收集器(HotSpot)
连线代表收集器可搭配使用,分为新生代、老年代、全区域收集器:
新生代收集器
- Serial:单线程串行GC,Client模式默认,全程STW;
- ParNew:Serial多线程版本,唯一能和CMS搭配的新生代收集器;
- Parallel Scavenge:吞吐量优先收集器,自带自适应调参策略。
老年代收集器
- Serial Old:Serial老年代版本,标记整理,CMS失败后备方案;
- Parallel Old:Parallel Scavenge配套,多线程标记整理,高吞吐量服务首选;
- CMS:低停顿并发收集器,标记清除算法;优点:用户线程和GC并发、停顿短;缺点:占用CPU、浮动垃圾、内存碎片。
全域收集器 G1(Garbage First):
JDK9默认,把堆切分成多个Region块,区分Eden/Survivor/Old/Humongous大对象区;整体标记整理、局部复制,优先回收垃圾最多的Region,平衡停顿和吞吐量,用来替代CMS。
六、JMM
Java内存模型(并发核心) 很多初学者把JMM和运行时数据区搞混,这里划清界限:
- JVM运行时数据区:内存物理划分;
- JMM:一套并发访问共享变量的规范,屏蔽操作系统硬件内存差异,保证多线程程序跨平台一致。
1. 主内存 & 工作内存
- 主内存:存储所有共享变量(实例、静态变量、数组);
- 工作内存:每个线程私有,保存共享变量副本; 线程不能直接读写主内存,所有操作在工作内存完成,通过Load/Save同步主内存。
2. JMM三大核心特性
- 原子性 :操作不可拆分,基础读写天然原子,复杂操作用
synchronized保证; - 可见性 :一个线程修改变量,其他线程立刻感知,
volatile、synchronized、final实现; - 有序性 :防止指令重排序,依靠
happens-before先行发生原则。
3. volatile关键字(面试高频)
两大作用:
- 保证可见性 :修改立刻刷回主内存,其他线程读取最新值; 坑点:不保证原子性,
num++自增并发会出错,因为自增分读、加、写三步; - 禁止指令重排序:读写volatile变量时,前后代码顺序不会打乱,解决双重检查锁单例空指针问题。
经典案例:
双重校验锁单例 不加volatile时,
new Singleton()会发生指令重排,先分配内存、再赋值引用,未初始化完成就被其他线程获取空对象;变量加volatile禁止重排序,保证对象初始化完整后再赋值。
4. happens-before先行发生8条规则
用来判断并发代码是否有序,无需锁也能保证可见性:程序次序、锁、volatile、传递性、线程启动/中断/结束、对象finalize规则。
七、学习总结
一周啃完JVM整套内容,最大感受就是知识点环环相扣: 从虚拟机是什么 → 程序怎么跑 → 内存怎么划分 → 类怎么加载进内存 → 不用的对象怎么回收 → 多线程下内存怎么同步。 对后端开发者来说,JVM不只是面试考点,更是线上问题排查的基础:OOM内存溢出、GC卡顿、线程死循环、并发数据错乱,全部都要靠这套知识定位根源。后续打算实操MAT堆分析、JProfiler监控、G1调优,
文末碎碎念
文章里很多流程图帮我理清了抽象概念,建议和我一样自学的同学,不要死记参数,先理解整体流程,再逐个攻克垃圾收集器、双亲委派、volatile这些难点,配合简单测试代码跑一遍,记忆会深刻很多。 需要的话我可以把文中测试堆OOM、栈溢出、循环引用、volatile自增的完整代码整理出来分享!