JVM——八股文
1. JDK, JRE和JVM的关系
- JDK = JRE + Java开发工具
- JRE = JVM + Java核心类库
JDK供Java程序开发人员开发软件,JRE供客户使用,只需要JVM运行环境即可。
JVM运行的是class字节码,不仅能运行Java代码,还能运行其他语言,只要语言能编译成字节码文件即可,比如Kotlin。
2. JVM的主要组成部分
- 类加载器
- 加载Loading
- 链接Linking
- 初始化Initialization
- 运行时数据区
- 执行引擎
- 本地接口
3. 你能解释一下JVM类加载器的作用吗
1. 加载 (Loading)
- 目的:将类的
.class
文件的二进制数据读入内存,并在方法区中创建一个java.lang.Class
对象来表示这个类。 - 主要工作:
- 通过类的全限定名获取其二进制字节流:这可以通过多种方式实现,比如从本地
.class
文件、JAR 包、网络、动态生成(如Proxy
类)、数据库等。 - 将字节流代表的静态存储结构转化为方法区的运行时数据结构:JVM 解析字节码,并在方法区(Method Area)或元空间(Metaspace,Java 8+)中创建该类的数据结构。
- 在内存中创建一个
java.lang.Class
对象:这个对象是java.lang.Class
的实例,它作为程序访问该类各种数据的入口。这个对象通常存储在堆(Heap)中。
- 通过类的全限定名获取其二进制字节流:这可以通过多种方式实现,比如从本地
- 关键点:
- 这个阶段主要由类加载器(ClassLoader) 完成。
- 类加载器遵循双亲委派模型(Parent Delegation Model),即先委托父类加载器尝试加载,只有当父类加载器无法完成时,子加载器才会尝试自己加载。
2. 链接 (Linking)
链接阶段确保加载的类是正确且符合 JVM 规范的,并为其分配内存。它分为三个子阶段:
(1) 验证 (Verification)
- 目的:确保
.class
文件的字节流包含的信息符合当前 JVM 的要求,不会危害 JVM 的安全。 - 主要检查:
- 文件格式验证:检查字节流是否符合
.class
文件格式规范(如魔数0xCAFEBABE
、版本号等)。 - 元数据验证:检查类的元数据信息是否有矛盾(如是否继承了
final
类、是否实现了不存在的接口等)。 - 字节码验证:这是最复杂和关键的一步。通过数据流和控制流分析,确定字节码指令不会做出危害 JVM 安全的操作(如类型转换错误、非法跳转、访问不存在的字段等)。
- 符号引用验证:确保解析动作能正常执行(如检查符号引用中描述的类、字段、方法是否存在)。
- 文件格式验证:检查字节流是否符合
- 重要性:这是 JVM 防止恶意代码攻击的重要屏障。虽然验证很耗时,但可以保证运行时的安全性。
(2) 准备 (Preparation)
- 目的:为类的静态变量(
static
fields)分配内存,并设置这些变量的初始值。 - 关键点:
- 分配内存:在方法区(或元空间)为
static
变量分配内存。 - 设置初始值:这里的“初始值”通常是零值(zero value),而不是你在代码中赋的值。
int
类型的static
变量初始值为0
。boolean
类型的static
变量初始值为false
。- 引用类型(
Object
)的static
变量初始值为null
。
final static
常量:如果static
变量同时被final
修饰,并且是基本类型或String
字面量,那么它的值(编译期常量)会在这个阶段直接赋值,而不是零值。例如:public static final int MAX = 100;
的值100
会在准备阶段就设置好。
- 分配内存:在方法区(或元空间)为
(3) 解析 (Resolution)
- 目的:将常量池内的符号引用(Symbolic References)替换为直接引用(Direct References)。
- 概念解释:
- 符号引用:以一组符号来描述所引用的目标。它可以是任何形式的字面量,只要能无歧义地定位到目标即可。例如,常量池中用
类名.方法名.描述符
来表示一个方法。 - 直接引用:可以直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。直接引用是与内存布局相关的。
- 符号引用:以一组符号来描述所引用的目标。它可以是任何形式的字面量,只要能无歧义地定位到目标即可。例如,常量池中用
- 解析的内容:
- 类或接口解析:将符号引用解析为具体的类或接口的
Class
对象。 - 字段解析:将符号引用解析为字段在类中的内存偏移量。
- 方法解析:将符号引用解析为方法在方法表中的索引或直接指针。
- 接口方法解析:类似方法解析。
- 类或接口解析:将符号引用解析为具体的类或接口的
- 时机:解析动作不一定在链接阶段一次性完成,它可能在初始化之后才进行(称为“延迟解析”或“惰性解析”)。只有当真正需要使用某个符号引用时,才会触发解析。
3. 初始化 (Initialization)
- 目的:执行类的初始化代码,为类的静态变量赋予程序中指定的值,并执行
static
代码块。 - 主要工作:
- 执行
<clinit>()
方法。<clinit>
是由编译器自动收集类中所有static
变量的赋值语句和static
代码块中的语句合并产生的类构造器方法。 - 按照代码中出现的顺序执行这些初始化语句。
- 执行
- 关键点:
- 这是类加载过程的最后一步。
<clinit>()
方法是线程安全的:JVM 会保证一个类的<clinit>()
方法在多线程环境下只被执行一次。其他线程会阻塞,直到第一个线程完成初始化。- 触发时机:这是主动使用一个类的时刻。以下操作会触发类的初始化:
- 创建类的实例(
new
关键字)。 - 访问类的静态变量(
public static
除外,final static
编译期常量也不会触发)。 - 调用类的静态方法。
- 使用反射(
Class.forName()
)。 - 初始化一个类的子类(会先触发父类的初始化)。
- 虚拟机启动时,包含
main()
方法的主类。 MethodHandle
和VarHandle
的某些操作。
- 创建类的实例(
- 被动引用不会触发:访问
final static
编译期常量、通过子类引用父类的static
变量(只会触发父类初始化,不会触发子类)、数组定义(new MyClass[10]
不会触发MyClass
的初始化)等属于被动引用,不会触发初始化。
4. 使用 (Using)
- 目的:类初始化完成后,就可以被程序正常使用了。
- 工作:程序通过
new
创建对象、调用静态方法、访问实例方法等。
5. 卸载 (Unloading)
- 目的:当类不再被任何地方引用,满足垃圾回收条件时,JVM 可以卸载该类,回收其占用的内存(主要是方法区/元空间和
Class
对象本身)。 - 条件:非常严格。需要该类的
ClassLoader
被回收、该类的所有实例都已被回收、该类的Class
对象没有被任何地方引用。
总结流程图
加载 (Loading)↓
链接 (Linking)├── 验证 (Verification)├── 准备 (Preparation) <-- static 变量赋零值 (或 final static 常量值)└── 解析 (Resolution) <-- 符号引用 -> 直接引用↓
初始化 (Initialization) <-- 执行 <clinit>(), static 变量赋程序值, 执行 static 块↓
使用 (Using)↓
卸载 (Unloading) (可选)
4. 你知道JVM的类加载器有哪些?双亲委派机制是什么?
一、JVM 的类加载器 (Class Loaders)
JVM 在启动时会创建一系列的类加载器,它们形成了一个层次结构。主要的类加载器有三种(从顶层到底层):
1. 启动类加载器 (Bootstrap ClassLoader)
- 角色:最顶层的类加载器,是 JVM 自身的一部分,通常由 C/C++ 实现。
- 负责加载:
JAVA_HOME/lib
目录下的核心类库(如rt.jar
,tools.jar
,resources.jar
等)。- 或者被
-Xbootclasspath
参数指定的路径中的类库。
- 特点:
- 用 C/C++ 编写,不是 Java 类,因此在 Java 代码中无法直接引用它(
getClassLoader()
返回null
)。 - 负责加载最基础、最核心的 Java 类(如
java.lang.*
,java.util.*
,java.io.*
等)。
- 用 C/C++ 编写,不是 Java 类,因此在 Java 代码中无法直接引用它(
2. 扩展类加载器 (Extension ClassLoader)
- 角色:
Bootstrap ClassLoader
的子加载器,由 Java 实现。 - 负责加载:
JAVA_HOME/lib/ext
目录下的类库。- 或者被
java.ext.dirs
系统变量所指定的路径中的所有类库。
- 特点:
- 允许开发者将具有通用功能的 JAR 包放在这个目录下,自动被加载,无需在
-classpath
中指定。 - 在 Java 9 的模块化系统(JPMS)之后,其重要性有所下降。
- 允许开发者将具有通用功能的 JAR 包放在这个目录下,自动被加载,无需在
3. 应用程序类加载器 (Application ClassLoader) / 系统类加载器 (System ClassLoader)
- 角色:
Extension ClassLoader
的子加载器,也是 Java 实现。 - 负责加载:
- 用户类路径(ClassPath)上所指定的类库。
- 即我们通常通过
-classpath
或-cp
参数指定的.jar
文件或.class
文件目录。
- 特点:
- 这是默认的类加载器,我们编写的 Java 类和第三方依赖库(如 Maven/Gradle 依赖)通常由它加载。
- 可以通过
ClassLoader.getSystemClassLoader()
获取它的实例。
(可选) 自定义类加载器 (Custom ClassLoader)
- 角色:开发者可以继承
java.lang.ClassLoader
类来创建自己的类加载器。 - 目的:
- 从非标准来源加载类(如网络、数据库、加密的 JAR 包)。
- 实现类的隔离(如 Tomcat 的 Web 应用隔离、OSGi 模块化)。
- 实现热部署(Hot Swap)。
- 常用场景:Web 服务器(Tomcat, Jetty)、应用服务器(WebLogic, WebSphere)、插件化框架、热更新系统。
二、双亲委派机制 (Parent Delegation Model)
双亲委派机制是 JVM 类加载器加载类时遵循的一种工作模式。它的核心思想是:当一个类加载器收到类加载请求时,它不会自己先去加载,而是把这个请求委派给它的父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器。只有当父类加载器无法完成这个加载请求(即在它的搜索路径下找不到所需的类)时,子加载器才会尝试自己去加载。
工作流程
- 发起请求:假设应用程序类加载器(AppClassLoader)收到一个加载
java.lang.String
的请求。 - 向上委派:AppClassLoader 不会直接加载,而是将请求委派给它的父加载器——扩展类加载器(ExtClassLoader)。
- 继续委派:ExtClassLoader 收到请求后,也不会直接加载,而是继续委派给它的父加载器——启动类加载器(Bootstrap ClassLoader)。
- 顶层尝试加载:Bootstrap ClassLoader 尝试在
rt.jar
等核心库中查找java.lang.String
,找到了,于是加载成功,返回Class
对象。 - 逐层返回:加载结果从 Bootstrap ClassLoader 逐层返回给 ExtClassLoader,再返回给 AppClassLoader,最终返回给发起请求的代码。
如果父加载器找不到呢?
- 假设请求加载一个用户自定义的类
com.example.MyClass
。 - 请求最终传到 Bootstrap ClassLoader,它在核心库中找不到。
- Bootstrap ClassLoader 返回失败。
- 请求返回到 ExtClassLoader,它在
lib/ext
目录下也找不到。 - 请求返回到 AppClassLoader,它在 ClassPath 下找到了
com.example.MyClass.class
,于是由它自己加载。
为什么需要双亲委派机制?
避免类的重复加载:
- 保证一个类在 JVM 中只有一个唯一的
Class
对象。 - 例如,无论哪个类加载器发起加载
java.lang.Object
的请求,最终都会由 Bootstrap ClassLoader 加载,确保所有地方使用的都是同一个Object
类。
- 保证一个类在 JVM 中只有一个唯一的
保证核心类库的安全性:
- 这是最关键的一点。它防止了恶意代码通过自定义类加载器来替换核心 Java 类。
- 例如,你不能自己写一个
java.lang.String
类放在 ClassPath 下,期望它被加载。因为当请求到达时,Bootstrap ClassLoader 会先加载它自己的、可信的String
类,你的恶意类永远没有机会被加载。 - 这确保了 Java 核心 API 的稳定性和安全性。
如何打破双亲委派?
虽然双亲委派是默认和推荐的模式,但在某些特殊场景下需要打破它:
基础类型回调用户代码:
- 典型例子:
JNDI
(Java Naming and Directory Interface)。 JNDI
的核心类由 Bootstrap ClassLoader 加载,但它需要回调由应用程序提供的服务实现(SPI - Service Provider Interface)。- Bootstrap ClassLoader 无法加载应用类路径下的类。
- 解决方案:通过线程上下文类加载器(
Thread.currentThread().getContextClassLoader()
)。这个加载器通常被设置为 AppClassLoader。JNDI
核心代码可以通过它来加载用户实现的 SPI 类,从而“逆向”委托。
- 典型例子:
实现热部署/模块化:
- 典型例子:Tomcat, OSGi。
- 需要隔离不同 Web 应用或模块的类,避免相互影响和核心库冲突。
- 解决方案:自定义类加载器,并重写
loadClass()
方法,改变委派逻辑。例如,Tomcat 的 Web 应用类加载器会优先尝试自己加载 Web 应用的类(/WEB-INF/classes
,/WEB-INF/lib
),只有当自己找不到时,才委派给父加载器(打破了“先委派”的原则)。这实现了应用间的类隔离。
总结
- 类加载器:Bootstrap -> Extension -> Application -> Custom,形成层次结构。
- 双亲委派:加载请求优先向上委派给父加载器,父加载器无法完成时,子加载器才尝试自己加载。
- 优点:保证类的唯一性、核心类库安全。
- 打破场景:SPI(如 JNDI)、热部署/模块化(如 Tomcat, OSGi),通常通过线程上下文类加载器或重写
loadClass()
实现。