摘要
本博文主要介绍 JVM的类加载机制,以及JDK中ClassLoader类加载器的相关知识;
一、类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
二、类的加载时机
类从被加载到虚拟机内存开始,到卸载出内存位置,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。其中,验证、准备、解析3个部分统称为连接。
加载、验证、准备、初始化和卸载这个五个阶段的顺序是确定的,类加载过程必须按照这种顺序按部就班的开始,而解析阶段则是不定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定或晚期绑定)。
什么情况下需要开始类加载过程的第一阶段:加载?Java虚拟机规范没有进行强制约束,这点可以由虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是有严格规定了有且只有5中情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
(1)、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器包结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
(2)、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3)、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4)、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
(5)、当使用JDK1.7的 动态语言支持 时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄锁对应的类没有进行过初始化,则需要先触发其初始化。【java对于动态语言支持相关的知识,将会在后面单独开博文来详细介绍,届时也会在此处添加上快捷访问方式】(未完待续)
对于以上5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
案例1
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value=1234;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
输出效果:
[Loaded java.net.Inet6Address$Inet6AddressHolder from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded jdk.SuperClass from file:/E:/workspace/demo/target/classes/]
[Loaded jdk.SubClass from file:/E:/workspace/demo/target/classes/]
SuperClass init!
1234
[Loaded java.lang.Shutdown from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.net.Socket$2 from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
Process finished with exit code 0
总结: 对于静态字段,只有直接定义这个字段的类才会被初始化;至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,这点取决于虚拟机的具体实现。对于Sun HotSpot虚拟机来说,可通过 -XX:+TraceClassLoading 参数观察到此操作会导致子类的加载。
案例2
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca=new SuperClass[10];
System.out.println(sca);
}
}
输出效果:
[Loaded sun.net.NetProperties$1 from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded jdk.SuperClass from file:/E:/workspace/demo/target/classes/]
[Ljdk.SuperClass;@27973e9b
[Loaded java.lang.Shutdown from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.net.Inet6Address from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
总结: 将一个类作为数组元素类型来创建一个数组时,并不会引发该类的初始化动作;因为虚拟机会自动生成一个直接继承与java.lang.Object的子类Ljdk.SuperClass,创建动作由字节码指令newaaray触发。这个类代表了一个元素类型为jdk.SuperClass的一维数组,数组中应有的方法和属性(用户可以直接使用的只有被修饰为pulbic的length属性和clone()方法)都实现在这个类里。
案例3
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLO="hello";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO);
}
}
输出效果:
[Loaded jdk.NotInitialization from file:/E:/workspace/demo/target/classes/]
[Loaded java.net.URI$Parser from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded sun.launcher.LauncherHelper$FXHelper from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded sun.net.spi.DefaultProxySelector$NonProxyInfo from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded sun.net.spi.DefaultProxySelector$3 from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Void from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.net.Proxy from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.net.Proxy$Type from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.util.ArrayList$Itr from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
hello
[Loaded sun.net.NetHooks from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.net.Inet6Address$Inet6AddressHolder from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Shutdown from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
总结: 可以看到,以上案例中,ConstClass类的加载动作都没触发;这是因为常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
这点我们可以通过class字节码内容来验证:
1、直接将class文件使用javap命令反编译
javap -v .\NotInitialization.class >NotInitialization
使用文本工具查看:
Classfile /E:/workspace/demo/target/classes/jdk/NotInitialization.class
Last modified 2022-6-10; size 606 bytes
MD5 checksum b65e2588dbd193b899323d6b23159f97
Compiled from "NotInitialization.java"
public class jdk.NotInitialization
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #25 // jdk/ConstClass
#4 = String #26 // hello
#5 = Methodref #27.#28 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #29 // jdk/NotInitialization
#7 = Class #30 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Ljdk/NotInitialization;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 MethodParameters
#20 = Utf8 SourceFile
#21 = Utf8 NotInitialization.java
#22 = NameAndType #8:#9 // "<init>":()V
#23 = Class #31 // java/lang/System
#24 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#25 = Utf8 jdk/ConstClass
#26 = Utf8 hello
#27 = Class #34 // java/io/PrintStream
#28 = NameAndType #35:#36 // println:(Ljava/lang/String;)V
#29 = Utf8 jdk/NotInitialization
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (Ljava/lang/String;)V
{
public jdk.NotInitialization();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljdk/NotInitialization;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
}
SourceFile: "NotInitialization.java"
2、使用jadx打开class文件反编译
package jdk;
/* loaded from: NotInitialization.class */
public class NotInitialization {
public static void main(String[] args) {
System.out.println("hello");
}
}
JDK在编译的时候就已经进行了常量传播优化。
三、类的加载过程
Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
(1)、加载
“加载”是”类加载”过程的第一个阶段。加载阶段,虚拟机需要完成三件事情:
A.通过一个类的全限定名来获取定义此类的二进制字节流。
B.将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构。
C.在内存中生成一个代表这个类的java.lang.Class对象,作为方法去这个类的各种数据的访问入口。
注意 java.lang.Class类的对象存放的位置,虚拟机规范并没有明确规定是存在Java对中;对于HotSpot虚拟机而言,JDK1.7中Class对象比较特殊,它虽然是对象,但存放在方法区里面;JDK1.8中,将JVM永久代取消,新增了元空间(Metaspace)来存储class的数据结构等等;但是,通过openJdk源码可以了解到,在JDK1.8中加载完成后的Class对象是存放在堆中的,而静态变量存在了Class对象内部。
(2)、验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致完成以下4个阶段的检查动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
(3)、准备
准备阶段是正式为类变量分配内存并设置类变量(被static修饰的变量)初始值(只数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。但是如果属性字段是常量是,在准备阶段虚拟机就会将常量值赋值。
(4)、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
(5)、初始化
根据程序员通过程序制定的代码去初始化类变量和其它资源。初始化阶段是执行类构造器<clinit>()方法的过程。它的特点和细节如下:
A.<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
public class Test {
static {
System.out.println(22);
i=2;//给变量赋值可以正常编译通过
//System.out.println(i); //编译器提示非法向前引用
}
static int i=1;
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(Test.i);
}
}
输出结果
[Loaded java.util.ArrayList$Itr from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded sun.net.NetHooks from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.net.Inet6Address$Inet6AddressHolder from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded jdk.Test from file:/E:/workspace/demo/target/classes/]
22
1
[Loaded java.lang.Shutdown from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from E:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar]
Process finished with exit code 0
B.<clinit>()方法与类的构造函数(或者说实例构造器<init>方法)不同,它不需要显示第调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
父类
public class SuperCl {
public SuperCl (){
System.out.println("SuperCl");
}
}
子类
public class SubCl extends SuperCl {
public SubCl (){
System.out.println("SubCl");
}
}
查看子类的字节码:子类构造器里显示的调用了父类的构造器方法。
Classfile /E:/workspace/demo/target/classes/jdk/SubCl.class
Last modified 2022-6-10; size 401 bytes
MD5 checksum 2febaa5d0b88519dbe19cb43724236bb
Compiled from "SubCl.java"
public class jdk.SubCl extends jdk.SuperCl
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#16 // jdk/SuperCl."<init>":()V
#2 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #19 // SubCl
#4 = Methodref #20.#21 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #22 // jdk/SubCl
#6 = Class #23 // jdk/SuperCl
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ljdk/SubCl;
#14 = Utf8 SourceFile
#15 = Utf8 SubCl.java
#16 = NameAndType #7:#8 // "<init>":()V
#17 = Class #24 // java/lang/System
#18 = NameAndType #25:#26 // out:Ljava/io/PrintStream;
#19 = Utf8 SubCl
#20 = Class #27 // java/io/PrintStream
#21 = NameAndType #28:#29 // println:(Ljava/lang/String;)V
#22 = Utf8 jdk/SubCl
#23 = Utf8 jdk/SuperCl
#24 = Utf8 java/lang/System
#25 = Utf8 out
#26 = Utf8 Ljava/io/PrintStream;
#27 = Utf8 java/io/PrintStream
#28 = Utf8 println
#29 = Utf8 (Ljava/lang/String;)V
{
public jdk.SubCl();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method jdk/SuperCl."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String SubCl
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
LineNumberTable:
line 8: 0
line 9: 4
line 10: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Ljdk/SubCl;
}
SourceFile: "SubCl.java"
C.由于父类的<cclinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的静态变量赋值操作。
public class Parent {
public static int a=1;
static {
a=2;
}
}
class Sub extends Parent{
public static int b=a;
}
public static void main(String[] args) {
System.out.println(Sub.a);
}
输出结果:2
D.<clinit>()方法对于类或接口来说不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法
E.接口中不能使用静态语句块,但任然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用是,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法.
四、类加载器
类加载器:实现通过一个类的全限定名来获取描述此类的二进制字节流的这个动作。应用程序可以自己实现类加载器决定怎么去获取需要的类。类加载器在类层次划分、OSGI、热部署、代码加密等领域应用的多。
(1)、 类与类加载器
对于任意一个类,都需要由它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载,都拥有一个独立的类名称空间。只有类文件相同且类加载器相同时,虚拟机中的这两个代表Class对象才相等。
(2)、 双亲委派模型
类加载器按Java虚拟机角度分为两类:
a、启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
b、另一种就是所有其他的类加载器,这些类加载器都是由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。
类加载器按Java开发人员分为四类:
a、启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名称不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存。
b、扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
c、应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所有一般也称为系统类加载器。它负责加载用户类路径(ClassPath)上的所有指定类库。开发者可以直接使用这个类加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。
d、自定义类加载器(User ClassLoader):由用户自定义实现继承自ClassLoader的类加载器。
类加载器的双亲委派模型:要求除了顶层的启动类加载器外,其余的类加载器都应当由自己的父类加载器。加载器之间的父子关系一般是以组合关系来复用父加载器的代码。
双亲委派模型的工作过程 : 如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器上完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜素范围中没有找到所需要的类)时,子加载器才会尝试自己去加载。
双亲委派模型实现(java.lang.ClassLoader#loadClass()):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
(3)、 破坏双亲委派模型
a、双亲委派模型的实现是方法:loadClass(String name, boolean resolve);在JDK1.2之后才引入的双亲委派模型,而类加载器和抽象类java.lang.ClassLoader则是在JDK1.0就存在。在JDK 1.2之前,用户自定义类加载器去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。为了向前兼容,设计者新添加了一个新的protected方法findClass()方法,同是提倡将自己的类加载逻辑写到findClass()方法中,在loadClass方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name) throws ClassNotFoundException {
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
b、JNDI服务、SPI相关加载动作
线程上下文类加载器
双亲委派模型很好的解决了各个类的加载器协作时基础类型的一致问题(越基础的类越由上层加载器加载),那么如果基础类型需要调用用户代码该如何解决? 比如JNDI服务,它的代码由启动类加载器加载,因为JNDI在jdk1.3加入到rt.jar包中。但JNDI的目的是对资源的查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的classpath下的JNDI服务提供接口(Service Provider Interface,SPI)
为了解决双亲委派模型的缺陷,java设计团队,引入一个不太优雅的设计,就是线程上下文加载器(Thread Context ClassLoader)。
这个类加载器可以通过Thread类的setContextClassLoader()进行设置,如果创建线程时未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,那么该类加载器默认是应用程序类加载器。
JNDI使用线程上下文加载器加载SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,该行为打破了双亲委派模型的一般性原则。
和JNDI类似的还有JDBC,JCE,JAXB和JBI,不过当SPI的服务提供者多于一个的时候,代码要使用硬编码判断。为了消除不优雅的实现方式,jdk6提供java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,给SPI加载提供了一种相对合理的解决方案。
c、OSGI模块化热部署
IBM公司实现模块化热部署的关键是自定义的类加载器机制的实现,每一个程序模块(OSGI中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现热部署。在OSGI环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步复杂的网状结构。
五、类加载器-ClassLoader源码解析
首先,我们来看下JVM自带的Java实现的类加载器;UML图如下:
(1)、 ClassLoader类
该类是一个抽象类,被Java定义为除了启动类加载器(由底层C++实现)外的所有类加载器的顶层父类!该类主要方法有:getParent(),loadClass(),findClass(),resolveClass(),defineClass()等;
getParent()
public final ClassLoader getParent()
用途:获取父类加载器
loadClass()
用途:根据类的全限定名加载(双亲委派机制)
findClass()
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
用途:查找二进制名称为name的类,返回结果为java.lang.Class类的实例;该方法会在loadClass()方法检查完父类加载器之后被调用;当loadClass()方法中父类加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委派模式。一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
resolveClass()
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
private native void resolveClass0(Class<?> c);
用途:链接指定一个java类,使用该方法可以使用类的Class对象创建完成的同时也被解析。
defineClass()
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError {
return defineClass(name, b, off, len, null);
}
用途:根据给定的字节数组b转换为Class实例,off和len参数表示Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。
findLoadedClass
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class<?> findLoadedClass0(String name);
用途:查找名称为name的已经被加载过的类,返回Class实例;
说明:
a、SystemDictonary
JVM里有一个数据结构叫做SystemDictonary,这个结构主要就是用来检索我们的类信息,其实也就是private native final Class<?> findLoadedClass0(String name)方法的逻辑;这些类信息对应的结构是klass,对SystemDictonary的理解,可以认为就是一个Hashtable,key是类加载器对象+类的名字,value是指向klass的地址;当我们任意一个类加载器去正常加载类的时候,就会到这个SystemDictonary中去查找,看是否有这么一个klass可以返回,如果有就返回它,否则就会去创建一个新的并放到结构里;
b、初始类加载器/定义类加载器
类加载问题中,子ClassLoader加载类加载的时候会委托给父ClassLoader来加载,当子ClassLoader调用loadClass()加载类,并最终由父ClassLoader加载,那么我们称父ClassLoader为该类的定义类加载器,子ClassLoader为该类的初始类加载器。在这个过程中,子ClassLoader、父ClassLoader都会在SystemDictonary生成记录;那么后续子加载器加载相同类时,就能在findLoadedClass0()中找到该类,不必再向上委托了;
(2)、 SecureClassLoader类
扩展了ClassLoader,新增了几个于与使用相关的代码源,对代码源的位置及其证书的验证和权限的定义类验证
(3)、 URLClassLoader类
是ClassLoader的具体实现类,实现了findClass,findResource等方法。新增了URLClassPath类协助获取Class字节码流相关功能。
(4)、 sun.misc.Launcher类
该类包装了两个内部类ExtClassLoader、AppClassLoader;是JVM启动过程中,Java部分的核心之一;
//静态初始化自身实例[单例模式:饿汉式]
private static Launcher launcher = new Launcher();
//构造器
public Launcher() {
Launcher.ExtClassLoader var1;
//初始化扩展类加载器
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
//初始化应用类加载器
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//设置线程上下文默认类加载器
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {...}
}
六、类加载 - Class.forName() 方式
相信用过JDBC的人都知道以上这种方式,当然,在很多框架使用反射的时候,有时也会用到这种方式进行类加载。
相当于使用当前调用类的classLoader加载
@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
可以显示传递classLoader加载器
@CallerSensitive
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException {
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
ClassLoader和Class.forName()加载的区别
(1)、Class.forName()会对加载的类进行初始化,静态块里的代码会被执行,JDBC使用它的原因是因为JDBC规范中明确要求Driver(数据库驱动)类必须向DriverManager注册自己。这段代码写在静态块中。
Class.forName("com.mysql.cj.jdbc.Driver")
Connection connection = DriverManager.getConnection(jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai&useSSL=false);
package com.mysql.cj.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
(2)、ClassLoader只是将类加载到jvm当中,springMVC的Ioc采用的就是ClassLoader。
七、自定义类加载器
一般自定义类加载器有以下几种用途:
(1)隔离加载类 -TOMCAT
(2)修改类加载的方式 -springboot
(3)扩展加载源
(4)防止源码泄露 - 商业中间件源码保护