共计 12468 个字符,预计需要花费 32 分钟才能阅读完成。
JVM 的艺术—类加载器篇(三)
引言
明天咱们持续来深刻的分析类加载器的内容。上篇文章咱们解说了类加载器的双亲委托模型、全盘委托机制、以及类加载器双亲委托模型的长处、毛病等内容,没看过的小伙伴请加关注。在公众号内能够找到,jvm 的艺术连载篇。欢送各位小伙伴儿的继续关注,同时也感激各位读者始终以来的反对,自己会始终保持原创、独立创作,给各位读者带来真正的、实用的干货。也会把文章写的通俗易懂,从人的思维、从程序员的思维中,一直的改善写作技巧。争取让每个人都能花起码的学习老本,读懂最好的文章。谢谢。
因为被一些公事耽搁了,文章曾经大略有一个月的工夫没有更新了,在这里给大家真挚的道个歉,上一篇文章,咱们提到了线程上下文类加载器,过后举了一个例子说来阐明,类加载器双亲委托模型的弊病。明天咱们首先来说明确线程上下文类加载这个货色到底是什么,为什么会有这个货色的呈现,它帮咱们到底解决了什么问题。接下来咱们一点点的来剖析。从案例动手。
正式介绍线程的上下文类加载器之前须要介绍一些理论性的东东
以后类加载器(Current ClassLoader):每一个类都会应用本人的类加载器(既加载本身的类加载器)来去加载其它类(指的是所依赖的类),如果 ClassX 援用了 ClassY,那么 ClassX 的类加载器就会加载 ClassY(前提是 ClassY 尚未被加载)。
线程上下文类加载器(Context ClassLoader):线程上下文类加载器是从 JDK1.2 开始引入的,类 Thread 中的 getContextClassLoader() 与setContextClassLoader(ClassLoader cl)别离用来获取和设置上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。
Java 利用运行时初始线程的上下文类加载器是零碎类加载器
为什么应用线程上下文类加载?
为什么应用线程上下文类加载?上篇文章我也简略的提到了。线程上下文类加载的设计初衷,起因就在于咱们 JAVA 语言的 SPI 机制,我又提供了一张图,心愿上面这张图能够全面的论述上下文类加载器的含意。
线程上下文类加载器的重要性
咱们在应用 JDBC 操作数据库时会如下进行编写:
Class.forName("com.mysql.driver.Driver");
Connection conn = Driver.getConnection();
Statement st = conn.getStatement();
JDBC 是一个规范,这就阐明应用到的 Connection 和 Statement 都是内置在 JDK 当中的规范,都是形象接口, 而且是位于 rt.jar 中,其实现必定是由不同的数据库厂商来实现,那么问题就来了:这些规范都是由根类加载器所加载的,然而具体的实现是由具体的厂商来做的,那必定是须要将厂商的 jar 放到工程的 classpath 当中来进行应用,很显然厂商的这些类是没方法由启动类加载器去加载,会由利用类加载器去加载 ,而依据“父类加载器所加载的类或接口是看不到子类加载器所加载的类或接口,而子类加载器所加载的类或接口是可能看到父类加载器加载的类或接口的” 这一准则,那么会导致这样一个场面:比如说 java.sql 包上面的某个类会由启动类加载器去加载,该类有可能会要拜访具体的实现类,但具体实现类是由利用类加载器所加载的,java.sql 类加载器是依据看不到具体实现类加载器所加载的类的,这就是基于双亲委托模型所呈现的一个十分致命的问题,这种问题不仅是在 JDBC 中会呈现,在 JNDI、xml 解析等 SPI(Service Provider Interface) 场景下都会呈现的
所以这里总结一下:父 ClassLoader 能够应用 以后线程 Thread.currentThread().getContextLoader()所指定的 ClassLoader 加载的类,这就扭转了父 ClassLoader 不能应用子 ClassLoader 或者其它没有间接父子关系的 ClassLoader 加载的类的状况,既扭转了双亲委托模型。线程上下文类加载器就是以后线程的 Current ClassLoader。在双亲委托模型下,类加载是由下至上的,既上层的类加载器会委托下层进行加载。然而对于 SPI 来说,有些接口是 Java 外围库所提供的,而 Java 外围库是由启动类加载器来加载的,而这些接口的实现却来自于不同的 jar 包(厂商提供)。Java 的启动类加载器是不会加载其它起源的 jar 包,这样传统的双亲委托模型就无奈满足 SPI 的要求。而通过给以后线程设置上下文类加载器,就能够由设置的上下文类加载器来实现对于接口实现类的加载。
上面以 JDBC 的这种 SPI 场景用图来更具体的形容一下:
很显著 JDBC 会去援用 JDBCImpl 的具体厂商的实现,而 JDBC 规范是由根类加载器所加载,那对于具体实现厂商的类也会用根类加载器去加载,而因为它们是处于工程中的 classPath 当中,由零碎类加载器去加载,很显然是没方法由根类加载器去加载的,为了解决这个问题,线程的上下文类加载器就发挥作用了。
剖析:
由下面的实践可知:Java 利用运行时初始线程的上下文类加载器是零碎类加载器
那思考一下:为什么默认的线程上下文类加载器就是零碎类加载器呢?必定是在某个中央给设置了,其实它是在 Launcher 中进行设置的,如下:
1、线程上下文类加载器的个别应用模式(获取 – 应用 – 还原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();// 获取
try{
ClassLoader targetTccl = xxx;// 要设置的上下文类记录器
Thread.currentThread().setContextClassLoader(targetTccl);// 设置
myMethod();// 应用} finally {Thread.currentThread().setContextClassLoader(classLoader);// 还原
}
2、如果一个类由类加载器 A 加载,那么这个类的依赖类也是由雷同的类加载器加载的(如果该依赖类之前没有被加载过的话),ContextClassLoader 的作用就是为毁坏 Java 的类加载委托机制。
3、当高层提供了对立的接口让低层来实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮忙高层的 ClassLoader 找到并加载该类。
Thread.currentThread().getContextClassLoader();// 获取
Thread.currentThread().setContextClassLoader(targetTccl);// 设置
至此线程上下文类加载器就介绍到这里。
类加载的过程
其实一个类从加载到应用是要经验很多个过程的,上面咱们来具体的说说,一个类从加载到初始化的这个过程,然而还有哪些坑鲜为人知。
上面给出一张图:
固定的类加载执行程序: 加载 验证 筹备 初始化 卸载 的执行程序是肯定的 为什么解析过程没有在这个执行程序中?(接下来剖析)
什么时候触发类加载不肯定,然而类的初始化如下四种状况就要求肯定初始化。然而初始化之前 就肯定会执行 加载 验证 筹备 三个阶段。
触发类加载的过程(由初始化过程引起的类加载)
1):应用 new 关键字 获取一个动态属性 设置一个动态属性 调用一个静态方法。
int myValue = SuperClass.value; 会导致父类初始化,然而不会导致子类初始化
SuperClass.Value = 3 ; 会导致父类初始化,不会导致子类初始化。
SubClass.staticMethod(); 先初始化父类 在初始化子类
SubClass sc = new SubClass(); 先初始化父类 再初始化子类
2):应用反射的时候,若发现类还没有初始化,就会进行初始化
Class clazz = Class.forName(“com.hnnd.classloader.SubClass”);
3):在初始化一个类的时,若发现其父类没有初始化,就会先初始化父类
SubClass.staticMethod(); 先初始化父类 在初始化子类
4):启动虚拟机的时候,须要加载蕴含 main 办法的类.
class SuperClass{
public static int value = 5;
static {System.out.println("Superclass ...... init........");
}
}
class SubClass extends SuperClass {
static {System.out.println("subClass********************init");
}
public static void staticMethod(){System.out.println("superclass value"+SubClass.value);
}
}
上面咱们对类的加载、连贯、初始化这几个过程逐个的解释:
1: 加载
1.1)依据全类名获取到对应类的字节码流(字节流的起源 class 文件,网络文件,还有反射的 Proxygeneraotor.generaotorProxyClass)
1.2)把字节流中的静态数据构造加载到办法区中的运行时数据结构
1.3)在内存中生成 java.lang.Class 对象,能够通过该对象来操作方法区中的数据结构(通过反射)
2: 验证
文件格式的验证: 验证 class 文件结尾的 0XCAFFBASE 结尾
验证主次版本号是否在以后的虚拟机的范畴之类
检测 jvm 不反对的常量类型
元数据的校验:
验证本类是否有父类
验证是否继承了不容许继承的类 (final) 润饰的类
验证本类不是抽象类的时候,是否实现了所有的接口和父类的接口
字节码验证:验证跳转指令跳转到 办法以外的指令.
验证类型转换是否为无效的,比方子类对象赋值父类的援用是能够的,然而把父类对象赋值给子类援用是危险的
总而言之: 字节码验证通过,并不能阐明该字节码肯定没有问题,然而字节码验证不通过。那么该字节码文件肯定是有问题:。
符号援用的验证(产生在解析的过程中):
通过字符串形容的全类名是否能找到对应的类。
指定类中是否蕴含字段描述符,以及简略的字段和办法名称。
3: 筹备: 为类变量分配内存以及设置初始值。
比方 public static int value = 123;
在筹备的过程中 value=0 而不是 123,当执行类的初始化的办法的时候,value=123
若是一个动态常量
public static final int value = 9; 那么在筹备的过程中 value 为 9.
4: 解析:把符号援用替换成间接援用
符号援用分类:
CONSTANT_Class_info 类或者接口的符号援用
CONSTANT_Fieldref_info 字段的符号援用
CONSTANT_Methodref_info 办法的符号援用
CONSTANT_intfaceMethodref_info- 接口中办法的符号援用
CONSTANT_NameAndType_info 子类或者办法的符号援用.
CONSTANT_MethodHandle_Info 办法句柄
CONSTANT_InvokeDynamic_Info 动静调用
间接援用:
指向对象的指针
绝对偏移量
操作句柄
5: 初始化:类的初始化时类加载的最初一步: 执行类的结构器,为所有的类变量进行赋值(编译器生成 CLInit<>)
类结构器是什么?:类结构器是编译器依照 Java 源文件总类变量和动态代码块呈现的程序来决定
动态语句只能拜访定义在动态语句之前的类变量,在其后的动态变量能赋值 然而不能拜访。
父类中的动态代码块优先于子类动态代码块执行。
若类中没有动态代码块也没有动态类变量的话,那么编译器就不会生成 Clint<> 类结构器的办法。
public class TestClassInit {public static void main(String[] args) {System.out.println(SubClass.sub_before_v);
}
}
class SubClass extends SuperClass{
public static int sub_before_v = 5;
static {
sub_before_v = 10;
System.out.println("subclass init.......");
sub_after_v=0;
// 抛错,static 代码块中的代码只能赋值前面的类变量 然而不能拜访。sub_before_v = sub_after_v;
}
public static int sub_after_v = 10;
}
class SuperClass {
public static int super_before_v = 5;
static{System.out.println("superclass init......");
}
public static int super_after_v = 10;
}
上面咱们通过一系列的案例来说验证下面所说的。先做个小的总结。
类的初始化须要对类进行被动应用,上面总结了几点,都能够看做是对类的被动应用:
1:创立类的实例。
2:拜访某个类或者接口中的动态变量,或者对其赋值。
3:拜访某个类的静态方法。
4:反射。
5:初始化一个类的子类。
6:蕴含 main 办法的类。
7:jdk1.7 开始提供动静语言的反对。
除了以上 7 种状况,都是被动应用,都不会导致类被初始化。
依据以上论断,咱们来写几个案例,针对每种状况进行一下证实。
论断一:
动态常量初始化过程是,在 jvm 连贯之后,动态常量的初始化,是由调用这个动态常量办法所在的类的常量池中被保留,此时,被调用的动态常量所在的类的 class 文件就能够被删除,即便被删除,该常量仍然无效。调用某个类的动态常量不能初始化该类。
代码:
package com.jdyun.jvm001;
public class TestClass03 {public static void main(String[] args) {System.out.println(Pet1.a);
}
}
class Pet1{
public static final int a = 10;
static {System.out.println("我是 Pet1,我被初始化了");
}
}
运行后果:"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=64451:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.TestClass03
10
Process finished with exit code 0
从下面这个案例可知,一个类调用另一个类的常量不会导致一个类的初始化。
论断二:
- 此处申明的动态常量,依照之前的了解是动态常量被调用不会初始化该动态常量所在的类
- 然而此处当动态常量的值是一个援用类型的时候,这个时候该动态常量所在的类就会被初始化
- 故此会先打印我被初始化了,而后在打印 a 的随机值
代码:
package com.jdyun.jvm001;
import java.util.UUID;
public class TestClass03 {public static void main(String[] args) {System.out.println(Pet1.a);
}
}
class Pet1{public static final String a = UUID.randomUUID().toString();
static{System.out.println("我被初始化了");
}
}
运行后果:"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=50237:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.TestClass03
我被初始化了
e5b56749-5a97-405f-9fe9-dfe4211bc0ce
Process finished with exit code 0
论断三:
动态变量初始化与动态常量初始化不同,动态变量初始化是在初始化阶段被赋予实在的值比方 int a = 2,那么 2 会被真正的赋值给 a。
如果某个类调用了该类的动态变量,那么动态变量所在的类就会被视为被被动调用了。那么该类就会被初始化。
该类如果有动态代码块儿那么动态代码块儿的优先级高于动态变量。
如果该动态变量所在的类中有父类,那么会优先初始化父类。
package com.jdyun.jvm001;
import java.util.Random;
import java.util.UUID;
public class TestClass03 {public static void main(String[] args) {System.out.println(Dog3.a);
}
}
class Dog3 extends Pet1{public static final int a = new Random().nextInt();
static {System.out.println("我是 Pet1,我是父类,我被最先加载了");
}
}
class Pet1{
static{System.out.println("我被初始化了");
}
}
运行后果:"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=64951:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.TestClass03
我被初始化了
我是 Pet1,我是父类,我被最先加载了
-1203457101
Process finished with exit code 0
论断四:
验证初始化次数,只会被初始化一次。
package com.jdyun.jvm001;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
public class MyTest02 extends ClassLoader{public static void main(String[] args) throws ClassNotFoundException {
//1, 验证初始化次数
for(int i=0;i<50;i++){Test01 test01 = new Test01();
}
}
}
class Test01{
static{System.out.println("我被初始化了");
}
}
运行后果:"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=65340:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.MyTest02
我被初始化了
Process finished with exit code 0
论断五:
接口的初始化,子接口的初始化不会导致父接口的初始化,如果能够导致父接口的初始化,那么 Test01 类中的动态代码块儿就会被打印。很显然后果来看,Test01
中的动态代码块儿没有被打印,所以,接口的初始化中,子接口的初始化,不会导致父接口的初始化。
package com.jdyun.jvm001;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
public class MyTest02 extends ClassLoader{public static void main(String[] args) throws ClassNotFoundException {
//2, 接口初始化, 子接口的初始化不会导致父接口的初始化
System.out.println(MyChild.b);
/* System.out.println(MyParent.test01);
System.out.println(MyChild.test001);*/
//3, 反射初始化类
//Class.forName("com.jdyun.jvm001.Test01");
//4, 创立数组不会导致类的初始化
//Test01[] test01 = new Test01[1];
//5, 动态变量赋值
//System.out.println(MyChild.b);
//Class clesses = String.class;
}
}
class Test01{
static{System.out.println("Test01 被初始化了");
}
}
interface MyParent{Test01 test01 = new Test01();
public static final String a="5";
}
interface MyChild extends MyParent {public static Integer b= UUID.randomUUID().hashCode();}
"C:\Program Files\Java\jdk-11.0. 2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=49632:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.MyTest02
-221561202
Process finished with exit code 0
论断六:
创立一个数组,不会导致类的初始化。
package com.jdyun.jvm001;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
public class MyTest02 extends ClassLoader{public static void main(String[] args) throws ClassNotFoundException {
//4, 创立数组不会导致类的初始化
Test01[] test01 = new Test01[1];
}
}
class Test01{
static{System.out.println("Test01 被初始化了");
}
}
运行后果:"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=50058:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm001.MyTest02
Process finished with exit code 0
论断七:
此处申明的动态常量,依照之前的了解是动态常量被调用不会初始化该动态常量所在的类
然而此处当动态常量的值是一个援用类型的时候,这个时候该动态常量所在的类就会被初始化
故此会先打印我被初始化了,而后在打印 a 的随机值
package com.jdyun.jvm07;
import java.util.Random;
import java.util.UUID;
/**
* 此处申明的动态常量,依照之前的了解是动态常量被调用不会初始化该动态常量所在的类
* 然而此处当动态常量的值是一个援用类型的时候,这个时候该动态常量所在的类就会被初始化
* 故此会先打印我被初始化了,而后在打印 a 的随机值
*/
public class Test {public static void main(String[] args) {System.out.println(Pet.a);
}
}
class Pet{public static final String a = UUID.randomUUID().toString();
static{System.out.println("我被初始化了");
}
}
运行后果:"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=50995:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm07.Test
我被初始化了
3febaad7-90fe-4d7f-be1c-62b70b9f41cc
Process finished with exit code 0
论断八:
对子接口动态常量调用时,父接口没有被加载也并没有被初始化。当咱们有两个接口,父子接口,而后在子接口中申明一个动态变量,此时对子接口中的动态变量进行被动调用,此时父接口没有被初始化,也没有被加载。(删除父接口中的 class)
package com.jdyun.jvm8;
import java.util.Random;
public class Test {public static void main(String[] args) {System.out.println(MyChild.b);
}
}
interface MyParent{public static final String a="5";}
interface MyChild extends MyParent{public static Integer b= 1;}
运行后果:"C:\Program Files\Java\jdk-11.0.2\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=51297:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath G:\jdyun-jvm\out\production\jdyun-jvm com.jdyun.jvm8.Test
1
Process finished with exit code 0
论断九:
接口中的变量赋予援用初始值会初始化子接口。
public class Test {public static void main(String[] args) {System.out.println(MyChild.b);
}
}
interface MyParent{public static String a=5;}
interface MyChild extends MyParent{Integer b= new Random().nextInt(2);
}
小结:
1,如果这个类还没有被加载和链接就先进行加载和链接。
2,如果类存在间接援用父类,并且这个父类还没有被初始化,就先初始化父类。
3,如果类中存在初始化语句,就顺次执行这些初始化语句。
命名空间相干论断总结:
1:同一个命名空间下的 Class 对象雷同(hasCode 雷同),不同命名空间下不同。
2:同一个类加载器加载的类处于一个命名空间。
3:不同的类加载器实例加载的类命名空间不同。
4:每一个类加载器都有本人的命名空间。
5:子类加载器加载的类能见父类加载器加载的类。
6:父类加载器不可见子类类加载加载的类。
至此:jvm 艺术类加载器篇就说这么多,如果 jvm 的艺术三篇文章,各位小伙儿伴都看懂了。并且把握了。那么祝贺你,至多在面试的时候,考类加载器应该不会丢分。前面的文章还是针对 jvm 的。将会开启一个新的篇章。次要针对,jvm 的内存模型、对象模型、以及 jvm 的堆栈、调优、垃圾回收等畛域进行粗疏的解说。欢送各位小伙伴儿继续关注更新。也感激大家始终以来的反对和关注。笔者会持续致力,深度学习并且拿出高质量的文章来回馈宽广的读者。谢谢!!!
更多内容请关注我的公众号:奇客工夫