荐 2. 类的加载机制
一、概述
1.1 什么是类的加载机制
Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行验证、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称作类的加载机制。
Class 文件代表着 Java 语言中的一个类或者接口,它并非磁盘上的一个具体文件,而是一串二进制字节流,可以来源于本地磁盘、网络、数据库、内存或者动态产生。
二、类加载发生的时机
2.1 类的生命周期
类的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备、解析三个部分统称为连接。
注意:生命周期中的加载指的是将 Class 文件加载到内存,而不是加载机制中的加载。
2.2 类加载发生的时机
有且只有六种情况必须立即对类进行初始化,这六种情况也称为主动引用:(这里的加载是类加载机制中的加载,而不是生命周期中的加载,完成初始化才算完成了类加载)
- new 一个对象或者访问被 static 关键字修饰的属性(final 类型的除外)或方法时。
- 使用反射的时候,如果类型累又进行过初始化,则需要先出发其初始化
- 在初始化类时如果发现其父类没有进行过初始化,需要先触发器父类的初始化
- 虚拟机启动时会初始化用户指定的包含 main 方法的主类
- JDK 7 引入的动态语言支持
- 当接口中定义了默认方法,而接口的实现类发生了初始化,那么接口要在其之前进行初始。
下面被动引用的例子不会引起类的初始化
class SuperClass {
static {
System.out.println("加载父类...");
}
static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("加载子类...");
}
}
public class NotInitialization1 {
public static void main(String[] args) {
System.out.println(SubClass.value); //
}
}
//输出:加载父类...
分析:对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
三、类加载过程的详细步骤
3.1 加载
“加载”阶段是“类加载”过程中的一个阶段。在加载阶段,Java 虚拟机完成以下三件事情:
- 通过一个类的全限定名来获取此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化成方法区的运行时数据结构;
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
3.2 验证
验证是连接的第一步,目的在于确保 class 文件中的字节流中包含信息符合虚拟机要求,保证被加载类的正确性。
包括四种验证:文件格式验证、元数据验证(对类的语义检查,如是否继承了被 final 修饰的类,是否实现了接口中要求的所有方法等)、字节码验证(对类中方法的语义检查,确保每一行代码没有语义错误)、符号引用验证(检查该类中所引用其他类的字段是否可以访问)
3.3 准备
为类中定义的静态变量分配内存并设置该变量的初始值,即零值,如:
static int value = 123;
//这行代码在准备阶段只会被初始成 0,而不是 123
注意:这里不包含用 final 修饰的 static 常量,final 在编译时就已经把符号转化成对应的值了,如:
static final int value = 123;
//value 在编译阶段就被转化成 123 存到了 class 文件中
3.4 解析
待解决
3.5 初始化
初始化的过程就是执行类构造器 clinit(class init)方法的过程。clinit 为静态变量赋值和执行静态代码块,有以下注意事项:
-
clinit 方法不需要定义,是由 javac 编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来的;
-
静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量可以赋值,但是不能访问。(因为静态变量在准备阶段就被分配内存并且赋初值了,所以可以使用赋值语句对其赋值);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qYcQjIQR-1594783603079)(D:\study\note\JVM\类的初始化代码示例.png)]
-
虚拟机会确保在子类的 clinit 方法执行前,父类的 clinit 已经执行完毕;
-
如果类中没有静态代码块和对静态变量的赋值操作,那么不会生成 clinit 方法;
-
虚拟机必须保证一个类的 clinit 方法在多线程下同步加锁,这样确保类只会被初始化一次,但是可能会造成阻塞,例:
class DeadLoopClass { static { System.out.println(Thread.currentThread().getName() + "初始化 DeadLoopClass"); //只会被输出一次 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } } public class TestClinit2 { public static void main(String[] args) { Runnable script = () -> { System.out.println(Thread.currentThread().getName() + "start..."); DeadLoopClass deadLoopClass = new DeadLoopClass(); }; Thread t1 = new Thread(script); Thread t2 = new Thread(script); t1.setName("<Thread1> "); t2.setName("<Thread2> "); t1.start(); t2.start(); } }
四、类加载器
在类的声明周期中的”加载“阶段,可以由 2 大种类的加载器来实现,对于任意一个类,全类名和它的类加载器一起共同确立在 Java 虚拟机中的唯一性。
4.1 类加载器的分类
-
站在 Java 虚拟机的角度,只存在两种类加载器,分别是引导类加载器和其他类加载器。引导类加载器是嵌入在 Java 虚拟机中的,由 C++ 语言实现,而其他类加载器是虚拟机外部的,由 Java 编写,继承于 ClassLoader 类;
-
站在程序员的角度,可以分为三种类加载器,分别是启动类加载器、扩展类加载器 和 应用程序类加载器。
启动类加载器负责加载 Java 的核心类库,如 Object、String 这样的核心类,还要加载扩展类加载器和应用程序类加载器,并为其指定父类加载器。
扩展类加载器负责加载定义在 <JAVA_HOME>/lib/ext 目录下的类库,如果我们把自己定义的类放到该目录下,就会被扩展类加载器加载。
应用程序类加载器负责加载用户类路径上的所有类库,也可以加载开发人员自己的代码。
4.2 双亲委派模型
4.2.1 什么是双亲委派机制
当一个类加载器收到类加载请求的时候,它并不会自己先去加载,而是把这个请求委托给它的上一级加载器去执行;如果上级加载器可以完成类的加载任务,就成功返回,否则子类加载器才会尝试自己去加载。
4.2.2 双亲委派机制的优势
可以做到防止核心 API 被随意篡改。如用户在程序中自定义了 java.lang.Object,那么如果没有双亲委派机制,Objcet 将会被篡改,其他继承于它的类都没有办法照常执行;相反,在双亲委派机制的模式下,因为该类是以 java 开头的,所以一定会由引导类加载器去加载 lib 目录下的 Objcet 类。
4.2 破坏双亲委派模型
被随意篡改。如用户在程序中自定义了 java.lang.Object,那么如果没有双亲委派机制,Objcet 将会被篡改,其他继承于它的类都没有办法照常执行;相反,在双亲委派机制的模式下,因为该类是以 java 开头的,所以一定会由引导类加载器去加载 lib 目录下的 Objcet 类。
4.2 破坏双亲委派模型
待解决
本文地址:https://blog.csdn.net/stable_zl/article/details/107356909