类的加载
类的加载过程(生命周期)
- 类的加载分几步?
- 按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
- 基本数据类型在虚拟机里面都预先定义了,引用数据类型需要加载。
- 按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
- 过程一:Loading(装载)阶段
- 问题一:每个类只会被加载一次吗?答:不是,每个类只会被一个类加载器加载一次,类和类加载器有一个双向引用的关系,类的加载会有一个命名空间。具体的比较复杂,牵扯到双亲委派,接下来课会讲的。
- 主要作用:将字节码文件加载进内存,生成一个Class的原型——类模版对象。
- 在加载类时,Java虚拟机必须完成以下3件事情:
- 通过类的全名,获取类的二进制数据流。
- 解析类的二进制数据流为方法区内的数据结构(Java类模型)
- 创建java.lang.class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
- 类模版:
- 所谓类模板对象,其实就是Java类在]VM内存中的一个快照,]VM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取]ava类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。
- 反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射。
- 类模型的位置:加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代:JDK1.8及之后:元空间)。
- 二进制流的获取方式
- 对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取的字节码符合JVM规范即可)
- 虚拟机可能通过文件系统读入一个class后缀的文件(最常见)
- 读入jar、zip等归档数据包,提取类文件。
- 事先存放在数据库中的类的二进制数据
- 使用类似于HTTP之类的协议通过网络进行加载
- 在运行时生成一段Class的二进制信息等
- 在获取到类的二进制信息后,Java虚拟机就会处理这些数据,并最终转为一个java.lang.Class的实例。
- 如果输入数据不是ClassFile的结构,则会抛出Class FormatError。
- Class实例的位置
- 类模版放在方法区;Class实例对象在堆空间;引用在栈里面
- 类将.class文件加载至元空间后,会在堆中创建一个Java. lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。(instanceKlass – >mirror :Class的实例)
- 说明:Class类的构造方法是私有的,只有JVM能够创建。java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过Cass类提供的接口,可以获得目标类所关联的.class文件中具体的数据结构:方法、字段等信息。
- 过程二:Linking(链接)阶段 : 又分为三个子步骤:验证、准备、解析。
- 验证:它的目的是保证加载的字节码是合法、合理并符合规范的。
- 对格式、语义、字节码、符号引用 进行检查
- 如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。
- 准备: 简言之,为类的静态变量分配内存,并将其初始化为默认值。
- 这个静态变量不是说final修饰的
- 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值,前提是所赋的值是一个字面量或者常量,如果涉及方法的调用,需要到初始化环节。
- 这一阶段不会给静态变量显式赋值,会在初始化阶段对他进行显式赋值,注意这里只是分配空间并初始化
- 注意这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。
- 在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。
- 解析:简言之,将类、接口、字段和方法的符号引用转为直接引用
- 符号引用就是一些字面量的引用,和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在Class类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时:只有符号引用是不够的,比如当如下print1n()方法被调用时,系统需要明确知道该方法的位置。
- 总结:所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此:百以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。
- 不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot VM中,加载、验证、淮备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 验证:它的目的是保证加载的字节码是合法、合理并符合规范的。
- 过程三:Initialization(初始化)阶段:初始化阶段,简言之,为类的静态变量赋予正确的初始值。
- 具体描述:类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行Java字节码。(即:到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。)
- 初始化阶段的重要工作是执行类的初始化方法:<clinit>( )方法。
- 该方法仅能由Java编译器生成并由]VM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成。
- 它是由类静态成员的赋值语句以及static语句块合并产生的。
- <clinit>( ): 只有在给类的中的static的变量显式赋值或在静态代码块中赋值了。才会生成此方法。
- <init>( ) : 一定会出现在Class的method表中。
- 在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的<clinit>总是在子类<clinit>之前被调用。也就是说,父类的static块优先级高于子类。口诀:由父及子,静态先行。
- clinit方法不是每个类都有的,哪些类在编译为字节码后,字节码文件不会包含<clinit>方法
- 一个类中并没有声明任何的类变量,也没有静态代码块时
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
- 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
-
//场景1:对于非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法 public int num = 1; //场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法 public static int num1; //场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法 public static final int num2 = 1; public static int a = 1; //在初始化阶段赋值 public static final int INT_CONSTANT = 10; //在链接阶段的准备环节赋值 public static Integer INTEGER_CONSTANT1 = Integer.valueOf(100); //在初始化阶段赋值 public static final Integer INTEGER_CONSTANT2 = Integer.valueOf(1000); //在初始化阶段赋值 public static final String s0 = "helloworld0"; //在链接阶段的准备环节赋值 public static final String s1 = new String("helloworld1"); //在初始化阶段赋值 public static String s2 = "helloworld2"; //在初始化阶段赋值 public static final int NUM1 = new Random().nextInt(10); //在初始化阶段赋值 static int a = 9;//在初始化阶段赋值 static final int b = a; //在初始化阶段赋值
- 注意:不是所有final static都在链接阶段赋值,如果需要执行代码才能赋值,则到初始化阶段才会赋值。
- clinit方法的调用导致死锁:
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
- 正是因为函数<clinit>()带锁线程安全的,因此,如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。
- 什么情况会触发类的加载:
- 主动使用:
- Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的“使用”,是指主动使用。
- 主动使用只有下列几种情况:(即:如果出现如下的情况,则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成。)
- 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
- 当调用类的静态方法时,即当使用了字节码invokestatic指令。
- 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstetic或者putstatic指令
- 如果final不生成clinit方法,则不需要初始化,则不进行类的加载。
- 当使用java.lang.reflect包中的方法反射类的方法时。比如:Class. forName ( “com. atguigu. java. Test*”)
- 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- Class的forName方法和Class的getClassLoader的loadClass方法有什么区别?
- forName会进行初始化,loadClass方法不会进行初始化,只进行到loading装载阶段。
- 被动使用:被动使用不会引起类的初始化
- 也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。
- 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
- 当通过子类引用父类的静态变量,不会导致子类初始化。子类会loading,单不会初始化
- 通过数组定义类引用,不会触发此类的初始化 。
- 例如 String str[] = new String[10], 这里没有赋值,没有进行初始化
- Str[0] = new String() 这样才会执行初始化,而String s = null,并不会初始化。
- 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
- 调用ClassLoader类的loadclass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
- 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
- 被动的使用,意味着不需要执行初始化环节,意味着没有<clinit>()的调用。
- 也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。
- 主动使用:
- 类加载过程执行顺序
-
public class T { //T:使用系统类加载器加载。 public static int k = 0; public static T t1 = new T("t1"); public static T t2 = new T("t2"); public static int i = print("i"); public static int n = 99; static { print("静态块"); } public int j = print("j");{ print("构造块"); } public T(String str) { System.out.println((++k) + ":" + str + " i=" + i + " n=" + n); ++n; ++i; } public static int print(String str) { System.out.println((++k) + ":" + str + " i=" + i + " n=" + n); ++n; return ++i; } public static void main(String[] args) { String str = "info"; //String:使用引导类加载器加载 } }
- 执行过程:
- 主进程的类需要初始化,所以开始加载T类
- 加载T类首先对其静态变量进行赋值,完成了k的赋值,当需要对t1进行赋值当时候,需要创建实例
- 创建实例时,原本也需要进行初始化,但是已经在初始化了,只需要初始化一次即可,所以这次不再初始化。
- 创建实例需要执行三步骤,执行顺序如下:对成员变量赋值、非静态代码块、构造器,先执行对j进行赋值
- 对j进行赋值,执行print函数,此时k是0,++k是1,i和n都分配了空间,初始化了默认值,但是还没有进行赋值,所以都是0。所以第一行是:1:j i=0 n=0。执行完之后,n变成1,i变成1,j赋值为1。
- 对j进行完赋值,然后执行非静态代码块,++k是2,i是1,n是1,于是输出:2:构造块 i=1 n=1。执行完之后n=2,i=2.
- 然后执行构造方法,++k是3,i是2,n是2,于是输出:3:t1 i=2 n=2。执行完之后i=3,n=3。
- 然后又对t2进行刚次t1的操作,简化写:4:j i=3 n=3、5:构造块 i=4 n=4、6:t2 i=5 n=5。
- 之后对i进行赋值,7:i i=6 n=6。然后对n进行赋值,此时n=99,i=7
- 然后执行静态代码块,8:静态块 i=7 n=99
-
- 过程四:类的using(使用)阶段:
- 任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“万事俱备,只欠东风”,就等着开发者使用了。
- 开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。
- 过程五:类的Unloading(卸载阶段):
- 类、类加载器、类的实例之间的关系
- 在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getclassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。
- 一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getclass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。
- 一般情况下不进行卸载,主要是类加载器和Class对象双向引用,而类加载器不能干掉,他可能指向很多其他Class对象。
- 类、类加载器、类的实例之间的关系
类的加载器
- 作用:
- ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。
- 因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。
- 类的加载分类:显式加载 vS 隐式加载:class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。
- 显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName (name)或this.getclass().getclassLoader().loadclass()加载class对象。
- 隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
- 类加载机制的必要性 :
- 避免在开发中遇到 java. lang.ClassNotFoundException异常或java. lang. NoClassDefFoundError异常时,手足无措。只有了解类加载器的加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题
- 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了。
- 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。
- 加载的类是唯一的吗?
- 类的唯一性:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。
- 命名空间
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
- 在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
- 通常类加载机制有三个基本特征:(了解即可)
- 双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制用户可以在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现。例如Java 中JNDI、JDBC、文件系统、Cipher等很多方面,都是利用的这种机制,这种情汤就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。
- 可见性。子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。
- 单一性。由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。
类加载器分类与测试
- JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定类加载器(User-Defined ClassLoader)
- 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划为自定义类加载器。
- 无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
- 启动类装载器 Bootstrap ClassLoader :引导类加载器
- 扩展类装载器 Extension ClassLoader :自定类加载器
- 应用程序类装载器 Application ClassLoader :自定类加载器
- 用户自定义类装载器 User ClassLoader :自定类加载器
- 子父类加载器的关系
- 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器。
- 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用。
- 启动类加载器
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVALHOME/jre/1ib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
- 并不继承自java.lang.ClassLoader,没有父加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、 javax、sun等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 扩展类加载器
- Java语言编写,由sun.misc. Launcher$ExtClassLoader实现。
- 继承于ClassLoader类
- 父类加载器为启动类加载器
- 从javaext.dirs系统属性所指定的目录中加载类库,或从]DK的安装目录的
- jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
- 应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 继承于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性 java.class. path 指定路径下的类库
- 应用程序中的类加载器默认是系统类加载器。
- 它是用户自是义类加载器的默认父加载器
- ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
- 用户自定义类加载器
- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
- 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
- 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
- 同时,自定义加载器能够实现应用隔离,例如 Tomcat, Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
- 所有用户自定义类加载器通常需要继承于抽象类java.lang.ClassLoader。
- 获取ClassLoader途径
- 获得当前类的ClassLoader ———— clazz.getclassLoadrr()
- 获得当前线程上下文的ClassLoader ———— Thread. currentThread() .getContextclassLoader()
- 获得系统的ClassLoader ———— ClassLoader.getSystemClassLoader()
- 说明:站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器,引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载器压根儿就不是一个Java类,因此在Java程序中只能打印出空值。
ClassLoader源码分析
- 除了启动类加载器,其他类加载器都是他的子类
- loadClass方法: 双亲委派机制。使用递归的形式实现双亲委派,如果parent不为空,优先调用parent.loadClass()。
- findClass方法:由于历史问题,一般不会重新loadClass方法,那样会破坏双亲委派机制,一般想实现类加载就重写这个方法。
- defineClass方法:通过字节码文件path,能够返回一个Class实例,内部关于流获取加载的逻辑。一般自定义类加载器的时候都需要调用这个方法。
- 对于核心API一定要使用引导类加载器加载,不然会出问题,
- preDefineClass方法:保护机制,即使重写了loadClass,破坏了双亲委派,但是这个方法依然可以保证java开头的API都由引导类加载器加载。在defineClass方法中。
- 实际操作的时候,可以通过继承URLClassLoader
自定义类加载器
- 为什么要自定义类加载器?
- 隔离加载类
在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。 (类的仲裁——>类冲突) - 修改类加载的方式
类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载 - 扩展加载源
比如从数据库、网络、甚至是电视机机顶盒进行加载 - 防止源码泄漏
Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。
- 隔离加载类
- 如何实现类加载器
public class UserDefineClassLoader extends ClassLoader { private String rootPath; public UserDefineClassLoader(String rootPath) { this.rootPath = rootPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //转换为以文件路径表示的文件 String filePath = classToFilePath(name); //获取指定路径的class文件对应的二进制流数据 byte[] data = getBytesFromPath(filePath); //自定义ClassLoader 内部调用defineClass() return defineClass(name,data,0,data.length); } private byte[] getBytesFromPath(String filePath) { FileInputStream fis = null; ByteArrayOutputStream baos = null; try { fis = new FileInputStream(filePath); baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { baos.write(buffer, 0, len); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (baos != null) baos.close(); } catch (IOException e) { e.printStackTrace(); } try { if (fis != null) fis.close(); } catch (IOException e) { e.printStackTrace(); } } return null; } private String classToFilePath(String name) { return rootPath + "\\" + name.replace(".", "\\") + ".class"; } }
相关机制
双亲委派机制
- 定义与本质: 每个类加载器都有自己负责加载的类,每当遇到一个需要加载的类的时候,都会层层向上委派,先由引导类加载器判断,如果不是他负责加载的,则交由子类进行加载。先是向上委托,然后向下推脱。
- 细节逻辑:
- 双亲委派机制在java.l ang.ClassLoader.loadclass(String, boolean)接口中体现。该接口的逻辑如下:
- 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
- 判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass (name,false)接口进行加载。
- 反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassOrNull(name)接口,让引导类加载器进行加载。
- 如果通过以上3条路径都没能成功加载,则调用findclass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。双亲委派的模型就隐藏在这第2和第3步中。
- 双亲委派机制在java.l ang.ClassLoader.loadclass(String, boolean)接口中体现。该接口的逻辑如下:
- 优势与劣势:
- 优势:避免重复、保护安全
- 避免类的重复加载,确保一个类的全局唯一性
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避
免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。 - 保护程序安全,防止核心API被随意篡改
- 避免类的重复加载,确保一个类的全局唯一性
- 弊端:
- 检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。简单说,下层可以用上层,但是上层不能用下层。
- 通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。
- 结论:
- 由于Java虚拟机规范并没有明确要求类加载器的加载机制载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑 定一个正厂方法:用手创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的一定要使用双亲委派模型,只是建议采用这种方式而已。
- 比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
- 优势:避免重复、保护安全
打破双亲委派
第一次破坏双亲委派机制:
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前一一即JDK 1.2面世以前的“远古”时代。
由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadclass()被子类覆盖的可能性,只能在JDK1.2之后的java. lang.ClassLoader中添加一个新的protected方法findclass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadclass()中编写代码。上节我们已经分析过loadclass()方法,双亲委派的具体逻辑就实现在这里面,按照loadclass()方法的逻辑,如果父类加载失败,会自动调用自己的findclass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。
第二次破坏双亲委派机制:
线程上下文类加载器
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础〞,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?
这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI工服务提供者接口 (ServiceProvider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?(SPI:在Java平台电,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI)
为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextclassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
第三次破坏双亲委派机制:
双亲委派模型的第三次“被破坏〞是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署 (Hot Deployment) 等
IBM公司主导的JSR-291(即oSGi R4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(osGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在osGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。
当收到类加载请求时,oSGi将按照下面的顺序进行类搜索:
- 将以java.*开头的类,委派给父类加载器加载。
- 否则,将委派列表名单内的类,委派给父类加载器加载。
- 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给FragmentBundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。 Loader delegate=“true”/>表斥遵循双亲委派机制
Tomcat的类加载机制
- Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类,各个web应用自己的类加载器(WebAppClassLoader)会优先查看自己的仓库加载,加载不到时再交给commonClassLoader走双亲委托。
- 上图中对于的灰色部分:
- Common类加载器: 对于整个tomcat容器而言,他加载的API整个容器都可以使用。
- Catalina类加载器:Tomcat私有,具体的webapp是无法访问,左右两边是隔离的
- Shared类加载器:对于各个webapp是共享的
- Webapp类加载器:可以有很多,不同webapp之间是隔离的
- Jsp类加载:一个Jsp文件对应一个类加载器,可以支持热加载
- 5个相关子问题
- 既然 Tomcat 不遵循双亲委派机制,那么如果我自己定义一个恶意的HashMap,会不会有风险呢?(阿里面试问题)
答:显然不会有风险,如果有,Tomcat都运行这么多年了,那能不改进吗?
tomcat不遵循双亲委派机制,只是自定义的classLoader顺序不同,但顶层还是相同的,还是要去顶层请求classloader。基础API还是遵循的 - 我们思考一下:Tomcat是个web容器,那么它要解决什么问题?
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的==不同版本==,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。webAPP ClassLoader
- 部署在同一个web容器中相同的==类库相同的版本可以共享==。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。Common ClassLoader & Shared ClassLoader
- web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。Catalina ClassLoader
- web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用?所以,web容器需要支持 jsp 修改后不用重启。 JspLoader
- 既然 Tomcat 不遵循双亲委派机制,那么如果我自己定义一个恶意的HashMap,会不会有风险呢?(阿里面试问题)
- Tomcat 如果使用默认的类加载机制行不行?
答案是不行的。为什么?我们看:- 第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
- 第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
- 第三个问题和第一个问题一样。
- 我们再看第四个问题,我们想我们要怎么实现jsp文件的热替换,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中己经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
- 如果tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,该怎么办?
- 看了前面的关于破坏双亲委派模型的内容,我们心里有数了,我们可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。
- 为什么java文件放在Eclipse/IDEA中的src文件夹下会优先jar包中的class?
1. tomcat类加载机制的理解,就不难明白。因为Eclipse/IDEA中的src文件夹中的文件java以及webContent中的JSP都会在tomcat启动时,被编译成class文件放在 WEB-INF/class中而Eclipse/IDEA外部引用的jar包,则相当于放在 WEB-INF/lib 中。
2. 当应用需要到某个类时,则会按照下面的顺序进行类加载:- 使用bootstrap引导类加载器加载
- 使用system系统类加载器加载
- 使用应用类加载器在WEB-INF/classes中加载
- 使用应用类加载器在WEB-INF/lib中加载
- 使用common类加载器在CATALINA HOME/lib中加载
沙箱安全机制
- 保证程序安全
- 保护Java原生的JDK代码
- Java安全模型的核心就是Java沙箱(sandbox)。什么是沙箱?沙箱是一个限制程序运行的环境。
- 沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。
- 沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
- 所有的Java程序运行都可以指定沙箱,可以定制安全策略。
JKD9中的变化
平台类加载器和应用程序类加载器都不再继承自 URLClassLoader。
现在启动类加载器、平台类加载器、应用程序类加载器全都继承于BuiltinClassLoader