02-ClassLoader_Subsystem
1. Summary
- 简图
- 完整图
2. Subsystem作用
- 类加载器子系统负责从文件系统或者网络中加载Class文件,Class文件在文件开头有特定的文件标识
- ClassLoader只负责Class文件的加载,至于它是否可以运行,则由
Execution Engine
决定 - 加载的类信息存放于方法区。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
3. ClassLoader角色
- Class_File存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行时是要加载到JVM当中来,根据这个文件实例化出n个一模一样的实例
- Class_File加载到JVM中,被称为DNA元数据模板,放在方法区
- 在.class文件 => JVM => 元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色
4. 类的加载过程
/**
* 类加载子系统
*/
public class HelloLoader {
public static void main(String[] args) {
System.out.println("我已经被加载啦");
}
}
完整的流程图
1. Loading
- 通过类的全限定名,获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构,转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class对象
,作为方法区这个类的各种数据的访问入口
1. 加载.class方式
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
2. Linking
1. Verification
- 目的:确保Class文件的字节流中包含的信息符合当前VM要求,保证被加载类的正确性,不会危害VM自身安全
- 主要包括四种验证,文件格式验证、元数据验证、字节码验证、符号引用验证
- 同时可以通过安装IDEA的插件《jclasslib Bytecode viewer》,来查看Class文件
工具:Binary Viewer查看
2. Preparation
- 为类变量分配内存并且设置该类变量的默认初始值,即零值
- 不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
- 不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Heap中
public class HelloApp {
private static int a = 1; // 准备阶段为0,在下个阶段,初始化时才是1
public static void main(String[] args) {
System.out.println(a);
}
}
3. Resolution
- 将常量池内的符号引用转换为直接引用
- 符号引用:一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中
- 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
- 解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
3. Initialization
- 执行类构造器方法
<clinit>()
的过程。cl => class, init - 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 当代码中包含static变量,就会有
<clinit>()
- 当代码中包含static变量,就会有
<clinit>()
不同于类的构造器。(构造器是JVM视角下的<init>()
)若该类具有父类,JVM会保证父类的<clinit>()
先执行- JVM必须保证一个类的
<clinit>()
,在多线程下被同步加锁
- 任何一个类在声明后,都有生成一个构造器,默认是空参构造器
- 构造器方法中指令按语句在源文件中出现的顺序执行。因此
num = 2
,number = 10
<clinit>()
public class ClassInitTest {
private static int num = 1;
static {
num = 2;
number = 20;
System.out.println(num);
// System.out.println(number); // 报错,非法的前向引用`Illegal forward reference`
}
private static int number = 10; // linking之prepare: (number = 0) => (initial: 20 => 10)
public static void main(String[] args) {
System.out.println(ClassInitTest.num); // 2
System.out.println(ClassInitTest.number); // 10
}
}
- 涉及到父类时的变量赋值过程
public class ClinitTest1 {
static class Father {
public static int A = 1;
static {
A = 2;
}
}
static class Son extends Father {
public static int b = A;
}
public static void main(String[] args) {
System.out.println(Son.b); // 2
}
}
输出结果为 2。首先加载ClinitTest1的时候,会找到main方法,然后执行Son的初始化,但是Son继承了Father,因此还需要执行Father的初始化,同时将A赋值为2。通过反编译得到Father的加载过程,首先看到原来的值被赋值成1,然后又被复制成2,最后返回
iconst_1
putstatic #2 <com/listao/java/chapter02/ClinitTest1$Father.A>
iconst_2
putstatic #2 <com/listao/java/chapter02/ClinitTest1$Father.A>
return
- JVM必须保证一个类的
<clinit>()
在多线程下被同步加锁
public class DeadThreadTest {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 线程t1开始");
new DeadThread();
}, "t1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 线程t2开始");
new DeadThread();
}, "t2").start();
}
}
class DeadThread {
static {
if (true) {
System.out.println(Thread.currentThread().getName() + "\t 初始化当前类");
while(true) {
}
}
}
}
下面结果。可以看出初始化后,只能够执行一次初始化,也就是同步加锁的过程
线程t1开始
线程t2开始
线程t2 初始化当前类
5. 类加载器分类
JVM支持两种类型的类加载器 。分别为
- 引导类加载器(Bootstrap ClassLoader)
- 自定义类加载器(User-Defined ClassLoader)
- 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类ClassLoader。但是JVM规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
在程序中最常见的类加载器始终只有3个:
四者之间是包含关系,不是上层和下层,也不是子系统的继承关系
- Bootstrap无法直接通过代码获取,同时目前用户代码所使用的加载器为AppClassLoader
- String类型是通过Bootstrap进行加载的,也就是说Java的核心类库都是使用Bootstrap进行加载的
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取其上层的:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@1540e19d
// 根加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); // null
// 获取自定义加载器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取String类型的加载器
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); // null
}
}
6. JVM自带的加载器
基本的加载流程:
- 寻找jre目录,寻找jvm.dll,并初始化JVM。产生一个Bootstrap Loader(启动类加载器)
- Bootstrap该加载器会加载它指定路径下的Java核心API,并且再自动加载Extended Loader(标准扩展类加载器)
- Extended会加载指定路径下的扩展JavaAPI,并将其父Loader设为BootstrapLoader
- Bootstrap也会同时自动加载AppClass Loader(系统类加载器),并将其父Loader设为ExtendedLoader
- AppClass Loader加载CLASSPATH目录下定义的类,即HelloWorld类
1. Bootstrap
启动类加载器(引导类加载器)
- 这个类加载是C/C++语言实现的,嵌套在JVM内部。加载器名称为Null
- 加载Java核心库(
JAVA_HOME/jre/lib/rt.jar
、resources.jar
、sun.boot.class.path
路径),用于提供JVM自身需要的类- 由
-Xbootclasspath
指定路径中的所有类型
- 由
- 并不继承自
java.lang.ClassLoader
,没有父加载器 - 加载ExtClassLoader、AppClassLoader,并指定为他们的父类加载器
- 出于安全考虑,Bootstrap启动类加载器只加载包名为
java, javax, sun
等开头的类
文件 | 描述 |
---|---|
rt.jar | 运行环境包,rt即runtime,J2SE的类定义都在这个包内 |
charsets.jar | 字符集支持包 |
jce.jar | 是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现 |
jsse.jar | 安全套接字拓展包Java(TM) Secure Socket Extension |
classlist | 该文件内表示是引导类加载器应该加载的类的清单 |
net.properties | JVM网络配置信息 |
2. ExtClassLoader
扩展类加载器
- Java语言编写,由
sun.misc.Launcher$ExtClassLoader
实现 - 派生于ClassLoader类。父类加载器为Bootstrap
- 从
java.ext.dirs
系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lib/ext子目录(扩展目录)
下加载类库。如果用户创建的JAR放在此目录下,也会自动被ExtClassLoader加载
3. AppClassLoader
系统类加载器(应用程序类加载器)
- java语言编写,由
sun.misc.LaunchersAppClassLoader
实现 - 派生于ClassLoader类。父类加载器为ExtClassLoader
- 它负责加载
环境变量classpath
或系统属性java.class.path
指定路径下的类库
- 它负责加载
- 是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过
ClassLoader#getSystemclassLoader()
可以获取到该类加载器
4. 自定义ClassLoader
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,还可以自定义ClassLoader,来定制类的加载方式
为什么要自定义类加载器?
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
用户自定义ClassLoader实现步骤:
- 开发人员可以通过继承抽象类
java.lang.ClassLoader
类的方式,实现自己的ClassLoader - 在JDK1.2之前,在自定义ClassLoader时,总会去继承ClassLoader类并重写
loadClass()
。JDK1.2之后,不再建议用户覆盖loadclass()
,而是把自定义类加载逻辑写在findclass()
中 - 在编写自定义ClassLoader时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免去编写
findclass()
及其获取字节码流的方式,使自定义ClassLoader编写更加简洁
/**
* 自定义用户类加载器
*/
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
// 从自定义路径中加载指定类:细节略
// 如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. 线程上下文类加载器
- 线程上下文类加载器 TCC(Thread Context ClassLoader)是从JDK1.2引入的,类Thread的
getContextClassLoader()
与setContextClassLoader(Classloader var1)
分别用来设置线程的上下文类加载器 - 如果没有指定线程的上下文的加载器,那么线程将会继承父线程的上下文类加载器。Java的初始化线程的上下文加载器,可以通过上下文类加载器加载类与资源
public class ContextClassLoader {
/**
* 每个类都会使用自己的类加载器尝试去加载所依赖的类
* 如果ClassX依赖了ClassY,那么ClassX的加载器将会在主动引用ClassY,并且ClassY尚未被加载的时候加载ClassY
*/
public static void main(String[] args) {
System.out.println(Thread.currentThread().getContextClassLoader());
System.out.println(Thread.class.getClassLoader());
}
}
- TCC作用:改变双亲委托模型(父加载器不能访问使用子加载器加载的类,子加载器可以访问使用父加载器加载的类)
- SPI的实现模式中加载器双亲委派模型存在一个缺陷。父ClassLoader可以使用
Thread.currentThread().getContextClassloader()
所指定的Classloader加载类,这就改变了父ClassLoader不能使用子ClassLoader加载的类以及其他没有父子关系的加载器加载类的访问情况,即改变了双亲委托模型- 当高层提供了统计的接口让低层去实现,同时又要在高层加载(或者实例化)这个类,那么就必须通过线程上下文类加载器帮助高层ClassLoader加载这个类
- 就SPI服务而言,有些接口是启动类加载器加载,但各个厂商有自己不同的实现方式,这些实现是不会被启动类加载器加载的,这样传统的双亲委托机制就无法满足SPI要求。而通过设置当前线程的上下类加载器,就可以通过当前线程的上下文类加载器加载这些类
Java中所有涉及SPI的加载动作都采用这种方式,实现方案包括: JDBC、JNDI、JCE以及JBI等
7. Bootstrap加载目录
根加载器只能够加载JDK1.8/Java/jre/lib
目录下的Class
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("*********启动类加载器*********");
// 获取BootstrapClassLoader能够加载的API的路径
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
// 从上面路径中,随意选择一个类,来看看他的类加载器是什么:得到的是null(根加载器)
ClassLoader classLoader = Provider.class.getClassLoader();
}
}
// 也可通过查找`sun.boot.class.path`系统属性得知
System.out.println(System.getProperty("sun.boot.class.path"));
*********启动类加载器*********
file:/E:/Software/JDK1.8/Java/jre/lib/resources.jar
file:/E:/Software/JDK1.8/Java/jre/lib/rt.jar
file:/E:/Software/JDK1.8/Java/jre/lib/sunrsasign.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jsse.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jce.jar
file:/E:/Software/JDK1.8/Java/jre/lib/charsets.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jfr.jar
file:/E:/Software/JDK1.8/Java/jre/classes
null
1. 关于ClassLoader
- ClassLoader类,是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括Bootstrap)
- Bootstrap是C、C++写的。App、Ext是Java写的
sun.misc.Launcher
它是一个JVM的入口应用
2. 获取ClassLoader
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
// 1. 获取String的ClassLoader
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader); // null
// 2. 获取当前线程上下文的ClassLoader
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 3. 获取系统的ClassLoader
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader();
System.out.println(classLoader2); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 4. 获取调用者的ClassLoader
// DriverManager.getCallerClassLoader();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
8. 双亲委派机制
JVM对Class文件是按需加载,当需要该类时才会将Class文件加载到内存生成Class对象。而且加载某个类的Class文件时,采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
为什么使用双亲委托这种模型呢?
可以避免重复加载,当父ClassLoader已经加载了该类,子ClassLoader没必要再加载一次。试想一下,如果不使用这种委托模式,那就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患。而双亲委托下,因为String已经在启动时就被Bootstrap加载,用户自定义的ClassLoader永远无法加载自定义的String,除非改变JDK中ClassLoader搜索类的默认算法
JVM在搜索类时,如何判定两个class是相同的呢?
不仅要判断两个类名是否相同,而且要判断是否由同一个ClassLoader实例加载的。只有两者同时满足,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class
JVM已经提供了默认的类加载器,为什么还要自定义类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class。如果想加载其它位置的类或jar(eg:加载网络上的一个class文件),默认的ClassLoader就不能满足需求
1. 工作原理
- 如果一个ClassLoader收到了类加载请求,判断没有加载过,并不会自己先去加载,而是把这个请求委托给父ClassLoader去执行。父ClassLoader判断没有加载过,则进一步向上委托,依次递归,请求最终将到达顶层的Bootstrap
- 如果父ClassLoader可以完成类加载任务,就成功返回。否则,子加载器才会尝试自己去加载,这就是双亲委派模式
- 当加载
jdbc.jar
用于实现数据库连接时,首先需要知道jdbc.jar
是基于SPI接口进行实现的。在加载时,会进行双亲委派,最终从Bootstrap中加载SPI核心类,然后加载SPI接口类,接着进行反向委派,通过AppClassLoader实现类jdbc.jar
加载
2. 沙箱安全机制
自定义String类
- 加载自定义String类时,会率先使用Bootstrap加载,而Bootstrap在加载的过程中会先加载JDK自带的文件(
rt.jar
包中java.lang.String.class
)。报错信息说没有main方法,就是因为加载的是rt.jar
包中的String类,保证对java核心源代码的保护。这就是沙箱安全机制
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello, ooxx.com");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
}
}
package java.lang;
public class String {
static{
System.out.println("自定义的String类的静态代码块");
}
// 错误: 在系统类`java.lang.String`中找不到`main()`
public static void main(String[] args) {
System.out.println("hello,String");
}
}
3. 双亲委派优势
- 能够有效确保一个类的全局唯一性,避免类的重复加载
- 保护程序安全(沙箱安全),防止核心API被随意篡改
- 自定义类:
java.lang.String
- 自定义类:
java.lang.ShkStart
(报错:阻止创建java.lang
开头的类)
- 自定义类:
9. Tomcat类加载机制
加载顺序
- 使用bootstrap引导类加载器加载JVM启动所需的类,以及标准扩展类(位于
jre/lib/ext
下) - 使用system系统类加载器加载tomcat启动的类。如
bootstrap.jar
,通常在catalina.bat
或catalina.sh
中指定。位于CATALINA_HOME/bin
下 - 使用应用类加载器在
WEB-INF/lib
中加载,每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于WEB-INF/lib
下的jar文件中的class和WEB-INF/classes
下的class文件 - 使用common类加载器在
CATALINA_HOME/lib
中加载
10. 其它
1. 两个Class对象是否相同?
- 在JVM中表示两个Class对象是否为同一个类存在两个必要条件
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(ClassLoader实例对象)必须相同
- 换句话说,在JVM中,即使这两个类对象(Class对象)来源同一个Class文件,被同一个JVM所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
2. 类加载器引用toClazz
JVM必须知道一个类型是由Bootstarp加载的还是由用户类ClassLoader加载的。如果一个类型是由用户类ClassLoader加载的,那么JVM会将这个ClassLoader的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用时,JVM需要保证这两个类型的ClassLoader是相同的
3. 类主动使用、被动使用
Java程序对类的使用方式分为:
- 主动使用(七种情况)
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(eg:
Class.forName("com.ooxx.Test")
) - 初始化一个类的子类
- JVM启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:
java.lang.invoke.MethodHandle
实例的解析结果REF getStatic, REF putStatic, REF invokeStatic
句柄对应的类没有初始化,则初始化
- 被动使用。除了以上七种情况,都不会导致类的初始化
11. Exception
1. ClassNotFoundException
- 这是最常见的异常,产生这个异常的原因为在当前的 ClassLoader 中加载类时未找到类文件,对位于 System ClassLoader 的类很容易判断,只要加载的类不在 Classpath 中,而对位于 User-DefinedClassLoader 的类则麻烦些,要具体查看这个 ClassLoader 加载类的过程,才能判断此 Classloader 要从什么位置加载到此类
2. NoClassDefFoundError
- 该异常较之
ClassNotFoundException
更难处理一些,造成此异常的主要原因是加载的类中引用到的另外的类不存在 - eg:要加载A,而A中调用了B,B不存在或当前ClassLoader没法加载B,就会抛出这个异常
public class A {
private B b = new B();
}
- 当采用
Class.forName()
加载A时,虽能找到A.class,但此时B.class不存在,则会抛出NoClassDefFoundError
- 因此,对于这个异常,须先查看是加载哪个类时报出的,然后再确认该类中引用的类是否存在于当前ClassLoader能加载到的位置
3. LinkageError
- 该异常在自定义 ClassLoader 的情况下更容易出现,主要原因是此类已经在 ClassLoader 加载过了,重复地加载会造成该异常,因此要注意避免在并发的情况下出现这样的问题
- 由于JVM的这个保护机制,使得在JVM中没办法直接更新一个已经load的Class,只能创建一个新的ClassLoader来加载更新的Class,然后将新的请求转入该ClassLoader中来获取类,这也是JVM中不好实现动态更新的原因之一,而其他更多的原因是对象状态的复制、依赖的设置等
4. ClassCastException
- 该异常有多种原因,在JDK 5支持泛型后,合理使用泛型可相对减少此异常的触发。这些原因中比较难查的是两个A对象由不同的ClassLoader加载的情况,这时如果将其中某个A对象转型成另外一个A对象,也会报出
ClassCastException