一 什么是类的加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类的加载指的是将类从“.java”代码文件编译成的“.class”字节码文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区(HotSpot虚拟机在方法区中)创建一个java.lang.Class
对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class
对象,Class
对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
一般情况在你的代码中用到这个类的时候,才会加载这个类,但是类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
二 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析三个阶段统称为连接(Linking),这7个阶段的发生顺序如图:
类的生命周期
在类的生命周期图中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定或者晚期绑定)。
Java虚拟机并没有强制约束什么情况下需要进行类加载过程的第一个阶段:加载,但是对于初始化阶段,虚拟机则严格规定了有且只有五种情况必须立即对类进行初始化操作,而加载、验证、准备操作自然要在这之前开始:
(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的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
以上5种场景中的行为称对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用:
(1)通过子类引用父类的静态字段,不会导致子类的初始化
public class SuperClass { static {//静态代码块,在类初始化时会执行 System.out.println("super class init!"); } public static int value = 3; }
public class SubClass extends SuperClass { static { System.out.println("sub class init!"); } }
public class Main { public static void main(String[] args) { /** * 通过子类引用父类的静态字段,不会导致子类的初始化 */ System.out.println(SubClass.value); } }
执行以上代码,只会输出"super class init!",说明SubClass没有进行初始化。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
(2)通过数组定义来引用类,不会触发此类的初始化
public class ArrayMain { public static void main(String[] args) { /** * 通过数组定义来引用类,不会触发此类的初始化 */ SuperClass[] superClasses = new SuperClass[10]; } }
上面代码运行后,并没有输出"super class init!",说明SuperClass类并没有进行初始化。
(3)引用类中的常量不会导致此类的初始化
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
public class SuperFinalClass { static {//静态代码块,在类初始化时会执行 System.out.println("super class init!"); } public static final int value = 3; }
public class FinalMain { public static void main(String[] args) { /** * 通过引用常量,不会导致常量所在类的初始化 */ System.out.println(SuperFinalClass.value); } }
上述代码运行后,并没有打印出:"super class init!",这是因为虽然在FinalMain类中引用了SuperFinalClass中的常量value,但是在编译阶段通过常量传播优化,已经将此常量的值存储到了FinalMain类的常量池中,以后FinalMain对常量SuperFinalClass.value的引用实际都被转换为FinalMain对自身常量池的引用了。也就是说,实际上FinalMain的Class文件中并没有SuperFinalClass类的符号引用入口,这两个类在编译成Class文件后就没有任何联系了。
注意:当一个类在初始化时,要求其父类已经全部初始化过了,但是一个接口在初始化时,并不要求其父接口都完成初始化,只有在真正使用到父接口的时候(如引用接口中定义的常来看看),才会初始化。
三 类加载的过程
1 加载
在加载阶段,虚拟机需要完成以下3件事情:
(1)通过类的全限定名来获取定义此类的二进制字节流。
(2)将这个二进制流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
相对于类加载过程中的其他阶段,一个非数组类的加载阶段(准确说,是加载阶段获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以通过系统提供的引导类加载器来完成,也可以通过自定义的加载器完成,开发人员可以通过自定义的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass方法)。
对于数组类而言,数组类本身不通过类加载器加载,它是由Java虚拟机直接创建的。但是数组类的元素类型(Element Type,指的是数组去掉所有维度的类型)最终要靠类加载器去加载。
加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象(对于HotSpot虚拟机而言,Class类的对象比较特殊,他虽然是对象,但是存储在方法区中,而不是在堆中)这个对象将作为程序访问方法区中的这些类型数据的外部接口。
加载阶段部分和连接部分是交叉进行的,有可能加载阶段尚未完成,连接阶段就已经开始。
2 验证:确保被加载的类的正确性
验证是连接阶段的第一步,这个阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
(1)文件格式验证
这个阶段主要验证字节流是否符合Class文件格式的规范,并且能够被当前版本的虚拟机处理。该阶段的主要目的是保证输入的字节流能被正确的解析并存储于方法区,格式上符合描述一个Java类型信息的要求。这个阶段的验证是基于二进制字节流进行的,后面的3个验证阶段全部是基于方法区的存储结构进行的,不在直接操作字节流。
(2)元数据验证
这个阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。这个阶段可能包括的验证点如下:
1. 这个类是否有父类(处理Object之外,所有的类都应该有父类)。
2. 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
3. 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法。
这个阶段主要是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
(3)字节码验证
这个阶段主要目的是通过数据流和控制流分析,确定程序的语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证被检验类的方法在运行时不会做出危害虚拟机安全的事件,例如保证跳转指令不会跳到方法体以外的字节码指令上、保证方法体中的类型转换是有效的(例如把一个子类对象赋值给父类类型是可行的,但是把父类对象赋值给子类类型是不合法的)等。
(4)符号引用验证
这个阶段的校验发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作将在连接的第三个阶段-解析阶段发生。符号引用校验可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,例如:
1. 符号引用中通过字符串描述的全限定名是否能够找到对应的类。
2. 在知道类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
3. 符号引用中的类、字段、方法的访问性(private、protected等)是否可以被当前类访问。
符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,将抛出诶NoSuchFiledError、NoSuchMethodError等异常。
可以通过-Xverify:none参数来关闭大部分的类验证措施,已缩短虚拟机类加载的时间。
3 准备:为类的静态变量分
配内存,并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中分配。对于该阶段有以下几点需要注意:
(1)这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
(2)这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:
public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的 public static
指令是在程序编译后,存放于类构造器 <clinit>()
方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
这里还需要注意如下几点: 1. 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。 2. 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。 3. 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。 4. 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
(3)如果类字段的字段属性表中存在 ConstantValue
属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
假设上面的类变量value被定义为:
public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据 ConstantValue
的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。
4 解析:把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用就是一组符号来描述所引用的目标,可以是任何字面量,只要使用时可以无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
5 初始化:为静态变量设置指定的值
初始化阶段为类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。
在编译生成class文件时,会自动产生两个方法,一个是类的初始化方法, 另一个是实例的初始化方法:
类的初始化方法<clinit>()
:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行。
实例的初始化方法<init>()
: 在实例创建出来的时候调用,包括调用new操作符;调用 Class 或 Java.lang.reflect.Constructor 对象的newInstance()方法;调用任何现有对象的clone()方法;通过 java.io.ObjectInputStream 类的getObject() 方法反序列化。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,即初始化阶段是执行类构造器<cinit>()方法的过程。
(1)<cinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块)中的语句合并产生的,编译器的收集顺序由语句在源文件中出现的顺序所决定的。静态代码块中只能访问到定义在静态代码块之前的变量,定义在他之后的变量, 静态代码块可以赋值,但是不能访问:
(2)<cinit>()方法与类的构造函数(即实例构造器<init>())不同,它不需要显示调用父类构造器,虚拟机会保证在子类的<cinit>()方法执行之前,父类的<cinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<cinit>()方法的类肯定是Object类。
(3)由于父类的<cinit>()方法先执行, 因此父类中定义的静态代码块要优先于子类的类变量赋值操作
(4)<cinit>()方法对于类和接口来说并不是必须的,如果一个类中没有静态代码块,也没有对类变量的赋值操作,那么编译器不会为这个类生成<cinit>()方法。
(5)接口中不能有静态代码块,但是仍有变量的赋值操作,因此接口也会生成<cinit>()方法。与类的<cinit>()方法不同的是接口的<cinit>()方法不需要先执行父接口的<cinit>()方法。只有当父接口中定义的变量使用时,才会父接口才会初始化。
为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
① 声明类变量时指定初始值
② 使用静态代码块为类变量指定初始值
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(如 Class.forName(“com.shengsiyuan.Test”)
)
初始化某个类的子类,则其父类也会被初始化
Java虚拟机启动时被标明为启动类的类( JavaTest
),直接使用 java.exe
命令来运行某个主类
6 结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
执行了 System.exit()
方法
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止
四 类加载器
1 类与类加载器
类加载器虽然只用于实现类的加载动作,但是在Java程序中起到的作用却远远不限于类加载阶段。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间。通俗的讲,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
这里所指的相等,包括代表类的Class对象的equals方法、isAssignableFrom方法、isInstance方法的返回结果,也包括instanceof关键字做对象所属关系判断等情况。
public class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { ClassLoader myClassLoader = new ClassLoader() {//自定义类加载器,这里重写了loadClass破坏了双亲委派模型 @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null){ return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name,b,0,b.length); } catch (IOException e) { e.printStackTrace(); } return super.loadClass(name); } };
//由于自定义的类加载器破坏了双亲委派模型,因此这里加载类的加载器是自定义的类加载器,而不是应用类加载器 Object o = myClassLoader.loadClass("com.dh.yjt.SpringBootDemo.test.classLoad.ArrayMain").newInstance(); System.out.println(o.getClass()); System.out.println(o instanceof ArrayMain); } }
输出结果:
class com.dh.yjt.SpringBootDemo.test.classLoad.ArrayMain false
可以看出,虽然是同一个类,但是在执行instanceof类型检查时,却返回了false,这是因为在虚拟机中存在了两个ArrayMain类,一个是由系统应用程序类加载器加载的,一个是由自定义的类加载器加载的。
JVM类加载机制
-
全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
-
父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
-
缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
类加载有三种方式:
-
1、命令行启动应用时候由JVM初始化加载
-
2、通过Class.forName()方法动态加载
-
3、通过ClassLoader.loadClass()方法动态加载
Class.forName()和ClassLoader.loadClass()区别
-
Class.forName()
:将类的.class文件加载到jvm中之外,还会对类进行初始化,执行类中的static块; -
ClassLoader.loadClass()
:只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。 -
Class.forName(name,initialize,loader)
带参函数也可控制是否加载static块。并且只有调用了newInstance()方法才用调用构造函数,创建类的对象 。
Class.forName 如果调用成功会:
-
保证一个Java类被有效得加载到内存中;
-
类默认会被初始化,即执行内部的静态块代码以及保证静态属性被初始化;
-
默认会使用当前的类加载器来加载对应的类。
ClassLoader.loadClass方式
如果采用这种方式的类加载策略,由于双亲托管模型的存在,最终都会将类的加载任务交付给Bootstrap ClassLoader进行加载。跟踪源代码,最终会调用原生方法:
与此同时,与上一种方式的最本质的不同是,类不会被初始化,只有显式调用才会进行初始化。综上所述,ClassLoader.loadClass 如果调用成功会:
-
类会被加载到内存中;
-
类不会被初始化,只有在之后被第一次调用时类才会被初始化;
-
之所以采用这种方式的类加载,是提供一种灵活度,可以根据自身的需求继承ClassLoader类实现一个自定义的类加载器实现类的加载。(很多开源Web项目中都有这种情况,比如tomcat,struct2,jboss。原因是根据Java Servlet规范的要求,既要Web应用自己的类的优先级要高于Web容器提供的类,但同时又要保证Java的核心类不被任意覆盖,此时重写一个类加载器就很必要了)
2 双亲委派模型
从Java虚拟机角度,只存在两种类加载器:(1)启动类加载器(Bootstrap ClassLoader),他是由C++实现的,是虚拟机自身的一部分。(2)所有其他的类加载器,这些类加载器都是用Java实现的,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。绝大多数java程序会用到以下三种系统提供的类加载器:
(1)启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的或者被-Xbootrclasspath参数所指定的路径中的,并且是虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader
加载)加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,那么直接使用null替代即可。
(2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
(3)应用程序类加载器(Application ClassLoader):这个加载器由sun.misc.Launcher$AppClassLoader实现,这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以一般也称为系统类加载器。负责加载用户类路径(ClassPath)上的所指定的类库,开发者可以直接使用,如果应用程序中没有自定义过类加载器,一般情况下,应用程序类加载器就是默认的类加载器。
上图展示的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系存在,而是都使用组合关系来复用父类加载器的代码。
双亲委派模型的工作过程是:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个类加载请求时(它的搜索范围内没有找到所需的类),子类加载器才会尝试自己去加载。
双亲委派模型对保证Java程序的稳定运作很重要,使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给了模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
相反,如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写了一个java.lang.Object类,并放到程序的ClassPath中,那么系统中将出现多个不同的Object类,Java体系中最基础的行为也就无法保证,应用程序将变得一片混乱。
源码分析
分析类加载器源码要从 sun.misc.Launcher类看起, 关键代码已添加注释,同时可以在此类中看到 ExtClassLoader 和 AppClassLoader 的定义,也验证了我们上文提到的他们不是继承关系,而是通过指定 parent 属性来形成的组合模型:
public Launcher() { Launcher.ExtClassLoader var1; try { //获取到扩展类加载器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { //获取到应用类加载器,同时指定其parent是var1,即扩展类加载器 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) { SecurityManager var3 = null; if (!"".equals(var2) && !"default".equals(var2)) { try { //调用loadClass方法,这里的loader是应用类加载器 var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { ; } catch (InstantiationException var6) { ; } catch (ClassNotFoundException var7) { ; } catch (ClassCastException var8) { ; } } else { var3 = new SecurityManager(); } if (var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } }
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先检查请求的类是否已经被加载过了 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) { //如果父类加载器抛出异常,说明父类加载器无法完成加载工作 } //在父类加载器无法完成加载的时候,再调用本身的findClass方法进行类加载 if (c == null) { 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; } }
以上代码,我们看到方法有同步块(synchronized), 这也就解释了多线程情况不会出现重复加载的情况。先检查是否已经被加载过,若没有则调用父类加载器的loadClass方法,若父类加载器为空,则默认使用启动类加载器做为父类加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,在调用自己的findClass方法进行类加载。URLClassLoader中的 findClass方法:
protected Class<?> findClass(final String name) throws ClassNotFoundException { final Class<?> result; try { result = AccessController.doPrivileged( new PrivilegedExceptionAction<Class<?>>() { public Class<?> run() throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); Resource res = ucp.getResource(path, false); if (res != null) { try { //定义class return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } else { return null; } } }, acc); } catch (java.security.PrivilegedActionException pae) { throw (ClassNotFoundException) pae.getException(); } if (result == null) { throw new ClassNotFoundException(name); } return result; }
寻找类加载器:
package com.neo.classloader; public class ClassLoaderTest{ public static void main(String[] args){ ClassLoader loader = Thread.currentThread().getContextClassLoader(); System.out.println(loader); System.out.println(loader.getParent()); System.out.println(loader.getParent().getParent()); } }
输出结果为:
sun.misc.Launcher$AppClassLoader@64fef26a sun.misc.Launcher$ExtClassLoader@1ddd40f3 null
从上面的结果可以看出,并没有获取到 ExtClassLoader
的父Loader,原因是 BootstrapLoader
(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
双亲委派模型意义:
-
系统类防止内存中出现多份同样的字节码
-
保证Java程序安全稳定运行
五 破坏双亲委派模型
1 自定义类加载器
要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流。
如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。
JDK1.2后不提倡用户再去覆盖loadClass()方法,而应该把自己的类加载逻辑写到findClass()方法里,在loadClass()方法的逻辑里,如果父类加载失败,会调用自己的findClass方法来完成加载,这样可以保证新写出来的类加载器是符合双亲委派模型的。
public class MyClassLoader extends ClassLoader{ private String classPath; public MyClassLoader(String classPath){ this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getData(name); if (classData == null){ return super.findClass(name); }else { return defineClass(name,classData,0,classData.length); } } private byte[] getData(String name) { String path = classPath + File.separatorChar + name.replace('.',File.separatorChar) +".class"; try { InputStream in = new FileInputStream(path); ByteArrayOutputStream stream = new ByteArrayOutputStream(); byte[] buffer = new byte[2048]; int num = 0; while((num = in.read(buffer)) != -1){ stream.write(buffer,0,num); } return stream.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { ClassLoader classLoader = new MyClassLoader("d:\\"); Class c = classLoader.loadClass("com.dh.yjt.SpringBootDemo.test.classLoad.ArrayMain"); System.out.println(c.newInstance()); } }
2 线程上下文类加载器
双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类越由上层类加载器加载),基础类总是作为被用户代码调用的API,但是如果基础类如过要调用回用户的代码,应如何处理?
一个典型的例子就是JNDI服务,它的代码由启动类加载器加载(在rt.jar包中),但是JNDI的目的是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath路径下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但是启动类加载器是无法识别这些代码的。
SPI机制简介
SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
Java本身有一套资源管理服务JNDI,是放置在rt.jar中,由启动类加载器加载的。以对数据库管理JDBC为例,java给数据库操作提供了一个Driver接口:
public interface Driver { Connection connect(String url, java.util.Properties info) throws SQLException; boolean acceptsURL(String url) throws SQLException; DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException; int getMajorVersion(); int getMinorVersion(); boolean jdbcCompliant(); public Logger getParentLogger() throws SQLFeatureNotSupportedException; }
然后提供了一个DriverManager来管理这些Driver的具体实现:
public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException { /* Register the driver if it has not already been added to our list */ if(driver != null) { registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); } else { // This is for compatibility with the original DriverManager throw new NullPointerException(); } println("registerDriver: " + driver); }
可以看到我们使用数据库驱动前必须先要在DriverManager中使用registerDriver()注册,然后我们才能正常使用。
(1)不破坏双亲委派模型的情况(不使用JNDI服务)
我们看下mysql的驱动是如何被加载的:
public static void main(String[] args) throws ClassNotFoundException, SQLException { //1. 在家数据库访问驱动,以Class.forName的方式加载,会初始化 Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/dss","root",""); }
核心就是这句Class.forName()触发了mysql驱动类的加载,这里的类加载器即调用者的类加载器,即应用类加载器,我们看下mysql对Driver接口的实现:
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!"); } } }
可以看到,Class.forName()其实触发了静态代码块的执行,然后向DriverManager中注册了一个mysql的Driver实例对象。这个时候,我们通过DriverManager去获取connection的时候只要遍历当前所有Driver实例对象,然后选择一个建立连接就可以了。因此,此时不需要再去通过类加载器去加载Driver类,而是已经被应用类加载器加载了。
(2)破坏双亲委派模型的情况
在JDBC4.0以后,开始支持使用 SPI 的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:
public static void main(String[] args) throws ClassNotFoundException, SQLException { Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/dss","root",""); }
可以看到这里直接获取连接,省去了上面的Class.forName()注册过程。现在,我们分析下看使用了这种SPI服务的模式原本的过程是怎样的:
(1)从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
(2)加载这个类,这里肯定只能用class.forName("com.mysql.jdbc.Driver")来加载
好了,问题来了,Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在<java_home>/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。
那么,这个问题如何解决呢?按照目前情况来分析,这个mysql的drvier只有应用类加载器能加载,那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。
这就是所谓的线程上下文类加载器。前面提到线程上下文类加载器可以通过 java.lang.Thread.setContextClassLoaser() 方法设置,如果不特殊设置会从父类继承,一般默认使用的是应用程序类加载器。
很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器完成类加载的动作,这实际上是打通了双亲委派模型的层次来逆向使用类加载器,这打破了双亲委派模型的原则
现在我们看下DriverManager是如何使用线程上下文类加载器去加载第三方jar包中的Driver类的,先来看源码:
public class DriverManager { private DriverManager(){} /** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
}
在我们直接调用DriverManager.getConnection() 方法自然会触发静态代码块的执行,在静态代码块中会调用loadInitialDrivers()方法:
private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
源码中我们看下ServiceLoader.load()的具体实现
public static <S> ServiceLoader<S> load(Class<S> service, { return new ServiceLoader<>(service, loader); } public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader();//获取线程上下文类加载器 return ServiceLoader.load(service, cl); }
继续向下看构造函数实例化 ServiceLoader 做了哪些事情,查看 reload() 函数,最终调用了ServiceLoader的nextService函数
private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader);//加载驱动类 } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance());//创建类实例对象 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
其中调用了c = Class.forName(cn, false, loader) 方法,我们成功的做到了通过线程上下文类加载器拿到了应用程序类加载器(或者自定义的然后塞到线程上下文中的),同时我们也查找到了厂商在子级的jar包中注册的驱动具体实现类名,这样我们就可以成功的在rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了同时在第16行完成Driver的实例化,等同于new Driver();
3 热部署
首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。
默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。
另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。
热部署步骤:
-
销毁自定义classloader(被该加载器加载的class也会自动卸载);
-
更新class
-
使用新的ClassLoader去加载class
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
-
该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
-
加载该类的ClassLoader已经被GC。
-
该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
参考
双亲委派模型:大厂高频面试题,轻松搞定 https://mp.weixin.qq.com/s/Dnr1jLebvBUHnziZzSfcrA
jvm系列(一):java类的加载机制 https://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247483934&idx=1&sn=41c46eceb2add54b7cde9eeb01412a90&chksm=ebf6da61dc81537721d36aadb5d20613b0449762842f9128753e716ce5fefe2b659d8654c4e8&scene=21#wechat_redirect
阿里面试:什么地方违反了双亲委派模型 https://mp.weixin.qq.com/s/o0CMIqnKx9qPi48t1cJqDg