JVM 类加载过程/对象创建过程/双亲委派机制/垃圾回收机制
目录
一 类加载过程
二 JVM内存模型
三 对象的创建过程
四 双亲委派机制
五 垃圾回收机制
1 标记-清除
2 标记-复制
3 标记-整理
六 垃圾回收器
1 早期 Serial + Serial Old
2 中期 Parallel Scavenge + Parallel Old
3 过渡期 :CMS + ParNew
4 最新:G1
一 类加载过程
类加载的过程是指将类的.class文件加载到JVM内存当中,并对数据进行处理,最终形成可以被JVM使用的Java的类型的过程。
总结流程:
加载:找
.class
文件 -> 读到内存(方法区) -> 在堆里创建Class
对象。链接:
验证:检查
.class
文件是否合法、安全。准备:为
static
变量在方法区分配内存并设置默认初始值 (0,false
,null
)。(static final
常量(编译期可知)则在此阶段直接赋指定值。)解析:把常量池里的符号引用(名字)替换成直接引用(内存地址/偏移量)。
初始化:执行
<clinit>()
方法,给static
变量赋程序设定的值,执行static {}
块中的代码。(首次主动使用时触发)
<clinit>
是 JVM 自动生成的类初始化方法,在类加载的“初始化”阶段执行,用于执行静态变量赋值和静态代码块,在这个阶段才会把用户指定的初始值覆盖掉之前准备阶段赋予的零值。
面试官问题:对于类加载的过程,你是如何理解的?
我:首先,类加载的核心目的是将类的.class文件加载到JVM内存当中,对数据进行操作后形成可以被JVM使用的Java的类型的过程。这部分操作分为三个阶段,加载,链接,初始化,而链接又分为验证,准备,解析。加载阶段JVM会寻找Class文件,将类的字节码数据(包括类结构信息,字段,方法,常量池等)解析后存入方法区当中,在堆当中创建Class对象,这个对象是java.lang.Class的实例,作为反射入口并提供访问方法区当中类元数据的接口。链接当中的验证阶段会对应的检查.class文件是否安全合法,准备阶段会对static变量在方法区分配内存,并设置默认值,解析阶段会将方法区类的常量池的符号引用转变为直接引用(存在延迟解析策略),初始化阶段JVM会执行<clinit>()方法,会给static变量赋予程序设定的值(执行存在static的代码块)。
补充
- 加载阶段会将Class文件中的常量池内容加载到方法区当中变为运行时常量池
- static final在主备阶段就会为变量赋予程序设定的值
- 实例变量及其所属的对象数据,只有在程序执行到
new
指令时才会在堆中动态分配内存并初始化。 main
方法作为Java程序的执行入口,其栈帧在虚拟机栈中创建,负责启动程序逻辑,并可能触发后续的对象创建和堆内存分配。- 延迟解析是JVM内部的自动化优化策略,目的是避免在类加载阶段解析所有可能未使用的符号引用。
- 在JDK7及之前,永久代是对方法区的具体实现,并且永久代存储在堆内存当中,他的大小受JVM堆参数的限制。在JDK8及之后方法区这个概念依旧存在,但是不再使用永久代,方法区的实现被替代成元空间,元空间不再使用堆内存使用的是本地内存。
AI分点
它的核心目的是将编译后的Java类(.class
文件)加载到JVM内存中,经过转换和处理,最终形成JVM可以直接使用的Java类型(即Class
对象)。整个过程主要分为三个阶段:加载(Loading)、链接(Linking)、初始化(Initialization),其中链接阶段又细分为验证(Verification)、准备(Preparation)、解析(Resolution)。
-
加载:
-
JVM通过类加载器查找并读取
.class
文件的二进制字节流。 -
将字节流所代表的静态结构解析并转换为方法区(在HotSpot等JVM中常由元空间实现)内的运行时数据结构,存储类结构、字段、方法、常量池等信息。
-
在堆内存中创建一个
java.lang.Class
对象。该对象是访问方法区中类元数据的入口,也是Java反射机制的基石。 -
将Class文件中的常量池加载到方法区,转换为运行时常量池。
-
-
链接:
-
验证: 对加载的字节码进行严格检查,确保其符合JVM规范、格式正确、逻辑安全,不会危害虚拟机自身。
-
准备: 在方法区中为类变量(static变量)分配内存空间,并设置该数据类型的默认初始值(零值,如
0
,0L
,0.0f
,0.0d
,null
,false
)。(可选补充:对于final static
修饰的基本类型和String字面量常量,在此阶段就会被直接赋值为程序中定义的值)。 -
解析: 将运行时常量池中的符号引用(如类/接口全名、字段名和描述符、方法名和描述符)替换为直接引用(指向目标在内存中的具体指针、偏移量或句柄)。解析动作可以发生在初始化之前,也可能采用延迟策略,等到该符号引用第一次被主动使用时才进行。
-
-
初始化:
-
这是类加载的最后一步,开始执行用户定义的Java初始化代码。
-
JVM执行编译器自动生成的类构造器
<clinit>()
方法。这个方法由类中所有类变量赋值语句和静态代码块(static {}
块)按源代码顺序合并而成。 -
<clinit>()
方法的主要作用是为类变量赋予程序中定义的初始值(覆盖准备阶段设置的零值),并执行静态块中的逻辑。 -
关键点: JVM会确保一个类的
<clinit>()
方法在多线程环境下被正确地加锁同步(线程安全),且只被执行一次。
-
至此,类就完成了加载过程,可以被JVM用来创建实例、访问静态成员、调用方法等。
二 JVM内存模型
JVM管理内存的物理划分,包含5个核心区域
堆 (Heap)
存储内容:所有对象实例和数组
方法区 (Method Area)
存储内容:类元信息、常量池、静态变量、JIT 编译代码
虚拟机栈 (JVM Stack)
存储内容:栈帧(局部变量表、操作数栈、动态链接、方法出口)
本地方法栈 (Native Method Stack)
存储内容:Native 方法(如 C/C++ 代码)的执行状态
程序计数器 (PC Register)
存储内容:当前线程执行的字节码指令地址
三 对象的创建过程
阶段 | 关键操作 | JVM 子系统 |
---|---|---|
1. 类加载检查 | 验证类是否加载,未加载则触发类加载过程 | 类加载器 |
2. 内存分配 | 在堆中分配内存(指针碰撞/空闲列表/TLAB) | 内存管理器 |
3. 内存初始化 | 所有字段置零值(0/null/false) | 执行引擎 |
4. 设置对象头 | 写入 Mark Word、Klass 指针等元数据 | 执行引擎 |
5. 执行 <init> | 初始化字段 → 构造代码块 → 构造函数(父类优先) | 执行引擎 |
6. 建立引用关联 | 将堆中对象地址绑定到栈帧的局部变量 | 运行时数据区协作 |
四 双亲委派机制
阿里二面:双亲委派机制?原理?能打破吗?-CSDN博客
什么是双亲委派机制?
“类加载请求优先委派给父加载器”,只有所有父加载器都无法完成加载时(返回 null
或抛出 ClassNotFoundException
),子加载器才会尝试自己加载。
-
通过递归委派,最终由 最顶层的启动类加载器(Bootstrap) 优先尝试加载。
-
确保类的加载从最高层级向下传递,形成严格的层次结构。
解决的问题:
-
安全性:防止用户自定义类冒充核心类(如伪造
java.lang.String
)。 -
唯一性:避免同一个类被不同加载器重复加载(破坏
equals()
、instanceof
等行为)。 -
有序性:明确类加载的责任边界(如核心类 → 扩展类 → 应用类 → 自定义类)。
通过这种机制,Java实现了类加载的层次结构。它可以确保类的加载是有序的(从最高级的类加载器向下),避免了重复加载、可以保证安全性,确保Java当中的核心类库,只能由启动类加载器加载,从而防止用户自定义同名类被加载。并且可以自定义类加载器,实现特定的加载策略。
每个类加载器有独立的加载范围:
-
启动类加载器:加载
JRE/lib
核心库(如rt.jar
) -
扩展类加载器:加载
JRE/lib/ext
扩展库 -
应用类加载器:加载用户类路径(
-classpath
指定的路径)-
自定义类加载器:开发者自定义的路径(如网络、加密文件等)
-
当需要加载一个类时,子类加载器收到请求会向上委派,直到启动类加载器Bootstrap,加载成功则返回结果,失败将返回null表示无法加载,下一级接收到返回值null,然后在下一级类加载器的路径下尝试,如果所有的父类都失败,子类加载器调用自身的findClass()进行加载,如果自身的加载机制仍然无法加载该类,则会抛出ClassNotFoundException异常。
打破双亲委派机制:
为何:标准的双亲委派模型在某些场景下不够灵活
方案:可以自定义一个类加载器,继承自ClassLoader类,并重写loadClass方法。在LoadClass方法当中我们可以自定义类的加载逻辑。
五 垃圾回收机制
概念:垃圾回收是JVM的一种内存管理机制,他会自动回收不再被使用的对象占用的内存空间,从而避免手动释放内存的操作。
在堆内存当中,从垃圾回收的范围上说,一般分为两种,正对新生代的垃圾回收动作,叫做MinorGC(也叫做YoungGC),针对老年代的垃圾回收动作,叫做MajorGC,由于MajorGc发生的时候,通常也会伴随着MinorGC。FullGC(针对整个堆内存)
1 可达性分析算法思想
从一系列被称为GC Roots
的根对象出发,沿着对象之间的引用链进行搜索。所有能被GC Roots
直接或间接引用到的对象,就是存活对象
;反之,任何GC Roots
都无法到达的对象,就是可回收的垃圾对象
。
2 三种核心回收算法 (标记-清除 | 标记-复制 | 标记-整理)
这三种算法都是基于可达性分析来确定对象是否存活。
1 标记-清除
从GCRoots出发遍历整个对象图,标记出所有的存活对象(对象头设置标志位),扫描内存,将未被标记的对象占用的内存块加入空闲列表,产生不连续内存碎片。当碎片无法满足大内存分配时,触发 Full GC 并切换为标记-整理算法。
空闲列表是管理碎片化内存的核心数据结构,用于解决标记-清除算法产生的内存碎片问题。空闲列表是一个记录堆内存当中所有空闲内存块位置和大小的双向链表。
清除阶段不会物理擦除垃圾对象数据,而是将其占用的内存块加入空闲列表。后续分配新对象时,从空闲列表搜索合适碎片分配。若无足够连续碎片,则触发 Full GC 执行标记-整理算法重组内存。
2 标记-复制
将新生代内存分为三个区域,一个是Eden(伊甸区),一个是Survivor0(From空间),一个是Survivor1(To空间)。Eden满时触发MoniorGC,标记Eden区和From区的存活对象,将存活对象复制到To区,更新所有指向这些对象的引用地址,清空Eden区和From区,交换From/To的角色。
这里复制到To区会将年龄+1,达到阈值会晋升老年代。 “Eden 区占80%,Survivor0/1各占10%”
3 标记-整理
从GCRoots出发遍历整个对象图,标记所有的存活对象,将所有的存活对象向内存起始端滑动,使其连续排列,同时更新对象的内存引用。最后回收标记对象区域外的碎片空间。
六 垃圾回收器
垃圾回收器的发展:
1 早期 Serial + Serial Old
Serial 是工作在新生代的垃圾回收器(也称 Serial New),采用标记-复制算法;其搭档 Serial Old 负责老年代回收,采用标记-整理算法。两者均为单线程工作模式,垃圾回收时触发 STW(Stop-The-World)所有用户线程暂停,待回收完成后恢复。
2 中期 Parallel Scavenge + Parallel Old
作为 Serial 的多线程升级版,Parallel Scavenge(新生代)和 Parallel Old(老年代)在 STW 期间并行执行垃圾回收。
3 过渡期 :CMS + ParNew
ParNew是Parallel Scavenge的并发增强版,搭配CMS使用,CMS作为首个并发老年代收集器。
- 初始标记(STW):标记GCRoots直接关联的对象
- 并发标记:用户线程与标记线程并发执行
- 重新标记(STW):修正并发期间的引用变更。
- 并发清除:清理垃圾对象。(与用户线程并发执行)
4 最新:G1
传统分代实现的是老年代与新生代的物理隔离。
G1分区:Eden/Suriver/Old逻辑分代+物理统一(同一个区域的2024个)
java -XX:+PrintCommandLineFlags -version
CMD查看垃圾回收器版本
知识点补充: 实例变量与局部变量的区别
特性 | 实例变量 | 成员变量(广义) |
---|---|---|
修饰符 | 不能有 static | 可以包含 static 或非 static |
存储位置 | 堆(对象实例内部) | 静态变量在方法区,实例变量在堆= |