欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 金融 > JVM梳理(逻辑清晰)

JVM梳理(逻辑清晰)

2025/5/22 13:16:02 来源:https://blog.csdn.net/weixin_45865163/article/details/148108516  浏览:    关键词:JVM梳理(逻辑清晰)

JVM总览

JVM 是什么,作用是什么

JVMJava Virtual MachineJava 虚拟机)是一个可以运行 Java 字节码的虚拟计算机

核心作用:实现跨平台(“一次编写,到处运行”),JVM 是平台相关的(Linux、Windows、macOS 都有对应实现),所以只要有 JVMJava 程序就能运行在任何平台上。

JVMJDKJRE 的关系

在这里插入图片描述

  • JVM(Java Virtual Machine)
    Java 虚拟机,负责运行 .class 字节码文件,是 Java 跨平台的关键。
  • JRE(Java Runtime Environment)
    Java 运行环境,包含 JVM + Java 标准类库,是运行 Java 程序所必需的环境,但不包含编译器
  • JDK(Java Development Kit)
    Java 开发工具包,包含 JRE + 开发工具(如 javac 编译器),用于开发和运行 Java 程序。

Java 程序的编译与执行过程

在这里插入图片描述

JVM的架构

在这里插入图片描述

Java虚拟机的架构中有三个主要的子系统

  • 类加载子系统
  • 运行时数据区域
  • 执行引擎

下面将依次介绍

类加载子系统

在这里插入图片描述

类加载的过程主要分为以下五点:加载、验证、准备、解析、初始化

加载(Loading)

通过类加载器读取 .class 文件字节流,并生成 Class 对象,每个加载器+每个类对应一个唯一Class对象

Class对象是什么?

Class 对象是 类的元数据 的载体,记录了一系列的元数据,大致了解一下:

分类里面装了什么?能拿来干嘛?(最常见)
身份
  • 类全名:java.util.List
  • 修饰符:public interface
  • 所属:ClassLoader、Module
判断是不是 public,做安全/模块检查
结构
  • 字段清单:名字、类型、修饰符、注解、初始值
  • 方法清单:参数、返回值、字节码、异常表、注解
反射调用方法、拿字段注解做依赖注入
关系
  • 父类指针:java.util.AbstractList
  • 接口数组:Serializable
  • 内部类 / 外部类
  • 泛型签名
判断继承、生成动态代理、泛型擦除后还原真实类型
常量池
  • 所有字面量:字符串、整型常量
  • 方法/字段符号引用
运行时解析方法调用、构造枚举/注解默认值
运行时状态
  • 静态变量实际值
  • <clinit> 是否跑完
  • JIT 优化 / 调试句柄
检查类是否已初始化、拿到或修改静态字段值

为什么需要Class对象

运行中的 Java 程序也能“看见、检查、操作”自己。

类加载的时机

  • 创建类的实例:new MyClass()
  • 访问类的静态成员变量(非常量),如果有static final int CONST = 3 ,访问CONST不会触发
  • 调用类的静态方法
  • 使用反射调用 Class.forName("全限定名")动态加载类
  • 子类被加载时,父类也被加载
  • JVM 启动时,含 main() 方法的类

类的加载是一个递归的过程,当一个类被加载后,它依赖的其他类也会被加载。

类加载器

类加载器类型中文名称职责/加载内容
BootstrapClassLoader引导类加载器加载 JDK 核心类,如 java.lang.*
ExtClassLoader扩展类加载器加载 JDK 扩展类,如 jre/lib/ext 下的类
AppClassLoader应用类加载器加载 classpath 下的用户类
User-Defined ClassLoader自定义类加载器用户自定义加载规则,如插件、热部署等

为什么需要自定义类加载器?

举一个常用的场景:热部署:服务器正在跑,代码改了,想“无缝”更新到最新逻辑,而 不停止 JVM

在正常情况下,AppClassLoader 已缓存旧类,无法再加载同名新字节码。下面简单代码演示:

// 自定义类加载器public class ClassReloader extends ClassLoader {private final Path classesDir;public ClassReloader(Path classesDir) {super(ClassReloader.class.getClassLoader());   // 父加载器 = Appthis.classesDir = classesDir;}/** 对业务包 com.example.* 不委派,其他包照旧双亲委派 */@Overrideprotected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {if (name.startsWith("com.example.")) {// 1) 先看自己是否已加载Class<?> c = findLoadedClass(name);if (c == null) c = findClass(name);        // 2) 读取最新字节码if (resolve) resolveClass(c);return c;}// 公共库 → 仍走父加载器return super.loadClass(name, resolve);}/** 读取字节码 → defineClass */@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {Path file = classesDir.resolve(name.replace('.', '/') + ".class");byte[] bytes = Files.readAllBytes(file);return defineClass(name, bytes, 0, bytes.length);} catch (Exception e) {throw new ClassNotFoundException(name, e);}}
}//主程序
public class ReloaderDemo {public static void main(String[] args) throws Exception {Path classesDir = Path.of("classes");          // 监听此目录while (true) {// ① 创建新的加载器实例ClassLoader loader = new ClassReloader(classesDir);// ② 反射调用最新业务类Class<?> helloClz = loader.loadClass("com.example.Hello");Method say = helloClz.getMethod("say");Object hello = helloClz.getDeclaredConstructor().newInstance();say.invoke(hello);                         // 打印版本号// ③ 放掉引用,等待 GC 卸载旧加载器 + 旧类hello = null; helloClz = null; say = null; loader = null;System.gc();Thread.sleep(3000);                        // 3 秒一次}}
}

总结:自定义类加载器,指定哪些包下面的类不走双亲委派机制,其余类仍然走双亲委派机制;监听指定包下的类,一旦发生变化就新建类加载器实例重新加载该目录下的类(new ClassReloader(classesDir))。

双亲委派机制

核心思想:一个类加载请求会先交给父加载器处理,只有父加载器找不到该类时,子加载器才会尝试自己加载。

流程:

子加载器收到加载请求↓
委托给父加载器加载↓
父加载器继续向上委托,直到启动类加载器↓
如果父加载器能加载,则返回成功↓
父加载器找不到才由当前加载器自己去加载

作用(为什么需要双亲委派机制):

  • 避免类重复加载,保证类唯一性:避免不同的类加载器加载同一个核心类(如 java.lang.String)产生冲突。
  • 保护核心 Java 类不被篡改,保证安全性:防止用户自定义类覆盖核心类,保障 JVM 基础功能的安全。
  • 提高类加载效率:父加载器已经加载的类,子加载器不用重复加载,减少资源消耗

总结:双亲委派机制保证了核心 Java 类的唯一性和安全性,同时避免重复加载,提高加载效率,使得类加载更加规范和稳定。

连接(Linking

  • 验证:校验.class文件是否合法
  • 准备:为静态变量分配内存,并设置默认值(0/false/null)
  • 解析:将常量池中的符号引用转成直接引用
    • 符号引用是.class文件常量池中的一种表示方式,用字符串符号来描述方法名、字段名、类名等,例如 "java/lang/String""length" 等。
    • 直接引用是JVM运行时可以直接使用的指针、内存地址或者偏移量

初始化(Initialization

执行 <clinit> 方法完成初始化

<clinit> 是编译器把所有 静态赋值static {} 代码块 拼接成的 隐藏方法,JVM 在“初始化阶段”自动调用它来完成类的一次性初始化。

一个类(或接口)对应 一个 <clinit>;初始化的顺序和源码完全一致;如果类里什么静态语句都没有,就根本不会生成 <clinit> 方法。

运行时数据区域

组成部分

在这里插入图片描述

图片来自:https://blog.csdn.net/qq_63218110/article/details/130601425

线程私有区:

(1)虚拟机栈:线程每次调用方法都会在虚拟机栈中产生一个栈帧,方法执行完毕后释放栈帧

区域作用
局部变量表保存方法体中声明的局部变量、返回值、方法参数
操作数栈字节码指令的“工作栈”,执行 push/pop 做计算
动态链接指向运行时常量池的符号引用,用于解析字段/方法
方法返回地址调用完成后,告诉 JVM 跳回到哪条字节码
额外信息对象头、锁记录(同步方法)、异常处理表

(2)本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机执行 Native 方法服务。 Native 方法是用 C/C++ 等非 Java 语言实现的方法,比如 JNI(Java Native Interface)调用的底层代码。

(3)程序计数器:保存当前线程所正在执行的字节码指令的地址(行号),方便线程切回后能继续执行代码

线程共享区:

(4)堆区:Jvm进行垃圾回收的主要区域,存放对象实例,分为新生代和老年代

参数默认值说明
新生代大小(NewSize约占堆的1/3包含 Eden 区和两个 Survivor 区
老年代大小约占堆的2/3存放长期存活的对象
Survivor 区比例(SurvivorRatio8(默认)Eden 和两个 Survivor 区比例是 8:1:1

堆内存分配流程

  • 对象创建优先在新生代 Eden 区分配
  • Eden 区满触发 Minor GC,存活对象移至 Survivor
  • 多次 Minor GC 存活后晋升到老年代
  • 老年代满触发 Full GC,清理更复杂,回收更彻底

(5)方法区:存储已被虚拟机加载的 类信息、字段信息、方法信息、类的运行时常量池、静态变量、即时编译器编译后的代码缓存等数据

内容类别说明
类的运行时常量池存放类编译期间生成的各种字面量和符号引用
字段和方法数据类的字段、方法信息,包括方法字节码和修饰符
静态变量类的静态成员变量
即时编译后的代码JIT 编译后的本地机器码

JDK1.8之前用永久代实现,JDK1.8及以后用元空间实现,元空间使用的是本地内存,而非在JVM内存结构中

垃圾回收机制

GC如何判断对象可以被回收?

引用计数法:为每个对象添加引用计数器,引用为0时判定可以回收,会有两个对象相互引用无法回收的问题

可达性分析法:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,凡被访问到的对象即标记为“可达”,遍历结束后,未被标记的对象即“垃圾”

哪些对象可以作为 GC Roots 呢?

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类的静态变量引用的对象
  • 方法区中常量池引用的对象
  • synchronized 关键字锁住的对象
  • 正在执行的类加载器实例

为什么这些对象可以被作为GC Roots?

  • 它们都是当前程序运行时直接使用中的对象
  • 它们是 Java 程序运行的基础,比如类加载器
  • 无法再“找引用”定位它们,所以必须作为起点从它们出发找其他引用

四大引用

  • 强引用:new出来的对象。哪怕内存溢出也不会回收

  • 软引用:通过softreference类实现,只有内存不足时才会回收

    SoftReference<Object> softRef = new SoftReference<>(new Object());
    
  • 弱引用:通过weakreference类实现,每次垃圾回收都会回收

    WeakReference<Object> weakRef = new WeakReference<>(new Object());
    
  • 虚引用:不能单独使用,必须配合引用队列使用,一般用于用于监控对象被回收的时机

    PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue);
    

垃圾收集算法

标记-清除算法(老年代)

标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象

缺点:标记清除后会产生大量不连续的内存碎片

复制算法(新生代)

为了解决标记-清除算法的内存碎片问题,它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉

缺点:可以使用的内存减少了,以及不适合老年代:存活对象数量比较大,复制性能会变得很差

新生代使用复制算法的流程:

步骤描述
对象分配新对象优先分配在 Eden 区。Eden 空间不足时触发 Minor GC。
触发 GC(STW)JVM 暂停所有用户线程(Stop-The-World),开始进行 Minor GC,扫描 Eden 区和当前使用的 Survivor 区(如 S0)。
复制存活对象将 Eden 区和 S0 区的存活对象复制到 S1 区。若 S1 不够放,部分对象则直接晋升到老年代。
清空 Eden 和 S0复制完后,Eden 和 S0 清空,下次 GC 时 S0 与 S1 角色互换,即下次从 Eden + S1 复制到 S0)。
对象年龄增长每次存活被复制后,对象年龄 +1。达到阈值(默认 15),对象会晋升到老年代

标记-整理算法(老年代)

标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,之后将所有存活对象向内存的一端移动,移动完成后,直接清理掉边界以外的内存

垃圾收集器

Serial 收集器:适用于新生代,使用复制算法,单线程执行,需要STW(Stop-The-World),适合单核 CPU 或小型应用

STW(Stop-The-World)是指垃圾回收时暂停所有应用线程

Serial Old 收集器:Serial 的老年代版本,与 Serial 配合使用,使用标记-整理算法

ParNew 收集器:适用于新生代,是Serial收集器的多线程版本,使用复制算法,常与 CMS 搭配

Parallel Scavenge 收集器:适用于新生代,多线程,使用复制算法,以吞吐量为目标

吞吐量 = 应用运行时间 ÷(应用运行时间 + 垃圾回收时间)
吞吐量越高,应用运行时间占比越大

Parallel Old 收集器:Parallel Scavenge 的老年代版本,使用“标记-整理”算法。

重点讲解下面两种收集器:

CMS 收集器(Concurrent Mark Sweep
  • 老年代收集器,并发收集,使用标记-清除算法

  • 以获取最短回收停顿时间为目标

    低停顿时间 = 更频繁、更小批次的 GC → 更加打断式,应用响应更快,但GC 总时间可能变多 → 吞吐量下降。
    高吞吐量 = 更少、更大批次的 GC → 更加集中式,GC 总时间少,但每次 GC 停顿时间长,响应差。
    
  • 可以让垃圾收集线程与用户线程(基本上)同时工作

CMS 垃圾收集器的工作可以分为以下四个主要阶段

  1. 初始标记(Initial Mark):标记所有直接可达的对象(即从 GC Roots直接引用的对象)。此阶段需要 STW(Stop-The-World),暂停所有应用线程。

  1. 并发标记(Concurrent Mark:从初始标记的对象开始,进行全堆扫描,标记所有可达对象(存活对象)。不会暂停应用线程,与应用程序线程并发执行

  1. 重新标记(Remark:修正并发标记阶段中,由于应用线程继续运行而导致的对象引用变化(新增或移除引用)。此阶段需要 STW。使用增量更新(Incremental Update

  1. 并发清除(Concurrent Sweep:回收不可达对象占用的内存空间;不会暂停应用线程,与应用线程并发执行。

CMS 的缺点

  1. 内存碎片问题::CMS 使用“标记-清除”算法,回收后不会整理内存,因此可能导致大量的内存碎片。当内存碎片过多时,可能触发 Full GC。
  2. 需要更多的 CPU 资源:并发标记和并发清除阶段会占用部分 CPU 资源,可能影响应用性能。
  3. **浮动垃圾:**并发清除阶段,应用线程继续运行,可能会产生一些新垃圾对象(浮动垃圾),需要等到下次 GC 才能被清理。
  4. 失败风险:如果老年代内存不足, CMS 的回收速度又跟不上新垃圾产生的速度,JVM 会放弃 CMS,而触发一次“Full GC”,由 Serial Old 收集器执行。Full GC(完全垃圾收集),使用一个单线程、停顿时间长的 Serial Old 收集器,来强制清理老年代。
G1 收集器
  • 采用标记-整理 + 复制算法来分代回收垃圾,JDK9中成为了默认的垃圾收集器
  • 可以预测停顿时间,允许用户设置 -XX:MaxGCPauseMillis 来控制最大停顿
  • G1 通过收集“回收性价比最高”的 Region,提升效率

堆内存结构:堆内存会被切分成为很多个大小相等的区域(Region),每个 Region 被标记了 E、S、O 和 H

Humongous 区域主要是为存储超过 50% 标准 region 大小的对象设计,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC 。

在这里插入图片描述

停顿预测模型

G1会根据用户设定的最大停顿时间优先选择回收性价比高的Region进行回收,G1 会为每个 Region 维护一个 “回收性价比”:回收它可以释放多少空间,需要耗费多长时间。G1 通过历史数据预测回收时间。

回收过程:

  1. 初始标记:标记一下GC Roots能直接关联到的对象。
  2. 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。
  3. 最终标记:暂停应用,补充并发阶段遗漏的对象引用,使用原始快照(Snapshot-At-The-Beginning, SATB) 算法
  4. 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划
增量更新(Incremental Update) 和 原始快照(Snapshot-At-The-Beginning, SATB)

增量更新:记录新增的引用,不记录删除的引用;并发标记阶段使用Card Marking(卡表)技术标记哪些内存区域被修改过

原始快照:记录删除的引用,不记录新增的引用,新增引用交给后续的GC正常处理。并发标记阶段如果发现某个引用被删除(A.field = null),则将该引用加入 SATB 队列

内存溢出和内存泄漏

内存泄漏:有对象不再使用但仍被引用,JVM无法回收,内存泄漏最终可能导致内存溢出

内存溢出:JVM所有内存都被用完,无法继续分配新内存

内存溢出

  • 堆内存溢出java.lang.OutOfMemoryError: Java heap space:大量对象创建且未被回收
  • 栈溢出java.lang.StackOverflowError:方法调用次数过多,一般是递归不当造成
  • Metaspace 溢出java.lang.OutOfMemoryError: Metaspace:类的数量或元数据过多

内存泄漏

  • 静态集合引起的内存泄漏:静态集合(如 static HashMapstatic List)的生命周期与 JVM 一致,若向其中添加对象后未及时移除,即使对象已不再使用,也会因被集合引用而无法回收。
  • 资源未关闭:数据库连接(Connection)、文件流(FileInputStream)、网络连接等未显式关闭,导致底层资源未释放
  • ThreadLocal 使用不当:使用完之后没有调用remove()方法清理
  • 字符串拼接导致的内存泄漏:有大量字符串拼接时,可能会产生大量中间对象
  • 缓存没有设置上限或者没有设置缓存淘汰策略
  • 非静态内部类持有外部类的引用:每个非静态的内部类都隐式的持有外部类实例的引用,若内部类被长生命周期对象(如线程池)引用,会导致外部类实例无法回收。解决方法是改为静态内部类,静态内部类不会持有外部类实例的引用。

执行引擎

架构

在这里插入图片描述

解释器

逐行解释执行字节码(.class 文件)指令边解释边执行,特点是启动快,执行慢

在JVM中冷代码(只执行一次或执行很少)有解释器直接执行,热代码交给即时编译器编译成本地机器码执行

即时编译器

热点代码编译成本地机器码,直接执行机器码;特点是启动慢(编译需要的时间长),执行快

怎么判定热点代码?

  • 方法调用计数器:某方法或代码块调用次数达到阈值
  • 回边计数器:如果某条跳转指令从后面跳回前面(比如进入一个循环体),这条跳转就是回边。回边计数达到阈值触发JIT编译。

分析器是一个性能监控组件,负责在程序运行期间 收集和分析运行数据:方法调用次数、回边次数等

两者并存是为了平衡启动速度和执行效率

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词