简述:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。php
下面咱们具体来看类加载的过程:html
类从被加载到内存中开始,到卸载出内存,经历了加载、链接、初始化、使用四个阶段,其中链接又包含了验证、准备、解析三个步骤。这些步骤整体上是按照图中顺序进行的,可是Java语言自己支持运行时绑定,因此解析阶段也能够是在初始化以后进行的。以上顺序都只是说开始的顺序,实际过程当中是交叉进行的,加载过程当中可能就已经开始验证了。java
首先要知道何时类须要被加载,Java虚拟机规范并无约束这一点,可是却规定了类必须进行初始化的5种状况,很显然加载、验证、准备得在初始化以前,下面具体来讲说这5种状况:程序员
其中状况1中的4条字节码指令在Java里最多见的场景是:
1 . new一个对象时
2 . set或者get一个类的静态字段(除去那种被final修饰放入常量池的静态字段)
3 . 调用一个类的静态方法spring
下面咱们一步一步分析类加载的每一个过程bootstrap
加载是整个类加载过程的第一步,若是须要建立类或者接口,就须要如今Java虚拟机方法区建立于虚拟机实现规定相匹配的内部表示。通常来讲类的建立是由另外一个类或者接口触发的,它经过本身的运行时常量池引用到了须要建立的类,也多是因为调用了Java核心类库中的某些方法,譬如反射等。设计模式
通常来讲加载分为如下几步:数组
建立名字为C的类,若是C不是数组类型,那么它就能够经过类加载器加载C的二进制表示(即Class文件)。若是是数组,则是经过Java虚拟机建立,虚拟机递归地采用上面提到的加载过程不断加载数组的组件。缓存
Java虚拟机支持两种类加载器:tomcat
用户自定义的类加载器应该是抽象类ClassLoader的某个子类的实例。应用程序使用用户自定义的类加载器是为了扩展Java虚拟机的功能,支持动态加载并建立类。好比,在加载的第一个步骤中,获取二进制字节流,经过自定义类加载器,咱们能够从网络下载、动态产生或者从一个加密文件中提取类的信息。
关于类加载器,会新开一篇文章描述。
验证做为连接的第一步,用于确保类或接口的二进制表示结构上是正确的,从而确保字节流包含的信息对虚拟机来讲是安全的。Java虚拟机规范中关于验证阶段的规则也是在不断增长的,但大致上会完成下面4个验证动做。
1 . 文件格式验证:主要验证字节流是否符合Class文件格式规范,而且能被当前版本的虚拟机处理。
主要验证点:
0xCAFEBABE
开头2 . 元数据验证:主要对字节码描述的信息进行语义分析,以保证其提供的信息符合Java语言规范的要求。
主要验证点:
3 . 字节码验证:主要是经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型作完校验后,字节码验证将对类的方法体进行校验分析,保证被校验类的方法在运行时不会作出危害虚拟机安全的事件。
主要有:
验证阶段很是重要,但不必定必要,若是全部代码极影被反复使用和验证过,那么能够经过虚拟机参数-Xverify: none
来关闭验证,加速类加载时间。
准备阶段的任务是为类或者接口的静态字段分配空间,而且默认初始化这些字段。这个阶段不会执行任何的虚拟机字节码指令,在初始化阶段才会显示的初始化这些字段,因此准备阶段不会作这些事情。假设有:
public static int value = 123;
value在准备阶段的初始值为0而不是123,只有到了初始化阶段,value才会为0。
下面看一下Java中全部基础类型的零值:
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
一种特殊状况是,若是字段属性表中包含ConstantValue属性,那么准备阶段变量value就会被初始化为ConstantValue属性所指定的值,好比上面的value若是这样定义:
public static final int value = 123;
编译时,value一开始就指向ConstantValue,因此准备期间value的值就已是123了。
解析阶段是把常量池内的符号引用替换成直接引用的过程,符号引用就是Class文件中的CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量。下面咱们看符号引用和直接引用的定义。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要能够惟必定位到目标便可。符号引用于内存布局无关,因此所引用的对象不必定须要已经加载到内存中。各类虚拟机实现的内存布局能够不一样,可是接受的符号引用必须是一致的,由于符号引用的字面量形式已经明肯定义在Class文件格式中。
直接引用(Direct References):直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不一样虚拟机上翻译出来的直接引用通常不会相同。若是有了直接引用,那么它必定已经存在于内存中了。
如下Java虚拟机指令会将符号引用指向运行时常量池,执行任意一条指令都须要对它的符号引用进行解析:
对同一个符号进行屡次解析请求是很常见的,除了invokedynamic指令之外,虚拟机基本都会对第一次解析的结果进行缓存,后面再遇到时,直接引用,从而避免解析动做重复。
对于invokedynamic指令,上面规则不成立。当遇到前面已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对于其余invokedynamic指令一样生效。这是由invokedynamic指令的语义决定的,它原本就是用于动态语言支持的,也就是必须等到程序实际运行这条指令的时候,解析动做才会执行。其它的命令都是“静态”的,能够再刚刚完成记载阶段,尚未开始执行代码时就解析。
下面来看几种基本的解析:
类与接口的解析: 假设Java虚拟机在类D的方法体中引用了类N或者接口C,那么会执行下面步骤:
java.lang.IllegalAccessError
异常。字段解析:
要解析一个未被解析过的字段符号引用,首先会对字段表内class_index项中索引的CONSTANT_Class_info
符号引用进行解析,这边记不清的能够继续回顾深刻理解JVM类文件格式,也就是字段所属的类或接口的符号引用。若是在解析这个类或接口符号引用的过程当中出现了任何异常,都会致使字段解析失败。若是解析完成,那将这个字段所属的类或者接口用C表示,虚拟机规范要求按照以下步骤对C进行后续字段的搜索。
1 . 若是C自己包含了简单名称和字段描述符都与目标相匹配的字段,则直接返回这个字段的直接引用,查找结束。
2 . 不然,若是在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,若是接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
3 . 再否则,若是C不是java.lang.Object
的话,将会按照继承关系从下往上递归搜索其父类,若是在类中包含
了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
4 . 若是都没有,查找失败退出,抛出java.lang.NoSuchFieldError
异常。若是返回了引用,还须要检查访问权限,若是没有访问权限,则会抛出java.lang.IllegalAccessError
异常。
在实际的实现中,要求可能更严格,若是同一字段名在C的父类和接口中同时出现,编译器可能拒绝编译。
类方法解析
类方法解析也是先对类方法表中的class_index项中索引的方法所属的类或接口的符号引用进行解析。咱们依然用C来表明解析出来的类,接下来虚拟机将按照下面步骤对C进行后续的类方法搜索。
1 . 首先检查方法引用的C是否为类或接口,若是是接口,那么方法引用就会抛出IncompatibleClassChangeError
异常
2 . 方法引用过程当中会检查C和它的父类中是否包含此方法,若是C中确实有一个方法与方法引用的指定名称相同,而且声明是签名多态方法(Signature Polymorphic Method),那么方法的查找过程就被认为是成功的,全部方法描述符所提到的类也须要解析。对于C来讲,没有必要使用方法引用指定的描述符来声明方法。
3 . 不然,若是C声明的方法与方法引用拥有一样的名称与描述符,那么方法查找也是成功。
4 . 若是C有父类的话,那么按照第2步的方法递归查找C的直接父类。
5 . 不然,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,若是存在相匹配的方法,说明类C时一个抽象类,查找结束,而且抛出java.lang.AbstractMethodError
异常。
java.lang.NoSuchMethodError
。java.lang.IllegalAccessError
异常。接口方法解析
接口方法也须要解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,若是解析成功,依然用C表示这个接口,接下来虚拟机将会按照以下步骤进行后续的接口方法搜索。
1 . 与类方法解析不一样,若是在接口方法表中发现class_index对应的索引C是类而不是接口,直接抛出java.lang.IncompatibleClassChangeError
异常。
2 . 不然,在接口C中查找是否有简单名称和描述符都与目标匹配的方法,若是有则直接返回这个方法的直接引用,查找结束。
3 . 不然,在接口C的父接口中递归查找,直到java.lang.Object
类为止,看是否有简单名称和描述符都与目标相匹配的方法,若是有则返回这个方法的直接引用,查找结束。
4 . 不然,宣告方法失败,抛出java.lang.NoSuchMethodError
异常。
因为接口的方法默认都是public的,因此不存在访问权限问题,也就基本不会抛出java.lang.IllegalAccessError
异常。
初始化是类加载的最后一步,在前面的阶段里,除了加载阶段能够经过用户自定义的类加载器加载,其他部分基本都是由虚拟机主导的。可是到了初始化阶段,才开始真正执行用户编写的java代码了。
在准备阶段,变量都被赋予了初始值,可是到了初始化阶段,全部变量还要按照用户编写的代码从新初始化。换一个角度,初始化阶段是执行类构造器<clinit>()
方法的过程。
<clinit>()
方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块(static语句块)中的语句合并生成的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句块中能够赋值,可是不能访问。
public class Test { static { i=0; //能够赋值 System.out.print(i); //编译器会提示“非法向前引用” } static int i=1; }
<clinit>()
方法与类的构造函数<init>()
方法不一样,它不须要显示地调用父类构造器,虚拟机会宝成在子类的<clinit>()
方法执行以前,父类的<clinit>()
已经执行完毕,所以在虚拟机中第一个被执行的<clinit>()
必定是java.lang.Object
的。
也是因为<clinit>()
执行的顺序,因此父类中的静态语句块优于子类的变量赋值操做,因此下面的代码段,B的值会是2。
static class Parent { public static int A=1; static { A=2; } } static class Sub extends Parent{ public static int B=A; } public static void main(String[] args) { System.out.println(Sub.B); }
<clinit>()
方法对于类来讲不是必须的,若是一个类中既没有静态语句块也没有静态变量赋值动做,那么编译器都不会为类生成<clinit>()
方法。
接口中不能使用静态语句块,可是容许有变量初始化的赋值操做,所以接口与类同样都会生成<clinit>()
方法,可是接口中的<clinit>()
不须要先执行父类的,只有当父类中定义的变量使用时,父接口才会初始化。除此以外,接口的实现类在初始化时也不会执行接口的<clinit>()
方法。
虚拟机会保证一个类的<clinit>()
方法在多线程环境中能被正确的枷锁、同步。若是多个线程初始化一个类,那么只有一个线程会去执行<clinit>()
方法,其它线程都须要等待。
Java虚拟机退出的通常条件是:某些线程调用Runtime类或System类的exit方法,或者时Runtime类的halt方法,而且Java安全管理器也容许这些exit或者halt操做。
除此以外,在JNI(Java Native Interface)规范中还描述了当使用JNI API来加载和卸载(Load & Unload)Java虚拟机时,Java虚拟机退出过程。
JVM系列之类加载流程-自定义类加载器
老实说,类加载流程做者仍是比较熟悉并且有实战经验的,由于有过一次自定义类加载器的实战经验(文章最后会和你们分享),虽然大部分小伙伴以为这部分对coding没什么实际意义,若是你一直写CRUD而且用现有的高级语言业务框架,我能够告诉你,确实没什么用。但话说回来,你若是想多了解底层,而且在类加载时作一些手脚,那么这一块就颇有必要学了。不少框架都是利用了类加载机制里的动态加载特性来搞事情,像比较出名的OSGI模块化(一个模块一个类加载器),JSP(运行时转换为字节流让加载器动态加载),Tomcat(自定义了许多类加载器用来隔离不一样工程)...这里就不一一列举了。本文仍是先把类加载流程先讲一讲,而后分享一下做者的一次自定义类加载的经验心得,概要以下:
文章结构
1 类加载的各个流程讲解
2 自定义类加载器讲解
3 实战自定义类加载器
做者找了下网上的图,参考着本身画了一张类生命周期流程图:
注意点:图中各个流程并非严格的前后顺序,好比在进行1加载时,其实2验证已经开始了,是交叉进行的。
加载阶段说白了,就是把咱们编译后的.Class静态文件转换到内存中(方法区),而后暴露出来让程序员能访问到。具体展开:
加载阶段得到的二进制字节流并不必定是来自.class文件,好比网络上发来的,那么若是不进行必定的格式校验,确定是不能加载的。因此验证阶段其实是为了保护JVM的。对于通常Javaer来讲,俺们都是.java文件编译出来的.class文件,而后转换成相应的二进制流,没啥危害。因此不用太关心这一部分。
准备阶段主要是给static变量分配内存(方法区中),并设置初始值。
好比: public static Integer value =1;在准备阶段的值实际上是为0的。须要注意的是常量是在准备阶段赋值的:
public static final Integer value =1 ;在准备阶段value就被赋值为了1;
解析阶段就更抽象了,稍微说一下,由于不过重要,有两个概念,符号引用,直接引用。说的通俗一点可是不太准确,好比在类A中调用了new B();你们想想,咱们编译完成.class文件后其实这种对应关系仍是存在的,只是以字节码指令的形式存在,好比 "invokespecial #2" 你们能够猜到#2其实就是咱们的类B了,那么在执行这一行代码的时候,JVM咋知道#2对应的指令在哪,这就是一个静态的家伙,假如类B已经加载到方法区了,地址为(#f00123),因此这个时候就要把这个#2转成这个地址(#f00123),这样JVM在执行到这时不就知道B类在哪了,就去调用了。(说的这么通俗,我都怀疑人生了).其余的,像方法的符号引用,常量的符号引用,其实都是一个意思,你们要明白,所谓的方法,常量,类,都是高级语言(Java)层面的概念,在.class文件中,它才无论你是啥,都是以指令的形式存在,因此要把那种引用关系(谁调用谁,谁引用谁)都转换为地址指令的形式。好了。说的够通俗了。你们凑合理解吧。这块其实不过重要,对于大部分coder来讲,因此我就通俗的讲了讲。
这一块其实就是调用类的构造方法,注意是类的构造方法,不是实例构造函数,实例构造函数就是咱们一般写的构造方法,类的构造方法是自动生成的,生成规则:
static变量的赋值操做+static代码块
按照出现的前后顺序来组装。
注意:1 static变量的内存分配和初始化是在准备阶段.2 一个类能够是不少个线程同时并发执行,JVM会加锁保证单一性,因此不要在static代码块中搞一些耗时操做。避免线程阻塞。
使用就是你直接new或者经过反射.newInstance了.
卸载是自动进行的,gc在方发区也会进行回收.不过条件很苛刻,感兴趣能够本身看一看,通常都不会卸载类.
类加载器,就是执行上面类加载流程的一些类,系统默认的就有一些加载器,站在JVM的角度,就只有两类加载器:
<JAVA_HOME>
/lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。<JAVA_HOME>
/lib/ext目录或java.ext.dirs系统变量指定的路径中的全部类库。若是一个类加载器收到类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器完成。每一个类加载器都是如此,只有当父加载器在本身的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试本身去加载。见下图:
须要注意的是,自定义类加载器能够不遵循双亲委派模型,可是图中红色区域这种传递关系是JVM预先定义好的,谁都更改不了。双亲委派模型有什么好处呢?举个例子,好比有人故意在本身的代码中定义了一个String类,包名类名都和JDK自带的同样,那么根据双亲委派模型,类加载器会首先传递到父类加载器去加载,最终会传递到启动类加载器,启动加载类判断已经加载过了,因此程序员自定义的String类就不会被加载。避免程序员本身随意串改系统级的类。
上面说了半天理论,我都有点火烧眉毛的想上代码了。下面看看如何来自定义类加载器,而且如何在自定义加载器时遵循双亲委派模型(向上传递性).其实很是简单,在这里JDK用到了模板的设计模式,向上传递性其实已经帮咱们封装好了,在ClassLoader中已经实现了,在loadClass方法中:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 检查是否已经加载过。 Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //2 .若是没有加载过,先调用父类加载器去加载 c = parent.loadClass(name, false); } else { // 2.1 若是没有加载过,且没有父类加载器,就用BootstrapClassLoader去加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { //3. 若是父类加载器没有加载到,调用findClass去加载 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; } }
从上面代码能够明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大体过程以下:
由上面能够知道,抽象类ClassLoader的findClass函数默认是抛出异常的。而前面咱们知道,loadClass在父加载器没法加载类的时候,就会调用咱们自定义的类加载器中的findeClass函数,所以咱们必需要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.
若是是是读取一个指定的名称的类为字节数组的话,这很好办。可是如何将字节数组转为Class对象呢?很简单,Java提供了defineClass方法,经过这个方法,就能够把一个字节数组转为Class对象啦~
defineClass:将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组.
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null);
上面介绍了自定义类加载器的原理和几个重要方法(loadClass,findClass,defineClass),相信大部分小伙伴仍是一脸蒙蔽,不要紧,我先上一副图,而后上一个自定义的类加载器:
import java.io.InputStream; public class MyClassLoader extends ClassLoader { public MyClassLoader() { } public MyClassLoader(ClassLoader parent) { //必定要设置父ClassLoader不是ApplicationClassLoader,不然不会执行findclass super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //1. 覆盖findClass,来找到.class文件,而且返回Class对象 try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null) { //2. 若是没找到,return null return null; } byte[] b = new byte[is.available()]; is.read(b); //3. 讲字节数组转换成了Class对象 return defineClass(name, b, 0, b.length); } catch (Exception e) { e.printStackTrace(); } return null; } }
稍微说一下:
其实很简单,继承ClassLoader对象,覆盖findClass方法,这个方法的做用就是找到.class文件,转换成字节数组,调用defineClass对象转换成Class对象返回。就这么easy..
演示下效果:
MyClassLoader mcl = new MyClassLoader(); Class<?> c1 = Class.forName("Student", true, mcl); Object obj = c1.newInstance(); System.out.println(obj.getClass().getClassLoader()); System.out.println(obj instanceof Student);
返回结果:
sun.misc.Launcher$AppClassLoader@6951a712
true
MyClassLoader mcl = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent()); Class<?> c1 = Class.forName("Student", true, mcl); Object obj = c1.newInstance(); System.out.println(obj.getClass().getClassLoader()); System.out.println(obj instanceof Student);
返回结果:
MyClassLoader@3918d722
false
重点分析:
第一个代码和第二个代码惟一一点不一样的就是在new MyClassLoader()时,一个传入的ClassLoader.getSystemClassLoader().getParent();(这个其实就是扩展类加载器)
当不传入这个值时,默认的父类加载器为Application ClassLoader,那么你们能够知道,在这个加载器中已经加载了Student类(ClassPath路径下的Student类),咱们在调用Class.forName时传入了自定义的类加载器,会调用自定义类加载器的loadClass,判断本身以前没有加载过,而后去调用父类的(ApplicationClassLoader)的loadClass,判断结果为已经加载,因此直接返回。因此打印ClassLoader为AppClassLoader.
验证默认父类加载器为ApplicationClassLoader:
MyClassLoader mcl = new MyClassLoader(); System.out.println(mcl.getParent().getClass());
打印结果:class sun.misc.Launcher$AppClassLoader
当咱们传入父类加载器为扩展类加载器时,当调用父类(扩展类加载器)的loadeClass时,因为扩展类加载器只加载java_home/lib/ext目录下的类,因此classpath路径下的它不能加载,返回null,根据loadClass的逻辑,接着会调用自定义类加载器findClass来加载。因此打印ClassLoader为MyClassLoader.
自定义类加载器就给你们说完了,虽然做者感受已经讲清楚了,由于无非就是几个方法的问题(loadClass,findClass,defineClass),但仍是给你们几个传送门,能够多阅读阅读,相互参阅一下:
www.cnblogs.com/xrq730/p/48…
www.importnew.com/24036.html
其实上面基本已经把自定义类加载器给讲清楚了,这里和你们分享一下做者一次实际的编写自定义类加载器的经验。背景以下:
咱们在项目里使用了某开源通信框架,但因为更改了源码,作了一些定制化更改,假设更改源码前为版本A,更改源码后为版本B,因为项目中部分代码须要使用版本A,部分代码须要使用版本B。版本A和版本B中全部包名和类名都是同样。那么问题来了,若是只依赖ApplicationClassLoader加载,它只会加载一个离ClassPath最近的一个版本。剩下一个加载时根据双亲委托模型,就直接返回已经加载那个版本了。因此在这里就须要自定义一个类加载器。大体思路以下图:
这里须要注意的是,在自定义类加载器时必定要把父类加载器设置为ExtentionClassLoader,若是不设置,根据双亲委托模型,默认父类加载器为ApplicationClassLoader,调用它的loadClass时,会断定为已经加载(版本A和版本B包名类名同样),会直接返回已经加载的版本A,而不是调用子类的findClass.就不会调用咱们自定义类加载器的findClass去远程加载版本B了。
顺便提一下,做者这里的实现方案实际上是为了遵循双亲委托模型,若是做者不遵循双亲委托模型的话,直接自定义一个类加载器,覆盖掉loadClass方法,不让它先去父类检验,而改成直接调用findClass方法去加载版本B,也是能够的.你们必定要灵活的写代码。
结语
好了,JVM类加载机制给你们分享完了,但愿你们在碰到实际问题的时候能想到自定义类加载器来解决 。Have a good day .
上文提到过双亲委派模型并非一个强制性的约束模型,而是 Java设计者推荐给开发者的类加载器实现方式。在Java 的世界中大部分的类加载器都遵循这个模型,但也有例外。
双亲委派模型的一次“被破坏”是由这个模型自身的缺陷所致使的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载) ,基础类之因此称为“基础”,是由于它们老是做为被用户代码调用的API ,但世事每每没有绝对的完美,若是基础类又要调用回用户的代码,那该怎么办?这并不是是不可能的事情,一个典型的例子即是JNDI 服务,JNDI如今已是Java的标准服务,它的代码由启动类加载器去加载(在 JDK 1.3时放进去的rt.jar),但JNDI 的目的就是对资源进行集中管理和查找,它须要调用由独立厂商实现并部署在应用程序的Class Path下的JNDI 接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识” 这些代码 ,由于启动类加载器的搜索范围中找不到用户应用程序类,那该怎么办?为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器能够经过java.lang.Thread类的setContextClassLoader()方法进行设置,若是建立线程时还未设置,它将会从父线程中继承一个,若是在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。
有了线程上下文类加载器,就能够作一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所须要的 SPI代码,也就是父类加载器请求子类加载器去完成类加载的动做,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器 ,实际上已经违背了双亲委派模型的通常性原则,但这也是迫不得已的事情。Java中全部涉及SPI的加载动做基本上都采用这种方式,例如JNDI 、JDBC、JCE、 JAXB 和JBI等。
双亲委派模型的另外一次“被破坏”是因为用户对程序动态性的追求而致使的,这里所说的“ 动态性”指的是当前一些很是“热门”的名词:代码热替换(HotSwap)、模块热部署(HotDeployment)等 ,说白了就是但愿应用程序能像咱们的计算机外设那样,接上鼠标、U盘,不用重启机器就能当即使用,鼠标有问题或要升级就换个鼠标,不用停机也不用重启。对于我的计算机来讲,重启一次其实没有什么大不了的,但对于一些生产系统来讲,关机重启一次可能就要被列为生产事故,这种状况下热部署就对软件开发者,尤为是企业级软件开发者具备很大的吸引力。Sun 公司所提出的JSR-29四、JSR-277规范在与 JCP组织的模块化规范之争中落败给JSR-291(即 OSGi R4.2),虽然Sun不甘失去Java 模块化的主导权,独立在发展 Jigsaw项目,但目前OSGi已经成为了业界“ 事实上” 的Java模块化标准,而OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每个程序模块( OSGi 中称为Bundle)都有一个本身的类加载器,当须要更换一个Bundle 时,就把Bundle连同类加载器一块儿换掉以实现代码的热替换。
在OSGi环境下,类加载器再也不是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索:
1)将以java.*开头的类委派给父类加载器加载。
2)不然,将委派列表名单内的类委派给父类加载器加载。
3)不然,将Import列表中的类委派给 Export这个类的Bundle的类加载器加载。
4)不然,查找当前Bundle的 Class Path,使用本身的类加载器加载。
5)不然,查找类是否在本身的Fragment Bundle中,若是在,则委派给 Fragment Bundle的类加载器加载。
6)不然,查找Dynamic Import列表的 Bundle,委派给对应Bundle的类加载器加载。
7)不然,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派规则,其他的类查找都是在平级的类加载器中进行的。
只要有足够意义和理由,突破已有的原则就可认为是一种创新。正如OSGi中的类加载器并不符合传统的双亲委派的类加载器,而且业界对其为了实现热部署而带来的额外的高复杂度还存在很多争议,但在Java 程序员中基本有一个共识:OSGi中对类加载器的使用是很值得学习的,弄懂了OSGi的实现,就能够算是掌握了类加载器的精髓。
Tomcat的类加载器架构
主流的Java Web服务器(也就是Web容器) ,如Tomcat、Jetty、WebLogic、WebSphere 或其余笔者没有列举的服务器,都实现了本身定义的类加载器(通常都不止一个)。由于一个功能健全的 Web容器,要解决以下几个问题:
1)部署在同一个Web容器上 的两个Web应用程序所使用的Java类库能够实现相互隔离。这是最基本的需求,两个不一样的应用程序可能会依赖同一个第三方类库的不一样版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库能够互相独立使用。
2)部署在同一个Web容器上 的两个Web应用程序所使用的Java类库能够互相共享 。这个需求也很常见,例如,用户可能有10个使用spring 组织的应用程序部署在同一台服务器上,若是把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,若是类库不能共享,虚拟机的方法区就会很容易出现过分膨胀的风险。
3)Web容器须要尽量地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的Java Web容器自身也是使用Java语言来实现的。所以,Web容器自己也有类库依赖的问题,通常来讲,基于安全考虑,容器所使用的类库应该与应用程序的类库互相独立。
4)支持JSP应用的Web容器,大多数都须要支持 HotSwap功能。咱们知道,JSP文件最终要编译成Java Class才能由虚拟机执行,但JSP文件因为其纯文本存储的特性,运行时修改的几率远远大于第三方类库或程序自身的Class文件 。并且ASP、PHP 和JSP这些网页应用也把修改后无须重启做为一个很大的“优点”来看待 ,所以“主流”的Web容器都会支持JSP生成类的热替换 ,固然也有“非主流”的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化。
因为存在上述问题,在部署Web应用时,单独的一个Class Path就没法知足需求了,因此各类 Web容都“不约而同”地提供了好几个Class Path路径供用户存放第三方类库,这些路径通常都以“lib”或“classes ”命名。被放置到不一样路径中的类库,具有不一样的访问范围和服务对象,一般,每个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库 。如今,就以Tomcat 容器为例,看一看Tomcat具体是如何规划用户类库结构和类加载器的。
在Tomcat目录结构中,有3组目录(“/common/*”、“/server/*”和“/shared/*”)能够存放Java类库,另外还能够加上Web 应用程序自身的目录“/WEB-INF/*” ,一共4组,把Java类库放置在这些目录中的含义分别以下:
①放置在/common目录中:类库可被Tomcat和全部的 Web应用程序共同使用。
②放置在/server目录中:类库可被Tomcat使用,对全部的Web应用程序都不可见。
③放置在/shared目录中:类库可被全部的Web应用程序共同使用,但对Tomcat本身不可见。
④放置在/WebApp/WEB-INF目录中:类库仅仅能够被此Web应用程序使用,对 Tomcat和其余Web应用程序都不可见。
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系以下图所示。
上图中灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的做用已经介绍过了。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat本身定义的类加载器,它们分别加载/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器一般会存在多个实例,每个Web应用程序对应一个WebApp类加载器,每个JSP文件对应一个Jsp类加载器。
从图中的委派关系中能够看出,CommonClassLoader能加载的类均可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader本身能加载的类则与对方相互隔离。WebAppClassLoader可使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并经过再创建一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
对于Tomcat的6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正创建Catalina ClassLoader和Shared ClassLoader的实例,不然在用到这两个类加载器的地方都会用Common ClassLoader的实例代替,而默认的配置文件中没有设置这两个loader项,因此Tomcat 6.x瓜熟蒂落地把/common、/server和/shared三个目录默认合并到一块儿变成一个/lib目录,这个目录里的类库至关于之前/common目录中类库的做用。这是Tomcat设计团队为了简化大多数的部署场景所作的一项改进,若是默认设置不能知足须要,用户能够经过修改配置文件指定server.loader和share.loader的方式从新启用Tomcat 5.x的加载器架构。
Tomcat加载器的实现清晰易懂,而且采用了官方推荐的“正统”的使用类加载器的方式。若是读者阅读完上面的案例后,能彻底理解Tomcat设计团队这样布置加载器架构的用意,那说明已经大体掌握了类加载器“主流”的使用方式,那么笔者不妨再提一个问题让读者思考一下:前面曾经提到过一个场景,若是有10个Web应用程序都是用Spring来进行组织和管理的话,能够把Spring放到Common或Shared目录下让这些程序共享。Spring要对用户程序的类进行管理,天然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?若是研究过虚拟机类加载器机制中的双亲委派模型,相信读者能够很容易地回答这个问题。
分析:若是按主流的双亲委派机制,显然没法作到让父类加载器加载的类 去访问子类加载器加载的类,上面在类加载器一节中提到过经过线程上下文方式传播类加载器。
答案是使用线程上下文类加载器来实现的,使用线程上下文加载器,可让父类加载器请求子类加载器去完成类加载的动做。看spring源码发现,spring加载类所用的Classloader是经过Thread.currentThread().getContextClassLoader()来获取的,而当线程建立时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为 AppClassLoader,spring中始终能够获取到这个AppClassLoader( 在 Tomcat里就是WebAppClassLoader)子类加载器来加载bean ,之后任何一个线程均可以经过 getContextClassLoader()获取到WebAppClassLoader来getbean 了 。
本篇博文内容取材自《深刻理解Java虚拟机:JVM高级特性与最佳实践》