JVM
JVM(Java Virtual Machine,Java虚拟机)是Java程序“一次编写,到处运行”这一特性的实现者。
JVM 主要由四个子系统组成。
- 类加载子系统 (Class Loader Subsystem): 负责将硬盘上的
.class文件加载到内存中。 - 运行时数据区 (Runtime Data Areas): 也就是我们常说的“JVM 内存”,用于存储数据和中间结果。
- 执行引擎 (Execution Engine): 负责解释命令,提交给操作系统执行。包含解释器、 JIT 编译器和垃圾回收器(GC) 。
- 本地库接口 (Native Interface): 融合不同编程语言为 Java 所用(通常是 C/C++)。那我为啥不用python?
类加载子系统
流程
它的核心职责包括:
- 加载(Loading):查找并读取类的二进制数据(.class)
- 连接(Linking):验证、准备、解析类
- 初始化(Initialization):执行类初始化逻辑
.class文件为二进制文件,里面包含0xCAFEBABE这个魔数,版本号,常量池,访问标志,代码等等。经过一番研究发现.class虽然与汇编代码样似,但是实际上根本不一样,因为class根本不是让人看的,汇编人类还是可以编写的,但不会有正常人编写class。所以了解即可,除非你干的是逆向。我们最多用到javap -c xxx.class看一些编译器的优化操作,而java -c -v xxx.class则是过于繁琐。
类的完整生命周期(注意所有的类都会连接,但是只有被触发的类才会走到初始化这一步)
|
|
从加载到初始化归类加载子系统负责,
加载就是将字节码形态的.class装载到JVM内存里面,字节码可以既可以来自本地,也可以来自网络和动态代理。
连接分为验证安全性,为静态变量分配内存并附上默认值(并非类自定义的值),将符号引用(字符串形态)解析为直接引用(内存地址、偏移量)。
初始化:
<clinit>(class initialization method):
- 由 javac 编译器自动生成
- 用于完成 类级别初始化
- 只负责:
static变量的显式赋值static {}静态代码块
- 在 类初始化(Initialization)阶段由 JVM 调用
- 一个类最多只有一个
<clinit>方法
初始化触发条件:
new创建对象- 访问
static非 final 变量 - 调用静态方法
- 反射调用
- JVM 启动主类
- 子类初始化,父类先初始化
实现
JVM 内置类加载器:
- Bootstrap ClassLoader: 祖宗级。用 C++ 写的,加载核心库(
rt.jar,如 String, System)。Java 代码无法直接获取它。 - Extension/Platform ClassLoader: 父级。加载扩展库。(属于历史遗留问题,现在的maven和gradle已经很好地管理第三方库了)在Java8中
ExtClassLoader加载JAVA_HOME/lib/ext所有的库,而Java9+中则变为PlatformClassLoader加载<JAVA_HOME>/jmods,但是仅限 JDK 内置模块和仅加载官方签名模块。 - Application ClassLoader: 应用级。加载
classpath下你自己写的类。
双亲委派模型 (Parent Delegation Model):
注意这个双亲和数据结构一个问题,都是政治正确导致的中性词语,本质上仍为上下级、父子这种一一相对的关系,parent也并未parents,双亲只是一个很烂容易误导初学者理解的翻译。
总结起来这个就是为了安全,防止篡改核心库的类加载设计,一切加载先交给父加载器,失败再回退到当前加载器。
运行时数据区
也就是我们常说的JVM内存,OOM警告!
关键划分原则:是否线程私有 (感谢ai大人的古法ascii画图)
想必都学过计组,PC就是那个PC:
执行 Java 方法 → 存字节码行号
执行 Native 方法 → 值为 Undefined
虚拟机栈就是类似汇编的PROC区域 本地方法栈也是个傻逼翻译,native method,就是非java语言的,c,cpp写的方法 在 HotSpot 虚拟机中,直接就把本地方法栈和虚拟机栈合二为一了。
Java堆,就是最大的一坨内存,用来存放对象实例。 分代架构(经典版):为了高效回收,堆被划分为:
- 新生代 (Young Generation):
- Eden 区:大部分对象出生的地儿。
- Survivor 区 (S0/S1):经历过 GC 幸存下来的对象存放地。
- 老年代 (Old Generation):存放生命周期长、大的对象。
方法区:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。(JDK7-为“永久代 (Permanent Generation)”使用堆内存,JDK8+为“元空间 (Metaspace)”,直接使用本地物理内存)
最后补充一下Java堆和物理内存的区别,JVM只是内存的“二房东”。提前划分好一块,然后自己搞GC,不让操作系统参与。
执行引擎
解释/编译
解释器:将字节码转换为机器码,启动快,无需编译
JIT 编译器 (Just-In-Time Compiler):A. 热点探测 (Hot Spot Detection)
- JVM 会监控代码的运行情况。如果某段代码(一个方法或一个循环体)运行频率非常高,就会被标记为**“热点代码”**。
- 编译优化 JIT 会把这些“热点代码”直接编译成本地机器指令,并存在内存的 Code Cache (就是在内存里面的一块,不是在java堆里面)中。下次执行时,CPU 直接跑机器码,速度接近 C/C++。
- C1 与 C2 编译器 现代 JVM(HotSpot)内置了两个 JIT 编译器:
- C1 (Client Compiler): 优化简单,耗时短。追求启动速度和基本的优化。
- C2 (Server Compiler): 耗时长,但优化极深。它会根据代码运行的统计信息进行激进优化(如逃逸分析)。
逃逸分析:如果一个对象只在当前方法内部使用,且没有返回,也没有赋值给外部变量,那它就是“不逃逸”的。
如果不逃逸会优化:
- 栈上分配 (Stack Allocation)
- 优化: 既然对象只在方法里用,执行引擎直接在**栈(Stack)**上分配内存。
- 好处: 方法执行完,栈帧弹出,对象直接消失。完全不需要 GC 介入,性能极大提升。
- 标量替换 (Scalar Replacement)
- 概念: “标量”是指不可再拆分的数据(如 int, long 等基本类型)。“聚合量”是指可以拆分的对象(如包含 x, y 坐标的
Point对象)。 - 优化: 如果一个对象不会逃逸,执行引擎可能根本不创建这个对象,而是把这个对象拆解掉,将其成员变量恢复成原始的局部变量存放在栈上。
- 好处: 节省了对象头(Object Header)的内存消耗(通常 12-16 字节),且基本类型的操作效率比对象高得多。
- 同步消除 (Lock Elision / Lock Coarsening)
- 优化: 如果执行引擎发现一个同步锁对象(
synchronized)只会在当前线程内被访问,不可能被其他线程竞争。 - 好处: 执行引擎会直接把锁删掉。因为单线程环境下的加锁、解锁是纯粹的性能浪费。
垃圾回收
最常被问到的GC。垃圾回收(GC)主要回收 JVM 堆(Heap),元空间也有很小概率会被回收,废弃常量和无用的类会被回收,但是无用的类很难被满足(不如好好想想加大元空间大小)。
JVM 不使用引用计数法(引用为0则删除),而是使用 可达性分析算法
基本原理:从一系列被称为 “GC Roots” 的根对象开始,像走迷宫一样向下搜索。如果一个对象到 GC Roots 没有任何引用链相连,就说明这个对象是不可达的,即为垃圾。
哪些可以作为 GC Roots?
- 虚拟机栈(栈帧中的局部变量表)引用的对象。
- 方法区中类静态属性、常量引用的对象。
- 本地方法栈中 JNI(Native方法)引用的对象。
- 所有被同步锁(synchronized)持有的对象。
清理垃圾策略:
- 标记 - 清除 (Mark-Sweep):把垃圾标记出来,直接原地抹掉。 缺点:会产生大量的内存碎片。
- 标记 - 复制 (Mark-Copying):把内存分成两块,每次只用一块。清理时,把活着的对象全部“搬家”到另一块,剩下的全清空。 优点:没有碎片,速度极快。 缺点:浪费空间,始终有一半内存是空着的。
- 标记 - 整理 (Mark-Compact):标记后,让所有存活对象都向内存的一端移动,然后直接清理掉边界以外的内存。 优点:没有碎片,也不浪费空间。 缺点:移动对象很费劲,需要更新所有指向这些对象的指针,效率较低。
分代回收:
JVM 发现,绝大多数 Java 对象都是“短命鬼”(比如方法里的临时变量),而极少数对象(如连接池、Spring Bean)能活很久。
于是,堆内存被划分为 新生代 和 老年代,采用不同的策略:
新生代 (Young Generation) —— “高频清理”
- 特点:对象死得快,存活率低。
- 结构:Eden 区 + 两个 Survivor 区 (S0, S1),默认比例 8:1:1。
- 策略:采用 复制算法。
- 新对象在 Eden 出生。
- Minor GC 时,把 Eden 和 S0 里的活口扔进 S1,清空 Eden 和 S0。
- 对象每横跳一次,年龄 +1。
- 当S0空的时候,Minor GC就会把活口扔进 S1,清空 Eden 和 S1,实现左右横条
注:Eden为伊甸园的意思 Minor GC在Eden满的时候触发
老年代 (Old Generation)
- 特点:对象生命周期长,空间大。
- 策略:采用 标记-整理 或 标记-清除 算法。
- 晋升机制:对象年龄达到阈值(默认 15)或者对象太大(大数组),直接进入老年代。当老年代满了,触发 Major GC / Full GC。
Full GC 触发(整堆回收,含新生代 + 老年代 + 元空间):老年代/元空间空间不够,新生代晋升老年代失败等
Major GC 触发(仅老年代回收):老年代占用达到阈值,大对象 / 大量对象直接进入老年代导致空间紧张,多次 Minor GC 后对象持续晋升,老年代逼近上限
注:元空间虽然不在java堆,但是满了也会触发Full GC是想通过类卸载来释放元空间
垃圾回收器
Parallel GC:追求最高吞吐量
Minor GC(新生代回收):
- STW 暂停应用程序线程。
- 从根节点(GC Roots,包括线程栈、静态变量等)开始,多线程并发标记存活对象。
- 使用复制算法,将存活对象从 Eden 区复制到 Survivor 区。
- 清空 Eden 区和已使用的 Survivor 区。
- 恢复应用程序线程。
Full GC(老年代回收,通常伴随 Minor GC):
- STW 暂停应用程序线程(暂停时间较长,可能数百毫秒至秒级)。
- 多线程并发标记所有存活对象(标记 - 整理 算法)。
- 标记阶段:从 GC Roots 追踪存活对象。
- 整理阶段:将存活对象压缩到内存一端,消除碎片。
- 清空剩余空间。
- 恢复应用程序线程。
CMS(Concurrent Mark Sweep)针对老年代设计,旨在减少暂停时间。
Minor GC(新生代回收,使用 ParNew):
- STW 暂停应用程序线程。
- 多线程从 GC Roots 标记存活对象。
- 使用复制算法,将存活对象从 Eden 区复制到 Survivor 区。
- 清空 Eden 区和已使用的 Survivor 区。
- 恢复应用程序线程。
Major GC(老年代回收):
- 初始标记(STW):短暂暂停,标记 GC Roots 直接引用的对象。
- 并发标记:应用程序继续执行,多线程从初始标记的对象追踪所有存活对象。
- 重新标记(STW):短暂暂停,修正并发标记期间的引用变化(使用增量更新机制)。
- 并发清除:应用程序继续执行,清除标记为垃圾的对象(不进行内存整理,导致碎片)。
- 如果碎片过多,可能触发 Full GC(退化为 Serial Old 的单线程 标记 - 整理 算法)。
当然如果你一想就会发现CMS很逆天,Full GC既然已经STW了,为什么还要用Serial Old呢,明明这个也会STW为什么不并行整理呢,事实上这就是没人原因维护这个屎坑了, ParOld在JDK1.6+才有。其设计哲学就是低停顿,但是都已经沦落到Full GC就证明设计失败了,所以就直接摆烂了,这种设计被弃用还是普大喜奔的。
注意:CMS 已于 JDK 14 移除,不推荐新项目使用。
G1 GC 执行过程
G1(Garbage-First)将堆分为多个 Region(通常 1-32MB),逻辑上分代(Eden、Survivor、Old),回收优先处理垃圾最多的 Region。过程包括年轻代回收和混合回收,支持暂停时间目标控制。
Young GC(年轻代回收):
- STW 暂停应用程序线程(通常数十至数百毫秒)。
- 多线程从 GC Roots 标记存活对象。
- 使用复制算法,将存活对象从 Eden 和 Survivor Region 复制到新的 Survivor 或 Old Region。
- 清空原 Eden 和 Survivor Region。
- 恢复应用程序线程。
Concurrent Marking Cycle(并发标记周期,用于老年代准备):
- 初始标记(STW,通常借用 Young GC 完成):标记 GC Roots 直接引用的对象。
- 根区域扫描(并发):扫描 Survivor Region 中的根引用。
- 并发标记:应用程序继续执行,多线程追踪所有存活对象,记录跨 Region 引用(使用 Remembered Set)。
- 重新标记(STW):修正并发标记期间的引用变化(使用 Snapshot-At-The-Beginning)。
- 清理(STW):计算每个 Region 的存活率,更新统计信息。
Mixed GC(混合回收,包含年轻代和部分老年代 Region):
- STW 暂停应用程序线程。
- 选择垃圾最多的 Region(基于优先级)。
- 多线程复制存活对象到新 Region(整理碎片)。
- 清空原 Region。
- 恢复应用程序线程(可能分多次 Mixed GC 以控制单次暂停)。
ZGC 执行过程(分代模式,JDK 21+ 默认)
ZGC(Z Garbage Collector)设计为低暂停收集器,使用着色指针(Colored Pointers)和读屏障(Load Barriers)实现并发回收。JDK 21 后引入分代,支持年轻代和老年代独立回收,暂停时间通常 <10ms。
Generational Minor GC(年轻代回收):
- STW 暂停(通常 <1ms):根扫描,标记 GC Roots 引用的年轻代对象。
- 并发标记和重定位:应用程序继续执行,使用读屏障追踪和更新引用,将存活对象重定位到新地址(多视图内存映射)。
- STW 最终标记(<1ms):修正根引用。
- 清空原年轻代空间。
Generational Major GC(老年代/全局回收):
- STW 初始标记(<1ms):标记 GC Roots 直接引用的对象。
- 并发标记:应用程序继续执行,使用读屏障和着色指针标记存活对象。
- 并发重定位准备:计算重定位集。
- 并发重定位:应用程序继续执行,重定位存活对象到新地址(读屏障确保引用更新)。
- STW 最终重映射(<1ms):更新根引用。
- 清空原空间。
ZGC有一些黑话术语听不懂,我也懒得听懂了,我还年轻
TODO