JVM4️⃣类加载技术
1、类加载阶段
类加载的阶段:加载、链接、初始化
1.1、加载
加载:将 Java 类的字节码载入方法区。
- 方法区内部采用 C++ 的 instanceKlass 描述 Java 类。
- 加载和链接可能交替运行,不一定是先后完成。
- 类加载时发现父类没有加载,则先加载父类。
浅聊 oop-klass 模型
以 JDK 8 为例。
在 Hotspot 中,使用 oop-klass 模型,用于表示 Java 类和对象。
- oop(ordinary object pointer):普通对象指针,标识对象实例。
- klass(instanceKlass):类的元数据,表示类的成员变量、方法、常量池等结构。
简单来说,就是 Java 面向对象中,类与实例的关系。
- 堆中的对象(xx.class)是一个个的实例。
- 实例所属的类(klass),位于方法区。
JVM 不把 instanceKlass 暴露给 Java,也就是说 Java 不能直接访问 instanceKlass。
那 Java 如何访问到 Klass?
使用 new 关键字创建对象时,JVM 会创建一个 instanceOopDesc 实例,用于表示 java.lang.Class 对象的实例。
- instanceOopDesc 实例,称为 instanceKlass 的 Java 镜像。
- 对象实例存放在堆中,对象实例的引用存放在栈中。
- 实例包括两部分信息
- 对象头:包含运行时数据,如多线程的锁信息
- 元数据:维护一个指针,指向对象所属类的 instanceKlass
- instanceKlass 中有一个
_java_mirror
属性,指向 instanceOopDesc 实例。 - 可以看出,instanceOopDesc 实例和 instanceKlass 之间互相持有对对方的引用。
1.2、链接
链接阶段:验证、准备、解析
- 验证:类是否符合 JVM 规范,安全性检查(e.g. 魔数 cafe babe)
- 准备:为 static 变量分配空间,设置默认值。
- 解析:将常量池中的符号引用解析为直接引用。
1.2.1、验证
类是否符合 JVM 规范,安全性检查。
如:检查魔数,是否为 cafe babe
1.2.2、准备(❗)
为 static 变量分配空间,设置默认值。
-
static 变量的存储位置
- < JDK 1.7:存储于 instanceKlass 的末尾(本地内存)
- >=JDK 7:存储于 instanceOopDesc 实例的末尾(堆)
-
赋值问题
-
在准备阶段,static 变量仅分配空间和赋默认初始值。
-
在初始化阶段,才对变量赋值。
// 举例 static Object o = new Object(); // 准备阶段:分配内存空间,赋初始值为null // 初始化阶段:赋值为new Object()
-
-
注意:若 static 变量是 final 的基本类型、字符串常量,则在准备阶段赋值。
1.2.3、解析
Oracle 官方文档中说到,常量池在最初都是符号引用。
解析阶段:将符号引用解析为直接引用。
-
符号引用
-
直接引用
1.3、初始化(❗)
初始化:调用
<cinit>()V
方法。
- JVM 会保证 <cinit>()V 的线程安全。
- 类初始化是懒惰的,可以理解为非必要不初始化。
1.3.1、触发时机
当主动引用一个类时,会触发初始化。
- main() 方法所在的类
- new 关键字创建对象
- Class.forName()
- 首次访问类的静态变量或静态方法(除 final 常量)
- 子类初始化,发现父类还没初始化:先触发父类的初始化
- 子类访问父类的静态变量:只触发父类初始化
1.3.2、何时不触发
当一个类被动引用时,不触发初始化。
- 访问类的静态常量(static final,基本类型或字符串)
- 获取某个类的 Class:类对象.class
- 创建某个类的数组
- Class.forName():第二个参数为 false,则触发加载,不触发初始化
- 类加载器的 loadClass():触发加载,不触发初始化
1.3.3、经典应用:单例模式
单例模式:静态内部类方式。
public final class Singleton{
private Singleton(){}
private static class LazyHolder{
private static final Singleton INSTANCE = new Singleton();
}
public Singleton getInstance(){
return LazyHolder.INSTANCE;
}
}
2、类加载器
类加载器:在类加载阶段,通过一个类的全限定名来获取此类的二进制字节流。
从 JVM 的角度,只有 2 种加载器
- 启动类加载器(Bootstrap ClassLoader):C++ 实现,属于虚拟机的一部分
- 其它类加载器: Java 实现(继承自抽象类
java.lang.ClassLoader
),独立于虚拟机,。
从 Java 程序员的角度,有 3 种加载器
在有需要的情况下,还可以编写自定义的类加载器。
说明:
- 启动类加载器无法被 Java 程序访问,显示为 null
- 应用程序类加载器是系统默认的类加载器
名称 | 默认加载范围 | |
---|---|---|
Bootstrap ClassLoader |
启动类(根) | JAVA_HOME/jre/lib/* |
Extension ClassLoader |
扩展类 | JAVA_HOME/jre/lib/ext |
Application ClassLoader |
应用程序类(系统类) | classpath |
2.1、双亲委派模型
The parent-delegation Model
双亲委派模型:调用类加载器的 loadClass() 方法时,查找类的规则。
-
亲:理解为上级比较好,而不是“父亲”(因为不是继承关系,而是级别关系)
-
双:理解为 2 个(应用程序类加载器,有 2 级垂直结构的上级,会向上委派两次)
2.1.1、工作机制(❗)
向上委派,向下加载
- 一个类加载器收到了类加载的请求。
- 把请求委托给父加载器,依次向上直到启动类加载器。
- 从启动类加载器开始:
- 根据类的全限类名,在加载范围内进行扫描查找。
- 找到则进行加载,否则由下一级类加载器尝试加载。
- 如果所有类加载器都无法完成加载,则抛出异常。
2.1.2、源码:loadClass()
-
name:类的二进制名
-
resolve:若 true 则解析类(链接阶段)
2.1.3、源码分析(❗)
大处着眼:方法结构
方法体:分为以下几个部分
-
findLoadedClass()
:根据类名,查找对应的已加载类 -
if (c == null)
:检查类是否已加载,未加载则进入 if 块。 -
if (resolve)
:根据方法调用传递的参数,判断是否要解析类。 -
返回被加载的类,若无法加载则为 null。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null){ ... } if (resolve) { resolveClass(c); } return c; } }
小处着手:具体代码(❗)
类加载的核心代码:if (c == null) 块
分为 try 块和 if 块,分别对应双亲委派模型的 “向上委托” 和 “向下加载”
- try 块:向上委托,直至启动类加载器
- 若 parent == null,说明上级是启动类加载器
- 此时调用 findBootstrapClassOrNull(),查找启动类加载器中的对应加载类(根据全限类名)
- 若返回被加载的类,说明 c 已被启动类加载器加载。
- 若返回 null,说明启动类加载器无法加载当前类。
- if 块:向下加载
- 若 c == null,说明上级类加载器无法完成类加载操作。
- 调用 findClass(),则当前的类加载器尝试去加载类(根据全限类名扫描)
- 若加载成功,则返回被加载后的类。
- 否则,抛出 ClassNotFoundException 异常
- (下一级类加载器会捕获异常,但不做任何操作)
可以看出:双亲委派模型就是从应用程序类加载器开始,递归向上级调用 loadClass() 方法。
- 只要在某次递归中,
c != null
说明 c 已被加载。 - 若递归结束,
c == null
说明 c 无法被加载,抛出 ClassNotFoundException 异常。
类解析:
if (resolve)
根据调用 loadClass() 时传递的参数。
-
若显式指定 resovle == true,则会触发类链接。
-
若显式指定为 false 或不指定,则不会触发类链接
// loadClass()的重载方法,默认为false public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
2.2、典例:自定义String
2.2.1、示例
自定义 java.lang.String
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("I'm the true String, haha!");
}
}
执行 main()
2.2.2、分析
main() 所在的类会触发初始化,而初始化之前需要经过加载和链接。
- 类加载器收到类加载请求,依次向上委托父加载器,直到启动类加载器。
- 启动类加载器:扫描
JAVA_HOME/jre/lib/*
中的类,发现全限类名为java.lang.String
的类加载。 - 类加载阶段完成,执行 main() 方法。
- 此时加载的是真正的
java.lang.String
,其中没有定义main()
,因此报错。