类加载器子系统的作用
- 类加载器子系统负责从文件系统或者网络中加载 class 文件,class文件在文件开头有特定的文件标识(验证阶段)
- ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则由 ExecutionEngine 决定
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是 Class 文件中常量池部分的内存映射)
类加载器 ClassLoader 的角色
1、class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到 JVM 当中来根据这个文件实例化出 N个一模一样的实例
2、class file 加载到 JVM 中,被称为 DNA元数据模板,放在方法区
3、在.class文件 --通过01数据流--> JVM ---> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色
类的加载过程
加载(Loding)
这里的加载和大标题的宏观加载有区别
1、通过一个类的全限定名获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构,转化为方法区的运行时数据结构,DNA元数据模板
类加载到内存中以后,就是用直接内存给缓存起来了,后面再使用这个类那就是从内存中直接取这个类本身。
所以 JVM 进行类的加载的时候只会调用一次<clinit>()方法,保证类只加载一次即可
3、在内存中生成一个代表这个类的 java.lang.class 对象(生成一个大的 class 实例),作为方法区这个类的各种数据的访问入口
自定义类用的是 系统 / 应用类加载器(ApplicationClassLoader)。
加载.class文件的方式
-
从本地系统中直接加载
-
通过网络获取,典型场景:Web Applet
-
从 zip 压缩包中读取,成为日后 jar、war 格式的基础
-
运行时计算生成,使用最多的是:动态代理技术
-
由其他文件生成,典型场景:JSP应用
-
从专有数据库中提取
.class文件,比较少见 -
从加密文件中获取,典型的防 class 文件被反编译的保护措施
链接(Linking)
验证(Verify)
目的在于确保 class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
每个能被 JVM 所识别的 class file 开头都要具备特殊标识,以此作为验证的手段之一。CA FE BA BE 作为对象持久化文件的魔数(magic numbers)
准备(Prepare)
为类变量分配内存,并且设置该类变量的默认初始值,即零值
public class HelloClass {
public static int a = 1;
// 在Prepare阶段只是为每个类型变量赋上初始值
// 浮点型0.0,整型0,char类型'\u0000',boolean类型false,引用类型null。在这里又温习一下
// 所以此时 a = 0(Prepare阶段)
// 在 Initialization 阶段才会赋值上 a = 1
}
这里不包含用 final 修饰的 static,因为 final 在编译的时候就会分配了,因为被 final 修饰的是常量 而不是变量,准备阶段会显式初始化
这里不会为实例变量分配初始化(此时还没创建对象,后续执行方法里面的比如 new对象才会涉及到具体的实例变量初始化),类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java堆中
解析(Resolve)
将常量池内的符号引用转换为直接引用的过程
事实上,解析操作往往会伴随着 JVM 在执行完初始化之后再执行
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的 class 文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
解析动作就不在这里多做赘述,在 JVM(中)字节码与类加载这一大块中会详细阐述
初始化(Initialization)
1、初始化阶段就是执行类构造器方法<clinit>()的过程
此方法不需定义,是 javac 编译器自动收集类中的所有类静态变量的赋值动作和静态代码块中的语句合并而来
注: 如果类中没有静态变量的赋值动作,或是未出现静态代码块,那可以不生成
public class HelloClass {
public static int a = 1; // 有静态变量赋值动作,会触发 <clinit>() 方法
static {
a = 10; // 如果有静态代码块,则合并
}
}
2、构造器方法中指令按语句在源文件中出现的顺序执行
3、<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>( ))
4、若该类具有父类,JVM 会保证子类的<clinit>()执行前,父类的<clinit>()己经执行完毕
5、虚拟机必须保证一个类的 <clinit> ()方法在多线程下被同步加锁,执行类的加载只会调用一次<clinit> ()方法
代码演示:
public class DeadThreadTest {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "------开始");
DeadThread deadThread = new DeadThread();
System.out.println(Thread.currentThread().getName() + "------结束");
},"线程1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "------开始");
DeadThread deadThread = new DeadThread();
System.out.println(Thread.currentThread().getName() + "------结束");
},"线程2").start();
}
}
class DeadThread {
static {
if (true) {
System.out.println(Thread.currentThread().getName() + "初始化:DeadThread");
while (true) {
// 这句循环没加载完,其他线程进不来,同步加锁了
// 原因是因为,class file加载到内存中,是给所有线程共用一份的
// 加载完毕之后就成为了DNA元数据模板,放在方法区(元空间,也就是内存中)
// 加载过程中如果遇到阻塞,那么就会导致其他线程都进入阻塞状态
}
}
}
}
输出结果:另一个线程的类初始化打印被阻塞。
注:反编译插件的使用、查看字节码文件
IDEA 中 jclasslib bytecode viewer 插件可以做到。省去了在命令行打 javap 命令的操作
使用方法:将类编译后,使用此插件即可