ClassLoader运行机制

编程

1.类加载器
1.1 ClassLoader抽象类
类加载器的任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转换为一个与目标类对应的java.lang.Class对象实例。

如果需要支持类的动态加载或需要对编译后的字节码文件进行解密操作等,就需要与类加载器打交道了。

(1)Bootstrap :一般用本地代码实现,负责加载 JVM 基础核心类库( rt.jar );负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

(2)Extension :从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap ;负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

(3)system class loader :又叫应用类加载器,其父类是 Extension 。它是应用最广泛的类加载器。它从环境变量 classpath或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。负责记载classpath中指定的jar包及目录中class。

·用户自定义类加载器: java.lang.ClassLoader 的子类 

defineClass方法将字节码的byte数组转换为一个类的Class对象实例,如果希望在类被加载到JVM内部时就被链接,那么可以调用resolveClass方法。

1.2双亲委派模型
Parents Delegation Model,双亲委派模型,约定类加载器的加载机制。


当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委派给它的超类加载器去执行,每一层的类都采用相同的方式,直至委派给最顶层的启动类加载器为止。如果超类加载器无法加载委派给它的类,便将类的加载任务退回给下一级类加载器去执行加载。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

使用这种方式的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个全限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的Class-Path中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

双亲委派机制只是Java虚拟机规范建议采用的加载机制,实际在tomcat中,类加载器所采用的加载机制与传统的双亲委派模型有一定的区别,当缺省的类加载器接收到一个类的加载任务时,首先会去由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行。

1.3自定义类加载器
程序中如果没有显式指定类加载器的话,默认是AppClassLoader来加载,它负责加载ClassPath目录中的所有类型,如果被加载的类型并没有在ClassPath目录中时,抛出java.lang.ClassNotFoundException异常。

一般是继承ClassLoader,如果要符合双亲委派规范,则重写findClass方法;要破坏的话,重写loadClass方法。

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。

为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的load-Class()。

上一节我们已经看过loadClass()方法的代码,双亲委派的具体逻辑就实现在这个方法之中,JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

若要实现自定义类加载器,只需要继承java.lang.ClassLoader 类,并且重写其findClass()方法即可。java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等,ClassLoader 中与加载类相关的方法如下:

方法

说明

getParent()

返回该类加载器的父类加载器。

loadClass(String name)

加载名称为 二进制名称为name 的类,返回的结果是 java.lang.Class 类的实例。

 

findClass(String name)

查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。

 

findLoadedClass(String name)

查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。

 

resolveClass(Class<?> c)

链接指定的 Java 类。

注意:在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。

在Java中,任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类必定不相等(这里的相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果)。

类加载器双亲委派模型是从JDK1.2以后引入的,并且只是一种推荐的模型,不是强制要求的,因此有一些没有遵循双亲委派模型的特例:

(1).在JDK1.2之前,自定义类加载器都要覆盖loadClass方法去实现加载类的功能,JDK1.2引入双亲委派模型之后,loadClass方法用于委派父类加载器进行类加载,只有父类加载器无法完成类加载请求时才调用自己的findClass方法进行类加载,因此在JDK1.2之前的类加载的loadClass方法没有遵循双亲委派模型,因此在JDK1.2之后,自定义类加载器不推荐覆盖loadClass方法,而只需要覆盖findClass方法即可。

(2).双亲委派模式很好地解决了各个类加载器的基础类统一问题,越基础的类由越上层的类加载器进行加载,但是这个基础类统一有一个不足,当基础类想要调用回下层的用户代码时无法委派子类加载器进行类加载。为了解决这个问题JDK引入了ThreadContext线程上下文,通过线程上下文的setContextClassLoader方法可以设置线程上下文类加载器。

2.类加载过程

一个完整的类加载过程必须经历加载、连接、初始化这三个步骤:


2.1加载
简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对象存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。

2.2连接
连接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备、解析三个阶段。

(1)验证阶段
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等


格式验证:验证是否符合class文件规范,比如以0xCAFEBABE开头,大小版本号等

语义验证:
a、检查一个被标记为final的类型是否包含派生类
b、检查一个类中的final方法是否被派生类进行重写
c、确保超类与派生类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)

操作验证:
在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否能通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)。

(2)准备阶段
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量将不再此操作范围内)

(3)解析阶段
将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段可以在初始化之后再执行。

2.3初始化
将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。

所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个类中的变量,使用用户指定的值覆盖之前在准备阶段设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。

如果超类还没有被初始化,那么优先对超类初始化,但在<clinit>方法内部不会显示调用超类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的超类<clinit>方法已经被执行。

JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

只有那些需要执行java代码来为类变量执行赋值操作的类型在编译之后才会在字节码中存在生成的<clinit>方法。如果一个类并没有声明任何的类变量,也没有静态代码块,那么这个类在编译为字节码后,字节码文件中将不会包含<clinit>方法;同样如果一个类声明类变量,但没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作,编译后的字节码中也不会有<clinit>方法;只有final的静态变量也不会有该方法。

3.类初始化的六种时机
(1)为一个类型创建一个新的对象实例时(比如new、反射、序列化)

(2)调用一个类型的静态方法时(即在字节码中执行invokestatic指令)

(3)调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式

(4)调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)

(5)初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)

(6)JVM启动包含main方法的启动类时。

数组本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。

4. 面试题:
1.问类加载,双亲委托,两个类全限定名一样,都会加载吗,怎么打破双亲委托,怎么自己继承写个classloader;

类加载器的任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转换为一个与目标类对应的java.lang.Class对象实例。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

使用这种方式的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个全限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。若要实现自定义类加载器,只需要继承java.lang.ClassLoader类,并且重写其findClass()方法即可。

在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法,因此要破坏的话,需要重写loadClass方法。

2.能不能自己写个类叫java.lang.System?

答案:通常不可以,但可以采取另类方法达到这个需求。 
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证父类加载器优先,父类加载器能找到的类,子类加载器就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。

但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

参考:

Java的类加载机制

《深入理解java虚拟机》学习笔记6——类加载机制
————————————————
版权声明:本文为CSDN博主「2k-Gamer」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/evan123mg/article/details/53068259

以上是 ClassLoader运行机制 的全部内容, 来源链接: utcz.com/z/511923.html

回到顶部