首页 >> 大全

JVM成神之路-类加载机制-双亲委派,破坏双亲委派

2023-06-23 大全 48 作者:考证青年

概述

概念

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接时候用的Java类型。

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析统称为连接

上图中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须严格按照这种顺序开始。

解析阶段则不一定,它在某些情况下,可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定|晚期绑定)

类加载-时机

主动引用

Java虚拟机规范中并没有进行强制约束什么时候开始类加载过程的第一个阶段-加载,可以交给虚拟机具体实现来自由把握。但对于初始化阶段,虚拟机规范严格规定有且只有5种情况必须立即对类进行初始化(加载、验证、准备自然要在此之前开始)

4条指令最常见Java代码场景:用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候、调用一个类的静态方法的时候。

注意这里和接口的初始化有点区别,,一个接口在初始化时,并不要求其父接口全部都完成了初始化,只要在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

被动引用

以上5种场景均有一个必须的限定:“有且只有”,这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

示例1

    /*** 通过子类引用父类的静态字段,不会导致子类初始化*/public class SuperClass {static {System.out.println("SuperClass init....");}public static int value = 123;}package com.xdwang.demo;public class SubClass extends SuperClass {static {System.out.println("SubClass init....");}}package com.xdwang.demo;public class Test {public static void main(String[] args) {System.out.println(SubClass.value);}}

运行结果:

SuperClass init....
123

结论:

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。(是否触发子类的加载和验证,取决于虚拟机具体的实现,对于来说,可以通过-XX:+参数观察到此操作会导致子类的加载)

示例2

package com.xdwang.demo;public class Test2 {public static void main(String[] args) {//SuperClass[] superClasses = new SubClass[10];}
}

无任何输出

结论:

通过数组定义来引用类,不会触发此类的初始化

这里其实会触发另一个类的初始化

示例3

    public class ConstClass {static {System.out.println("ConstClass init....");}public static final String MM = "hello Franco";}package com.xdwang.demo;public class Test3 {public static void main(String[] args) {System.out.println(ConstClass.MM);}}

运行结果:

hello Franco

并没有 init….,这是因为虽然Test3里引用了类中的常量,但其实在编译阶段通过常量传播优化,已经将此常量存储到Test3类的常量池中。两个类在编译成class之后就不存在任何联系了。

类加载-过程

加载

加载阶段(可参考java.lang.的()方法),虚拟机要完成以下3件事情:

通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

加载阶段和连接阶段()的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的工具,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大一部分。

验证阶段大致会完成4个阶段的检验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范,并且能够被当前版本的虚拟机处理 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求; 字节码验证:整个验证过程最复杂的一个阶段。主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件 符号引用验证:目的是确保解析动作能正常执行,发生在虚拟机将符号引用转换为直接引用的时候,这个转化动作将在连接的第三阶段-解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。

至于“特殊情况”是指:

public static final int value=123

即当类字段的字段属性是时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器()方法的过程。

static class Parent{public static int A=1;static{A=2;}
}
static class Sub extends Parent{public static int B=A;
}
public class Test{public static void main(String[] args){System.out.println(Sub.B);}
}


public class DealLoopTest {static class DeadLoopClass {static {if (true)// 如果不加上这个if语句,编译器将提示“Initializer does not complete normally”错误{System.out.println(Thread.currentThread() + "init DeadLoopClass");while (true) {}}}}public static void main(String[] args) {Runnable script = new Runnable() {public void run() {System.out.println(Thread.currentThread() + " start");DeadLoopClass dlc = new DeadLoopClass();System.out.println(Thread.currentThread() + " run over");}};Thread thread1 = new Thread(script);Thread thread2 = new Thread(script);thread1.start();thread2.start();}
}

运行结果:(即一条线程在死循环以模拟长时间操作,另一条线程在阻塞等待)

Thread[Thread-1,5,main] start
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass

需要注意的是,其他线程虽然会被阻塞,但如果执行()方法的那条线程退出()方法后,其他线程唤醒之后不会再次进入()方法。同一个类加载器下,一个类型只会初始化一次。

将上面代码中的静态块替换如下:

static {System.out.println(Thread.currentThread() + "init DeadLoopClass");try {TimeUnit.SECONDS.sleep(10);}catch (InterruptedException e) {e.printStackTrace();}
}

运行结果:

Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass
Thread[Thread-0,5,main] run over
Thread[Thread-1,5,main] run over

原因在类加载-时机的主动引用中已经解释了。

类加载器(class )

概念

类加载器(class )用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 ()方法就可以创建出该类的一个对象。

类加载器应用在很多方面,比如类层次划分、OSGi、热部署、代码加密等领域。

基本上所有的类加载器都是 java.lang.类的一个实例

java.lang.类

java.lang.类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

为了完成加载类的这个职责,提供了一系列的方法

方法

说明

()

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

( name)

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

( name)

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

( name)

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

( name, byte[] b, int off, int len)

把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为final的。

(Class c)

链接指定的 Java 类。

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在java程序中起到作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。(比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类肯定不会相等)

这里说的相等,包括代表类的Class对象的()方法、()方法、()方法的返回结果,也包括使用关键字做对象所属关系判定等情况。

双亲委派模型

类加载器分类

在虚拟机的角度上,只存在两种不同的类加载器:

从Java开发人员的角度看,类加载器还可以划分得更细一些,如下:

这个类加载器负责将放置在\lib目录中的,或者被-参数所指定路径中的,并且是虚拟机能识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放置在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接使用。程序员在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。

这个类加载器由sun.misc.$实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

这个类加载器由sum.misc..$来实现。由于这个类加载器是中的()方法的返回值,所以一般也被称为系统类加载器。它负责加载用户类路径()上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序由这三种类加载器互相配合进行加载的,如果有必须,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图

双亲委派模型概念

上图中展示的类加载器之间的层次关系,就称为类加载器的双亲委派模型( Model)。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承()的关系来实现,而是使用组合()关系来复用父加载器的代码。

类加载器的双亲委派模型在JDK1.2期间被引入并广泛用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载实现方式。

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

双亲委派模型优点

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如类java.lang.,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委派给出于模型最顶端的启动类加载器进行加载,因此类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.的类(该类具有系统的类一样的功能,只是在某个函数稍作修改。比如函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中,哈哈,那就热闹了),并放在程序的中,那系统中将会出现多个不同的类,java类型体系中最基础的行为也就无法保证了,应用程序也将变得一片混乱。

双亲委派模型实现

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现代码都集中在类默认的方法中。

默认实现如下:

public Class loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}

再看看( name, )函数:

protected Class loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// 1、检查请求的类是否已经被加载过了Class c = findLoadedClass(name);if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求}if (c == null) {// 在父类加载器无法加载的时候,再调用本身的findClass方法来进行类加载c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
}

检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用.(name, false);).或者是调用类加载器来加载。如果父加载器及类加载器都没有找到指定的类,那么调用当前类加载器的方法来完成类加载。

换句话说,如果自定义类加载器,就必须重写方法!

的默认实现如下:

protected Class findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}

可以看出,抽象类的函数默认是抛出异常的。而前面我们知道,在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的函数,因此我们必须要在这个函数里面实现将一个指定类名称转换为Class对象.

如果是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为Class对象呢?很简单,Java提供了方法,通过这个方法,就可以把一个字节数组转为Class对象啦~

主要的功能是:

将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。如,假设class文件是加密过的,则需要解密后作为形参传入函数。

默认实现如下:

protected final Class defineClass(String name, byte[] b, int off, int len)throws ClassFormatError  {return defineClass(name, b, off, len, null);
}

函数调用过程:

示例

首先,我们定义一个待加载的普通Java类:Test.java。放在com..demo包下:

package com.xdwang.demo;public class Test {public void hello() {System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass() + " 加载进来的");}
}

如果你是直接在当前项目里面创建,待Test.java编译后,请把Test.class文件拷贝走,再将Test.java删除。因为如果Test.class存放在当前项目中,根据双亲委派模型可知,会通过sun.misc.$ 类加载器加载。为了让我们自定义的类加载器加载,我们把Test.class文件放入到其他目录。

接下来就是自定义我们的类加载器:

import java.io.FileInputStream;
import java.lang.reflect.Method;public class Main {static class MyClassLoader extends ClassLoader {private String classPath;public MyClassLoader(String classPath) {this.classPath = classPath;}private byte[] loadByte(String name) throws Exception {name = name.replaceAll("\\.", "/");FileInputStream fis = new FileInputStream(classPath + "/" + name+ ".class");int len = fis.available();byte[] data = new byte[len];fis.read(data);fis.close();return data;}protected Class findClass(String name) throws ClassNotFoundException {try {byte[] data = loadByte(name);return defineClass(name, data, 0, data.length);} catch (Exception e) {e.printStackTrace();throw new ClassNotFoundException();}}};public static void main(String args[]) throws Exception {MyClassLoader classLoader = new MyClassLoader("D:/test");//Test.class目录在D:/test/com/xdwang/demo下Class clazz = classLoader.loadClass("com.xdwang.demo.Test");Object obj = clazz.newInstance();Method helloMethod = clazz.getDeclaredMethod("hello", null);helloMethod.invoke(obj, null);}
}

运行结果:

恩,是的,我是由 class Main$MyClassLoader 加载进来的

破坏双亲委派模型

上面提到过双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐给开发者的类加载器实现方式,在java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过三次较大规模的“被破坏”情况。

Class.()和.()的区别

Class.()方法,内部实际调用的方法是 Class.(,true,);

第2个参数表示类是否需要初始化, Class.()默认是需要初始化。

一旦初始化,就会触发目标对象的 块代码执行,参数也也会被再次初始化。

.()方法,内部实际调用的方法是 .(,false);

第2个 参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以,

不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行

参考与扩展

《深入理解Java虚拟机》

链接:Java类的加载、链接和初始化-'s Blog

链接:深度分析Java的机制(源码级别)-'s Blog

链接:双亲委派模型与自定义类加载器 -

链接:Java双亲委派模型及破坏 - CSDN博客

球友

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了