类加载机制
如何确定类的唯一性
如果加载类的类加载器 + 类的class文件相同则判定两个类型是同一个类
类加载器的初始化
在虚拟机启动时由C++代码初始化sun.misc.Launcher类,在该类的构造函数中初始化了ExtClassLoader和AppClassLoader
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
...
}
类是如何被加载的
调用Launcher类中loader属性的loadClass()方法,而loader属性在Launcher类初始化时被赋值为AppClassLoader,最终使用java.lang.ClassLoader的loadClass()方法加载类并返回被加载类的Class对象:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果没有找到要加载的类,由父加载器抛出异常
}
// 如果无法加载就使用当前对象加载
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
类加载器与双亲委派(基于JDK8)
类型 | 加载路径 | 语言 |
---|---|---|
BootstrapClassLoader | <JAVA_HOME>\lib或-Xbootclasspath指定的路径 | C++ |
ExtClassLoader | <JAVA_HOME>\lib\ext或被java.ext.dirs指定的路径 | Java |
AppClassLoader | ClassPath | Java |
根据以上类加载代码,双亲委派的流程如下图:
![](https://gulimall-54321.oss-cn-beijing.aliyuncs.com/20220818165319712.png)
好处:保证了在各种类加载器环境中都能够保证是同一个类
类的加载时机
《Java虚拟机规范》中并没有进行强制约束如何开始“加载”阶段,但规定了有且只有六种情况必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
-
遇到new、getstatic、putstatic或invokstatic这四条字节码指令,典型场景有:
-
使用new关键字实例化对象
-
读取一个类变量,即类中使用static关键字修饰的变量
注意!使用static和final共同修饰的字段在编译期已把结果放入常量池,这种情况除外
-
调用一个类型的静态方法
-
-
如果未进行初始化,使用java.lang.reflect包的方法对类型进行反射调用
-
初始化类型时,如果发现父类还未初始化
-
当虚拟机启动时,main()方法所在的类会先被初始化
-
JDK7新加入的动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种方法句柄,且这个方法句柄对应的类没有进行过初始化,则需要触发初始化
-
JDK8的默认方法,如果有这个接口的实现类发生了初始化,那该接口将优先触发初始化
类加载过程
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期将经历以下七个阶段:
类加载的全过程包括:加载、验证、准备、解析、初始化
图中的加载过程仅代表了这些过程按照这个顺序开始,但并不代表其中某一个过程结束后才会开始下一个过程。通常这些阶段都是穿插进行的,可以参考验证阶段符号引用验证来辩证。
-
加载:通过类的全限定名获取此类的二进制流,并将该二进制流所代表的存储结构转化为方法区的运行时数据结构,在内存中生成一个代表该类的java.lang.Class对象作为方法区这个类的各种数据的访问入口。
关于获取二进制流并没有强制约定必须从某个.class文件中获取,因此衍生出了各种获取二进制流的方式:
-
可以从ZIP包中读取或通过网络
-
可以通过网络传输获取
-
可以在运行时计算生成
…
-
-
连接
-
验证:
-
文件格式验证:验证字节流是否符合Class文件规范,并能被当前虚拟机处理,例如:是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机接受范围…
-
元数据验证:保证其描述的信息符合《Java语言规范》的要求,例如:是否有父类、是否继承了不被允许继承的类…
-
字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。例如:保证任何跳转指令都不会跳转到方法体以外的字节码指令上;
-
符号引用验证:校验该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。例如:符号引用中通过字符串描述的全限定名是否能够找到对应的类…
该阶段的校验行为将在解析阶段中发生
-
-
准备:为类变量(即static修饰的变量)分配内存并设置初始值
该阶段仅为类变量分配初始值,而不包括实例变量,实例变量的赋值是在对象实例化时
这里的初始值指的时0值而非指定值,例如:boolean 的0值为false,int 的0值为0等。
类变量的确切赋值动作要到类的初始化阶段才会被执行,但如果类变量被final修饰,会在字段属性表中生成ConstantValue属性,存在该属性在准备阶段就会对类变量进行确切赋值。
-
解析:Java虚拟机将常量池内的符号引用替换为直接引用
-
-
初始化:初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit>()并不仅是java代码层面的构造方法,而是通过Javac编译器的自动生成物。它包含以下细节:
-
包含由编译器自动收集的所有类变量的赋值动作和静态语句块中的语句;
编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
-
Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行;
-
<clinit>()方法对于类和接口来说不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,编译器可以不生成这个方法;
-
接口虽然不能使用静态语句块,但是可以对变量赋值,因此也会有<clinit>()方法,但与类不同的是,接口初始化时不会先执行父接口的<clinit>()方法,只有当父接口中的变量被使用时才会初始化,接口的实现类在初始化时也一样不会先执行接口的<clinit>()方法;
-
Java虚拟机会保证一个类的<clinit>()方法并发安全性,当多线程同时执行该方法时,只有一个线程会执行该方法,可能会造成阻塞。
-