JVM 中类的加载
简介
从编译后的 class
文件到内存中的类,需要经过加载,链接以及初始化三个步骤。链接需要经过验证,而内存中的类没有初始化,也无法使用。
加载 —— 双亲委派模型
在说明加载前,需要理解双亲委派模型是个啥玩意儿。
子-类加载器
需要加载某个 class
文件时,会将这个任务委派给他的 父-类加载器
,然后递归这个操作,如果 父-类加载器
没有加载,则 子-类加载器
才会去加载。
(注: 上面的类加载器的父子关系并不是 java 语言的继承关系,只是一种组合关系,或者说层级关系)
类的唯一性
类加载器名称 + 类全限定名称
类加载器的类别
BootstrapClassLoader(启动类加载器)
启动类加载器是用 C++
实现的,没有对应的 Java 对象,因此在 Java 中只能用 NULL 来指代。相当于这个加载器是最顶级的加载器,不能直接通过引用进行操作。
该加载器加载 Java
的核心库 java.*
,构造 ExtClassLoader
和 AppClassLoader
。
ExtClassLoader(标准扩展类加载器)
ext:extension
由 Java
编写,用于加载扩展库,比如 classpath
中的 jre
,javax.*
或者 java.ext.dirs
指定位置中的类,可以直接调用该类加载器。
AppClassLoader(引用类加载器)
同样是用 Java
编写,加载程序所在的目录。
CustomClassLoader(用户自定义类加载器)
也是用 Java
编写的,用于加载指定路径的 class
文件。
以上的类加载的层级是:CustomClassLoader < AppClassLoader < ExtClassLoader < BootstrapClassLoader
网络资料:Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载
ClassLoader 源码分析
1 | protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
流程图
链接
链接阶段主要分为三个阶段:
-
验证
验证的目的是确保被加载的类满足 JVM 的约束条件。
-
准备
为被加载类的静态字段分配内存,对于静态字段的初始化,会在初始化阶段中进行。
如果字段被
final
修饰,在编译的时候会给字段添加 ConstantValue 属性,在准备阶段完成赋值。 -
解析
在
class
加载到JVM
前,类中对于其他类、方法或者字段是不知道具体地址的,甚至不知道自己的方法和地址;所以编译器会给这些成员生成一个符号引用,符号引用放在class
文件的常量池中,而解析阶段就是将这些符号引用解析成实际引用。
初始化
初始化一个静态字段,可以在声明该静态字段时就直接复制,也可以在静态代码块中对其赋值。
被 final
修饰的静态代码块在 Java
中被定义为常量,常量直接由 JVM
进行初始化;而常量以外的静态字段全部被编译器放置到一个名为 clinit
的方法中。
真正进入初始化后,会对常量字段进行赋值,执行 clinit
方法对静态字段或者静态代码块进行初始化。
对于类初始化的条件,JVM
枚举了一下触发条件:
- 虚拟机启动时,初始化指定的类;
new
一个对象时,初始化目标类;- 静态方法调用或者静态字段访问时会初始化目标类;
- 子类初始化会先初始化父类;
- 如果接口实现了
default
方法,直接或间接实现该接口的类初始化,该接口也会初始化; - 使用反射时也会初始化被反射的类;
- new、getstatic、putstatic、invokestatic
(补充:类的初始化是线程安全的)