JVM虚拟机

#前言

最近在啃JVM,抱着厚厚的PDF课件啃了整整一周,从虚拟机发展史、内存分区、类加载到垃圾回收、JMM内存模型全部梳理了一遍。以前总觉得JVM是面试天书,拆开一步步学才发现逻辑特别连贯,今天以学生视角整理一份完整学习笔记,零基础也能看懂,面试高频考点全部划重点!

一、什么是JVM?

各大虚拟机发展史盘点 JVM全称Java Virtual Machine,Java虚拟机,是一套软件模拟的、隔离环境下的虚拟计算机,也是实现一次编译,到处运行的核心。 很多人分不清JVM和VMware、VirtualBox。

  • VMware/VirtualBox:模拟完整物理CPU指令集,完整复刻硬件;
  • JVM:只模拟Java字节码指令集,只保留PC寄存器,是专门为Java设计的专用虚拟机。

主流JVM发展史

  1. Sun Classic VM 第一款商用Java虚拟机,JDK1.0诞生,只有解释器,JIT编译器需要外挂,二者无法同时工作,JDK1.4彻底淘汰,现在HotSpot内置了它。
  2. Exact VM JDK1.2推出,解决Classic的缺陷,支持热点探测、解释器+编译器混合执行,但只在Solaris系统短暂使用,很快被HotSpot替代。
  3. HotSpot VM(我们现在用的) 目前绝对主流,JDK1.3成为默认虚拟机,Oracle JDK、OpenJDK默认实现,核心是热点代码探测,解释器和JIT编译器协同工作,平衡启动速度和运行性能。
  4. JRockit 服务端专用虚拟机,无解释器,全靠JIT编译,性能极强,主打低延迟,后来Oracle收购BEA后,把它的优秀特性移植到HotSpot中。
  5. OpenJ9(IBM J9) IBM研发,多场景通用,2017年开源交给Eclipse,服务器、嵌入式都能用,IBM产品默认虚拟机。
  6. 阿里Taobao JVM(AJDK) 国产定制虚拟机,基于OpenJDK HotSpot改造,自研GCIH堆外共享、ZenGC垃圾收集器,解决高并发电商场景GC卡顿问题,淘宝、天猫全线替换官方JDK。

二、JVM完整执行流程,四大核心模块

.class字节码文件到CPU执行,全程分四大组件:

  1. 类加载子系统 ClassLoader:读取class文件,加载类到内存;
  2. 运行时数据区 Runtime Data Area:内存布局,程序运行时存储数据;
  3. 执行引擎 Execution Engine:把字节码翻译成操作系统指令,含解释器、JIT编译器;
  4. 本地方法接口 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方法存字节码地址。

内存溢出实战小实验
  1. 堆OOM:循环创建对象,设置-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError,溢出自动导出堆快照,用MAT工具分析区分内存泄漏 (对象无法被GC)和内存溢出(内存确实不足);
  2. 栈溢出:递归无限调用方法,调小-Xss128k,直接抛出StackOverflowError。

四、类加载机制:

类的完整生命周期 一个类从加载到卸载分为5大阶段:加载 → 验证 → 准备 → 解析 → 初始化 ,前5步统称类加载。

  1. 加载Loading :通过全限定名读取class二进制流,在元空间生成类结构,创建Class对象作为访问入口;
  2. 验证Verify:校验class文件格式、字节码合法性、符号引用安全,防止恶意代码破坏虚拟机;
  3. 准备Prepare :给static静态变量分配内存,赋默认初始值static int a=123这里a先等于0);
  4. 解析Resolve:把常量池里的符号引用转为直接内存引用;
  5. 初始化Init:执行静态代码块、给静态变量赋程序员写的真实值,真正执行Java代码。

双亲委派模型

如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆法完成这个加载请求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成加载。

四层类加载器
  1. 启动类加载器Bootstrap:C++实现,加载jre/lib/rt.jar核心类;
  2. 扩展类加载器Platform:加载jre/lib/ext扩展包;
  3. 应用程序加载器AppClassLoader:加载我们自己写的项目代码、ClassPath下jar;
  4. 自定义类加载器:用户自己继承ClassLoader实现。
双亲委派规则

收到加载请求,先交给父加载器,层层向上直到Bootstrap;父加载器找不到类,子类才自己加载。

两大好处
  1. 避免类重复加载;⽐如A类和B类都有⼀个⽗类C类,那么当A启动时就会将C类加载起来,那么在B类进⾏加载时就不需要在重复加载C类了。
  2. 安全防护:防止用户自定义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的对象:

  1. 虚拟机栈局部变量引用的对象;
  2. 元空间static静态变量引用;
  3. 常量引用;
  4. Native本地方法引用。

四种引用强度递减

  1. 强引用Object o = new Object(),永远不回收;
  2. 软引用SoftReference:内存即将溢出时回收,适合缓存;
  3. 弱引用WeakReference:下次GC必回收;
  4. 虚引用PhantomReference:仅用于回收时收到系统通知,无法获取对象。

2. 三大基础垃圾回收算法

  1. 标记-清除:先标记垃圾再统一清除;缺点:效率低、产生大量内存碎片;
  2. 复制算法:内存对半分,存活对象复制到另一半,清空原区域;无碎片,新生代默认使用(Eden+S0/S1);
  3. 标记-整理:标记垃圾后,存活对象向内存一端移动,清理边界外垃圾;老年代使用,解决碎片问题。

3. 分代收集(商用虚拟机统一方案)

根据对象存活周期分区,不同区域使用对应算法:

  • 新生代:98%对象朝生夕死,复制算法;对象在S0/S1来回拷贝,默认15次GC后晋升老年代;
  • 老年代:对象存活率高,标记清除/标记整理
Minor GC vs Full GC - Minor GC:

新生代回收,频率极高,速度快; - Full GC:整堆(新生代+老年代+元空间)回收,STW停顿极长,尽量避免触发。

4. 七大垃圾收集器(HotSpot)

连线代表收集器可搭配使用,分为新生代、老年代、全区域收集器:

新生代收集器
  1. Serial:单线程串行GC,Client模式默认,全程STW;
  2. ParNew:Serial多线程版本,唯一能和CMS搭配的新生代收集器;
  3. Parallel Scavenge:吞吐量优先收集器,自带自适应调参策略。
老年代收集器
  1. Serial Old:Serial老年代版本,标记整理,CMS失败后备方案;
  2. Parallel Old:Parallel Scavenge配套,多线程标记整理,高吞吐量服务首选;
  3. CMS:低停顿并发收集器,标记清除算法;优点:用户线程和GC并发、停顿短;缺点:占用CPU、浮动垃圾、内存碎片。
全域收集器 G1(Garbage First)

JDK9默认,把堆切分成多个Region块,区分Eden/Survivor/Old/Humongous大对象区;整体标记整理、局部复制,优先回收垃圾最多的Region,平衡停顿和吞吐量,用来替代CMS。

六、JMM

Java内存模型(并发核心) 很多初学者把JMM和运行时数据区搞混,这里划清界限:

  • JVM运行时数据区:内存物理划分;
  • JMM:一套并发访问共享变量的规范,屏蔽操作系统硬件内存差异,保证多线程程序跨平台一致。

1. 主内存 & 工作内存

  • 主内存:存储所有共享变量(实例、静态变量、数组);
  • 工作内存:每个线程私有,保存共享变量副本; 线程不能直接读写主内存,所有操作在工作内存完成,通过Load/Save同步主内存。

2. JMM三大核心特性

  1. 原子性 :操作不可拆分,基础读写天然原子,复杂操作用synchronized保证;
  2. 可见性 :一个线程修改变量,其他线程立刻感知,volatilesynchronizedfinal实现;
  3. 有序性 :防止指令重排序,依靠happens-before先行发生原则。

3. volatile关键字(面试高频)

两大作用:

  1. 保证可见性 :修改立刻刷回主内存,其他线程读取最新值; 坑点:不保证原子性,num++自增并发会出错,因为自增分读、加、写三步;
  2. 禁止指令重排序:读写volatile变量时,前后代码顺序不会打乱,解决双重检查锁单例空指针问题。
经典案例:

双重校验锁单例 不加volatile时,new Singleton()会发生指令重排,先分配内存、再赋值引用,未初始化完成就被其他线程获取空对象;变量加volatile禁止重排序,保证对象初始化完整后再赋值。

4. happens-before先行发生8条规则

用来判断并发代码是否有序,无需锁也能保证可见性:程序次序、锁、volatile、传递性、线程启动/中断/结束、对象finalize规则。

七、学习总结

一周啃完JVM整套内容,最大感受就是知识点环环相扣: 从虚拟机是什么 → 程序怎么跑 → 内存怎么划分 → 类怎么加载进内存 → 不用的对象怎么回收 → 多线程下内存怎么同步。 对后端开发者来说,JVM不只是面试考点,更是线上问题排查的基础:OOM内存溢出、GC卡顿、线程死循环、并发数据错乱,全部都要靠这套知识定位根源。后续打算实操MAT堆分析、JProfiler监控、G1调优,

文末碎碎念

文章里很多流程图帮我理清了抽象概念,建议和我一样自学的同学,不要死记参数,先理解整体流程,再逐个攻克垃圾收集器、双亲委派、volatile这些难点,配合简单测试代码跑一遍,记忆会深刻很多。 需要的话我可以把文中测试堆OOM、栈溢出、循环引用、volatile自增的完整代码整理出来分享!

相关推荐
wuminyu3 小时前
虚拟线程底层ForkJoinPool的工作窃取算法机制
java·linux·c语言·jvm·c++
leisoo80974 小时前
股票数据本地化存储实战:JSON、数据库与列式存储的方案对比
jvm·数据库·json
SamChan908 小时前
PDF翻译中的并发控制:用Semaphore防止API限流与资源耗尽
java·jvm·pdf
snow@li10 小时前
Java:跨平台原理与JDK、JRE、JVM全景深度解析
java·开发语言·jvm
Livia要学习10 小时前
Python闭包
开发语言·jvm·python
行者-全栈开发1 天前
美团后端开发面试题全集(2026最新版):HashMap、JVM、MySQL、Redis高频考点深度解析
jvm·hashmap·java面试题·后端开发·redis缓存·mysql索引·面经汇总
豆沙沙包?1 天前
C++-程序的内存模型(P84-P88)
java·jvm·c++
未秃头的程序猿1 天前
凌晨3点被叫醒:线上OOM,我用这套流程40分钟定位根因
java·jvm·后端
lisin-lee-cooper1 天前
JVM知识体系
java·jvm