类加载过程
类加载过程分为以下三个过程 加载 -> 链接 -> 初始化,而链接又可以分为三个过程 验证 -> 准备 -> 解析。
整个类加载过程入下图
类的整个生命周期在此基础上又加了两个过程 使用 -> 卸载。
-
加载
查找并加载类的二进制数据。 -
链接
- 验证
确保类的信息都是正确的,符合当前使用虚拟机的规范。- 文件格式验证:验证字节流是否符合Class文件的格式规范,且能被当前虚拟机处理
- 源数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范。。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的符合逻辑的。
- 符号引用验证:对类自身以外的信息进行匹配性校验。
- 准备
为类的静态变量分配空间(方法区)并初始化为默认值。
需要注意一下几点- 对于基本类型来说,类变量和全局变量如果不显式的对其赋值而直接使用,系统则为其赋值为默认的零值;对于局部变量在使用之前必须显式的为其赋值,否则编译不通过。
- 对于同时被final和static修饰的常量,必须在声明的时候就为其显式的赋值,否则编译不通过;而只被final修饰的常量既可以在声明是显式的赋值,也可以在类初始化时显示的复制,总之在使用之前必须显式的复制。
- 对于引用类型,如果没有对其显示的赋值而直接使用,系统会为其赋值为默认的零值。
- 对于数组在初始化时没有对数组中的各个元素复制,那么系统会根据数据对应的元素类型赋值默认的零值
- 解析
将符号引用转为直接引用(地址引用)的过程。直接引用就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。解析主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用进行。
- 验证
-
初始化
JVM对类初始化,为类的静态变量赋正确值初始值。对类变量进行初始值设置有两种方式:- 声明类变量时指定初始值;
- 使用静态代码块为类变量指定初始值。
类初始化时机
只用当类主动使用的时候才会导致类的初始化。主动是要包含一下6种:- 创建类的示例,new;
- 访问某个类或接口的静态变量,或者对静态变量赋值;
- 调用类的静态方法;
- 反射;
- 初始化某个类的子类;
- Java虚拟机启动是被表名为启动类的类,直接使用java.exe命令来运行某个类。
如果大家觉得对于类的整个生命周期不方便我们记忆,我们把它想象成购物买手机的过程就很好记忆了。
故事是这样的…
故事是这样的…
张三存了好久的钱想买一个手机,于是他在网上买了一个IPhone13 Pro Max 1TB 远峰蓝手机,下单就是(加载);
怀着激动心情等了几天终于收到新手机(链接);
收到手机后我们肯定不能直接使用,还有一些使用前的流程,看看手机屏幕有没有刮痕,电池有没有鼓包等等(验证);
如果没有问题,我们贴膜,把手机卡从旧手机中拿出来,放到新手机中等等(准备);
然后开机,开机过程中会解析我们的手机卡是移动、联调亦或者电信(解析);
正常开启后我们就需要对手机进行数据同步呀等等(初始化);等都操作完我们就可以开心的用新手机了(使用);
过了些许时间,张三又买了其他的手机,于是就把这个手机给卖了(卸载);
至此该手机的使命就完成。
于是整个流程就是
- 下单(加载)
- 收货(链接)
- 验货(验证)
- 使用前准备( 准备)
- 插卡开机(解析)
- 使用(使用)
- 换手机(卸载)
类加载器
从虚拟机的角度来讲,类加载器可分为两类:
- 启动类加载器:使用C++实现(也有java实现的),是虚拟机自身的一部分。
- 其他类加载器:由java实现,独立于虚拟机之外,全部继承于java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存后才能去加载其他类。
从开发人员的角度来看,类加载器可大致分为三类:
- 启动类加载器:BootstrapClassLoader,最顶层的类加载器,负责加载JAVA_HOME/lib目录下的jar和类或者被 -Xbootclasspath 参数指定的路径中的所有类。
- 扩展类加载器:ExtensionClassLoader,负责加载JAVA_HOME/lib/ext目录下的jar包和类,或者被 java.ext.dirs系统变量所指定的路径下的jar包。
- 应用程序加载器:ApplicationClassLoader,面向开发人员的加载器,负责加载当前应用classpath下的所有jar包和类。
双亲委派
在类被加载的时候,系统会首先判断当前类是否被加载过,如果加载过直接返回,否则才会尝试加载。加载的时候,首页会把改请求委派给父类加载器来加载当前类,因此所有的类加载最终都会传到顶层的启动类加载器 BootstrapClassLoader中。当父加载器无法加载当前类是,才由自己来处理。当父类加载器为null时,会使用启动类加载器来加载。
所以类加载流程如下
我们可以简单验证下子父关系
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println("当前类的类加载器 ===> " + ClassLoaderTest.class.getClassLoader());
System.out.println("当前类的类加载器的父类 ===> " + ClassLoaderTest.class.getClassLoader().getParent());
System.out.println("当前类的类加载器的父类的父类 ===> " + ClassLoaderTest.class.getClassLoader().getParent().getParent());
}
}
输出如下
当前类的类加载器 ===> sun.misc.Launcher$AppClassLoader@18b4aac2
当前类的类加载器的父类 ===> sun.misc.Launcher$ExtClassLoader@76fb509a
当前类的类加载器的父类的父类 ===> null
当父类为空时类加载器为启动类加载器
类加载源码
源码在 java.lang.ClassLoader.loaderClass() 中,代码很简单
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) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果父类都不能加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 尝试自己加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派的好处
避免类被重复加载;保证Java核心API类不被篡改;如果没有双亲委派机制,每个类都用自己的类加载器,如果我们也编写一个和java核心类一样的类,系统中就会出现多个同名的类,例如我们编写一个java.lang.String,那么系统总就会有两个String类