类加载子系统
JVM内存结构
虚拟机栈中的LV,OS,DL,RA是
类加载器子系统的作用
负责将磁盘或者网络中加载Class文件,加载的类信息存放于方法区。
一个例子
比如现在有一个Car.class文件(可能来自磁盘也可能来自网络),首先通过类加载器进行加载和初始化得到DNA元数据模板(也就是内存中的一个Car Class)放到方法区中,Car Class调用getClassLoader()可以得到类加载器实例。创建实例时通过Car Class的构造方法来创建。实例通过调用getClass方法可以得到Car Class。
类的加载过程
类的加载过程分为:加载,链接,初始化
类的加载阶段一:加载
类的加载的第一个阶段也叫加载,完成了3件事
1.通过类的全限类名获取定义此类的二进制字节流(也就是class文件),也就是根据文件地址(可能在磁盘,也可能是Jar包中)去读取文件。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。方法区存放类的元数据,常量池放类中引用的变量,方法区还存放类中的字段和方法。
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类的加载阶段二:链接
链接是类的加载中的第二个阶段,分为3个子阶段。
1.验证(Verify)
目的是确保Class文件的字节流中包含信息符合虚拟机要求(如以魔数开头),保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括:文件格式验证,元数据验证,字节码验证,符号引用验证
2.准备(Prepare)
为类变量(static修饰的变量)分配内存并设置初始值,不同类型变量有不同的初始值。
注意
1.这里的类变量中不包含被final修饰的static变量,因为被final修饰会在编译阶段就会分配,在准备阶段只需 要进行设置初始值。
2.这里不会为实例变量分配内存和初始化,因为实例变量会和对象一起分配到堆中。
3.解析(Resolve)
将常量池中的符号引用转变成直接引用的过程
类的加载阶段三:初始化
初始化阶段就是执行类构造器方法<clinit>。
1.类构造器方法不是类构造器,类构造器对应的方法是<init>,而类构造器方法<clinit>不需要自己定义,而是将类中所有的类变量的赋值动作或者静态代码块中的语句合并而来。
2.构造器方法<clinit>中的指令按照语句在源文件中出现的顺序执行。
3.如果该类有父类,那么JVM会保证在子类的<clinit>执行前,会先执行完父类的<clinit>。
4.虚拟机必须保证在多线程下,一个类的<clinit>方法是同步加锁的。也就是只会执行一次。
(1) 文件格式验证(File Format Verification)
- 目标:确保
.class
文件的基本格式是合法的,能够被JVM识别。 - 内容:主要验证文件的魔数(Magic Number)、版本号、常量池的格式、字段和方法的描述符等。
-
- 魔数:每个
.class
文件都以4字节的魔数(0xCAFEBABE)开头,JVM通过这个魔数判断该文件是否是有效的Class文件。 - 版本号:每个Class文件都有版本信息,JVM会检查文件的版本号是否在当前虚拟机支持的范围内。
- 魔数:每个
(2) 元数据验证(Metadata Verification)
- 目标:验证
.class
文件中的元数据是否符合JVM规范。 - 内容:确保类的结构和元数据(如字段、方法、常量池等)没有违反Java虚拟机的规范。例如,字段和方法的访问修饰符、继承关系、接口实现等是否符合类设计的要求。
(3) 字节码验证(Bytecode Verification)
- 目标:确保Class文件中的字节码在语法和语义上是合法的。
- 内容:字节码验证器会检查类中的字节码指令是否正确,防止出现非法操作,如:
-
- 字节码是否访问了不存在的方法或字段。
- 确保字节码指令的操作数和操作数栈是符合要求的。
- 防止出现无效的类或非法的类型转换。
(4) 符号引用验证(Symbolic Reference Verification)
- 目标:验证符号引用的合法性,确保类加载过程中不会出现非法的符号引用。
- 内容:符号引用是指字节码中对其他类、字段、方法的引用,它们在Class文件中是以符号的方式存在的。符号引用验证检查这些符号引用在实际加载时是否能够正确映射到对应的内存地址。包括:
-
- 类的符号引用:检查类的符号引用是否指向一个有效的类。
- 方法的符号引用:检查方法的符号引用是否能够映射到实际的有效方法。
- 字段的符号引用:检查字段的符号引用是否指向有效的字段。
类加载器的分类
1.JVM将所有类加载器分为两种,分别是引导类加载器(Bootstrap ClassLoader) 和自定义类加载器(User-Defined ClassLoader),JVM规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
其中的Extension Class Loader在java9中改为PlatformClassLoader平台类加载器
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器 = " + systemClassLoader);//AppClassLoader
//获取系统类加载器的上层类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println("系统类加载器的上层类加载器 = " + extClassLoader);//PlatformClassLoader
//获取PlatformClassLoader的上层类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("PlatformClassLoader的上层类加载器 = " + bootstrapClassLoader);//null
//获取本类的类加载器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println("本类的类加载器 = " + classLoader);//AppClassLoader
//获取String的类加载器
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println("String的类加载器 = " + classLoader1);//null
}
}
启动类加载器 Bootstrap ClassLoader
1.c++语言实现的,属于JVM的一部分
2.用来加载Java的核心库,加载JVM自身需要的类
3.没有父加载器
4.Bootstrap ClassLoader是扩展类加载器和应用类加载器的父加载器
扩展类加载器 Extsion ClassLoader,后面改为Platform ClassLoader
应用程序加载器 AppClassLoader,也叫系统类加载器
该类是程序默认的加载器,一般来说,Java应用的类都是由它来进行加载
通过ClassLoader.getSystemClassLoader()方法能获取到应用程序加载器
自定义类加载器
什么情况下需要自定义类加载器
1.隔离加载类
2.修改类加载的方式
3.扩展加载源
4.字节码加密,在类初始化时用自定义类加载器解密
自定义类加载器实现步骤
1.继承ClassLoader类、
2.重写fineClass()方法而不是loadClass方法
或者直接继承URLClassLoader类,不需要重写findclass方法,也不用编写获取字节码二进制流的方法。
ClassLoader
双亲委派机制
JVM对Class文件采用的是按需加载的方式,当需要使用该类时才会将它的class文件加载到内存中生成Class对象,在具体加载某个类时,JVM采用的是双亲委派机制。
场景,cat继承animal,此时系统刚启动,第一行就是调用cat类。
1.确定加载类顺序
当对一个自定义的cat类进行使用时,首先会将cat的父类animal类加载,加载animal时发现还有父类Object,就先将Object类加载
2.具体加载某个类时,采用双亲委派机制
对于Object类,首先用AppClassLoader进行加载,而AppClassLoader会让自己的父类先尝试加载,最终让BootStrapClassLoader对Object类进行加载,发现可以加载,成功返回;
然后要加载Animal类,首先用AppClassLoader进行加载,而AppClassLoader会让自己的父类先尝试加载,最终让BootStrapClassLoader对Object类进行加载,发现不能加载,子类加载器ExtClassLoader尝试加载,发现不能加载,子类加载器AppClassLoader尝试加载,发现可以加载,成功返回。
cat类和Animal类的加载过程是一致的,最终被AppClassLoader加载。
原理
1.当一个类加载器需要加载一个类时,首先并不会自己去加载,而是让自己的父加载器去加载。
2.如果父加载器还有父加载器,就继续让上层类加载器去加载,最终会让引导类加载器去加载
3.如果父类加载器可以加载这个类,就成功返回,如果父类加载器发现这个类自己加载不了,子加载器才会尝试去加载。
举例
这里的SPI接口时java核心类库,会被引导类加载器加载,而接口的实现类是第三方库,会被系统类加载器加载。
双亲委派机制的优点
- 避免类的重复加载,通过委托机制确保所有的加载请求都会传到启动类加载器,避免不同的类加载器加载相同类的情况
- 保护程序的安全,防止核心API被随意篡改:java的核心类库被启动类加载器加载,启动类加载器只加载信任的类路径中的类
- 不同的类加载器服务不同的类加载需求,层次清晰职责分明
沙箱安全机制
对java核心源代码的保护。
当自定义一个和核心源代码名字一样的类时(java.lang.String),会被引导类加载器加载,由于引导类加载器会加载核心类(核心类库中的String类),就会报错没有main方法。
JVM判断2个class对象是否为同一个类
1.类的完整类名必须相同
2.加载类的必须是同一个类加载器实例。
对类加载器的引用会和类一起保存在方法区中
JVM会知道一个类是引导类加载器还是用户类加载器加载的,如果是用户类加载器加载的就会把这个类加载器的引用作为类的类型信息存放到方法区中,而引导类是null,所以不用存。
作用是在动态链接时,解析一个类型到另一个类型的引用时,JVM需要保证这2个类型的类加载器是相同的。
类的主动使用和被动使用
主动使用会导致类的初始化,而被动使用不会。
初始化就是执行类的<clinit>方法。