一、前奏,举个生活中的小栗子java
二、为什么Java类型加载、链接在程序运行期完成?数据库
三、一个类在什么状况下才会被加载到JVM中?数组
什么是主动使用、被动使用?代码示例助你透彻理解类初始化的时机。缓存
四、类的加载(Loading)内幕透彻剖析tomcat
类加载作的那些事儿、双亲委派模型工做过程、ClassLoader源码解析安全
五、Tomcat如何打破双亲委派模型的网络
六、上下文类加载器深刻浅出剖析数据结构
七、最后总结并发
春节立刻要到了,你们是否是都在火烧眉毛的等着回家团圆了呢?框架
大春运早已启动,回家的过程实际上是个「辛苦活」,有的同窗尚未买到票呢,蒙眼狂奔终于抢到了,发现居然是个站票~,退了,连站票的机会都没了吧?
昨天还听一位同窗说:『嘿嘿,去年我提早就买到票了,可是... 可是... 去错火车站了。。。尼玛,当时那是啥心情啊~ 幸运的是后来又刷到票了,否则就真回不去了!』
回家大部分朋友都要乘坐交通工具,无论你乘坐什么样的交通工具出行,对于「交通管理」内部来讲,最最重要的任务就是保障你们得出行安全。
那么如何保障你们的出行安全呢?
乘坐地铁、飞机等这些公共交通工具,必不可少的最重要的环节就是『安检』,不是什么东西均可以随便让你带的,都是有明文规定的,好比易燃易爆、酒类等都是有限制的。
交通出行的大致过程,有点相似类文件加载到Java虚拟机(简称 JVM)的过程,程序中运行的各类类文件(好比Java、Kotlin),也是要必须通过『安检』的,才能容许进入到JVM中的,一切都是为了安全。
固然,安检的标准是不一样的。
接下来,咱们进入正题,一块儿来看看类文件是如何被加载到JVM当中的。
上图的对比只是为了方便理解 ,抽象出来一层『安全检查』,其实就是『类加载』的过程。
这个过程JVM当中约束了规范和标准,都会通过加载、验证、准备、解析、初始化五个阶段。
这里必定要说一个概念,我的认为对于理解类加载过程挺重要的。
更准确的说法,应该是类型
的加载过程,在Java代码中,类型的加载、链接、初始化都是在程序运行时完成的。
这里的类型,是指你在开发代码时常见的class、interface、enum这些关键字的定义,并非指具体的class对象。
举个🌰:
Object obj = new Object();
new出来的obj是Object类型吗?固然不是,obj只是经过new建立出来的Object对象,而类型实际是Object类自己
。而要想建立Object对象的前提,必需要有类型的信息,才能在Java堆中建立出来。因此,这里要明确区分开。
绝大多数状况下,类型是提早编写好的,好比Object类是由JDK已经提供的。另一些状况是能够在运行期间动态的生成出来,好比动态代理(程序运行期完成的)。
其实,运行区间能作这件事,就为一些有创意的开发人员提供了不少的可能性。一切的文件都已经存在,程序运行的过程当中能够采起一些特殊的处理方式把这些以前已经存在或者运行期生成出来的这些类型有机的装配在一块儿。
Java自己是一门静态的语言,而他的不少特性又具备动态语言才能拥有的特质,也所以类型的加载、链接和初始化在运行期间完成起到了很大的帮助做用。
类型的加载:查找并加载类的二进制数据(字节码文件),最多见的,是将类的Class文件从磁盘加载到内存中。
类型的链接:将类与类的关系肯定好,对于字节码相关的处理、验证、校验在加载链接阶段去完成的。字节码自己能够被人为操纵的,也所以可能有恶意的可能性,因此须要校验。
验证:确保被加载类的正确性,就是要按照JVM规范定义的。
准备:为类的静态变量分配内存,并将其初始化为默认值
class Test { public static int num = 1; }
上述代码示例中的中间过程,在将类型加载到内存过程当中,num分配内存,首先设置为0,1是在后续的初始化阶段赋值给num变量。
符号引用: 间接的引用方式,经过一个符号的表示一个类引用了另外的类。 直接引用:直接引用到目标对象中的内存的位置
初始化阶段:为类的静态变量赋予正确的初始值。
类型的初始化:好比一些静态的变量的赋值是在初始化阶段完成的。
### 三、一个类在什么状况下才会被加载到JVM中?
Java程序对类的使用方式可分为两种:
主动使用
被动使用
特别的重要:
全部的Java虚拟机实现必须在每一个类或接口被java程序首次主动使用时才初始化他们。
主动使用(八种状况
):
1)建立类的实例,好比new一个对象
2)访问某一个类或接口的静态变量,或者对该静态变量赋值 (访问类的静态变量的助记符getstatic,赋值是putstatic)。
3)调用类的静态方法 (应用invokestatic助记符)。
4)使用java.lang.reflect包的方法对类型进行反射调用,好比:Class.forName(“com.test.Test") 经过反射的方式获取类的Class对象。
5)初始化一个类的子类,好比有class Parent{}、子类class Child extends Parent{},当初始化Child类时也表示对Parent类的主动使用,Parent类也要所有初始化。
6)Java虚拟机启动时被标注为启动类的类,即有main方法的类。
7)JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic, REF_putStatic, REF_invokeStatic句柄对应的类没有初始化,则初始化。
8)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,若是有这个接口的实现类发生了初始化,那该接口要在其以前被初始化。
除了上述所讲的八种状况,其余使用Java类的方式都被看做是类的被动使用,都不会致使类的初始化。
另外,要特别说明的一点
:
接口的加载过程与类加载过程会有所不一样,接口不能使用 「static{}」语句块,可是编译器会为接口生成对应的
主动使用的第5种:当子类初始化时,要求其父类也要所有初始化完成。可是,对于一个接口的初始化时,并不要求其父接口要所有初始化完成,只有在真正使用到父接口时(好比引用接口中定义的常量)时才会去初始化,有点延迟加载的意思。
被动使用示例:
1)经过子类引用父类的静态字段,不会致使子类的初始化
public class Parent { static { System.out.println("Parent init...."); } public static int a = 123; } public class Child extends Parent { static { System.out.println("Child init..."); } } // Test类打印,子类直接调用父类的静态字段 public static void main(String[] args) { System.out.println(Child.a); }
输出结果:
Parent init.... 123
根据输出结果看到,不会输出 Child init...,经过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化,对于静态字段,只有直接定义这个字段的类才会被初始化。
2) 建立数组类对象,并不会致使引用的类初始化
public class Child extends Parent { static { System.out.println("Child init..."); } } // 使用 Child 引用建立个数组 public static void main(String[] args) { Child[] child = new Child[1]; System.out.println(child); }
输出结果:
[Lcom.dskj.jvm.beidong.Child;@7852e922
并无输出Child init...证实并无初始化com.dskj.jvm.beidong.Child类,根据输出结果看到了[Lcom.dskj.jvm.beidong.Child
,带了[L
说明触发了数组类的初始化阶段,它是由JVM自动生成的,继承自java.lang.Object类,因为anewarray
助记符触发建立动做的。
对于数组来讲,JavaDoc一般将其所构成的元素称做为Component,实际上就是将数组下降一个维度的类型。
助记符:
anewarray:表示建立一个引用类型的(如类、接口、数组)数组,并将其引用值压入栈顶。
newarray:表示建立一个指定的原始类型的(如int、float、char、short、double、boolean、byte)的数组,并将其引用值压入栈顶。
对应字节码内容:
3)调用ClassLoader的loadClass()方法,不会致使类的初始化。
代码以下:
public class LoadClassTest { public static void main(String[] args) { try { ClassLoader.getSystemClassLoader().loadClass("com.dskj.jvm.passivemode.LoadClass"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } class LoadClass { public static final String STR = "Hello World"; static { System.out.println("LoadClass init..."); } }
没有输出 LoadClass init...,证实了调用系统类加载器的loadClass()方法,并不会初始化LoadClass类,由于ClassLoader#loadClass()方法内部传入的resolve参数为false,表示Class不会进入到链接
阶段,也就不会致使类的初始化。
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { ... if (resolve) { //** Links the specified class** resolveClass(c); } }
4)final修饰的常量,编译时会存入调用类常量池中,本质上没有引用到定义常量的类,不会致使类的初始化动做。
看下面代码:
public class ConstClassTest { public static void main(String[] args) { System.out.println(ConstClass.STR); } } class ConstClass { static { System.out.println("ConstClass init..."); } public static final String STR = "Hello World"; }
输出结果:
Hello World
结果只会输出 Hello World,不会输出ConstClass init...,ConstClassTest类对常量ConstClass.STR的引用,实际被转化为ConstClassTest类对自身常量池的引用了。也就是说,实际上ConstClassTest的Class文件之中并无ConstClass类的符号引用入口。
编译完成,两个ConstClassTest和ConstClass就没有任何关系了。这句话如何能证实一下?
你能够先运行一次,而后将编译后的ConstClass.class文件从磁盘上删除掉,再次运行跟上面输出结果是同样的。
还不信?以下图所示Idea中的运行结果:
在IDEA下测试时,若是你使用的Gradle来构建,模拟上面的删除class文件过程,要使用 xxx/out/production/ 目录下生成编译后的class文件,当类没有发生变化时不会从新生成class文件。若是使用默认的 xxx/build/xx,每次运行都会从新生成新的class文件。
若是有问题,能够在 Project Settings -> Modules -> 项目的 Paths 中调整编译输出目录。
咱们继续在这个示例基础上作修改:
public class ConstClassTest { public static void main(String[] args) { System.out.println(ConstClass.STR); } } class ConstClass { // STR 定义的常量经过UUID生成一个随机串 public static final String STR = "Hello World" + UUID.randomUUID(); static { System.out.println("ConstClass init..."); } }
注意,这里 STR 常量经过UUID生成一个随机串,编译是经过的。
直接运行,输出结果:
ConstClass init... Hello World:d26d7f1d-2d46-41cb-b5dc-2b7b3fe61e74
看到了ConstClass init...,说明ConstClass类被初始化了。
将ConstClass.class文件删除后,再次运行:
Exception in thread "main" java.lang.NoClassDefFoundError: com/dskj/jvm/passivemode/ConstClass at com.dskj.jvm.passivemode.ConstClassTest.main(ConstClassTest.java:7) Caused by: java.lang.ClassNotFoundException: com.dskj.jvm.passivemode.ConstClass at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 1 more
你们看到了吗?ConstClass.class文件被删除后,再次运行就发生了 java.lang.NoClassDefFoundError
异常了,为何?正是由于 ConstClass 类里定义的STR常量并不是编译器可以肯定的值,那么其值就不会被放到调用类的常量池中。
这个示例能够好好理解下,同时印证了该类的初始化时机中,主动使用和被动使用的场景。
你们记住一个类的8种主动使用状况,都是在开发过程当中常见的使用方式。另外,注意下被动使用的几种状况,结合上面的列举的代码示例透彻理解。
类加载全过程的每个阶段,结合前文给出的图示,详细展开。
前面提到的类文件,就是后缀文件为.class
的二进制文件。
#### JVM在加载阶段主要完成以下三件事
1)经过一个类的全限定名,即包名+类名
来获取定义此类的二进制字节流。
2)将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
3)JVM内存中生成一个表明该类的java.lang.Class对象
,做为方法区这个类的各类数据的访问入口。
对于第一点来讲,并无要求这个二进制字节流,具体以什么样的方式从Class文件中读取。
经过下面一张图来汇总一下:
解释下比较常见的Class文件读取方式:
1)从ZIP包中读取Class文件,流行的SpringBoot/SpringCoud框架基本都打成Jar包形式,内嵌了Tomcat,俗称Fat Jar
,经过java -jar能够直接启动,很是方便。
另外,还有一些项目仍然是使用War包形式,而且使用单独使用Tomcat这类应用容器来部署的。
2)运行时生成的Class文件,应用最多的就是动态代理技术了,好比CGLIB、JDK动态代理。
思考个问题,这些Class文件是由谁来加载的呢?
实现这个动做的代码正是类加载器来完成的,类加载器在类层次划分、OSGi、程序热部署、代码加密等领域大放异彩,成为Java技术体系中一块重要的基石。
对于任意一个类,如何肯定在JVM当中的惟一性?必须是由加载该类的类加载器和该类自己一块儿共同确立在JVM中的惟一性。
每个类加载器,都拥有一个独立的类名称空间。通俗理解:比较两个类是否『相等』,这两个类只有在同一个类加载器加载的前提下才有意义。不然,即便这两个类来源于同一个Class文件,被同一个JVM加载,只要加载它们的类加载器不一样,那这两个类就一定不相等。
类加载器之间是什么关系?
以下图所示,三种加载器之间的层次关系被称为类加载器的 『双亲委派模型(Parents Delegation Model)』。
双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应有本身的父类加载器。不过这里类加载器之间的父子关系通常不是以继承(Inheritance)的关系来实现的,而是一般使用组合(Composition)关系来复用父加载器的代码。图:
这里说个有意思的问题,不止一次在某些文章留言中看到纠结:『为何叫作双亲?』国外文章写的 parent delegation model,这里的parent不是单亲吗??应该翻译为单亲委派模型才对,全互联网都跟着错误走。。。其实parent这个英文单词翻译过来也有双亲的意思,不须要作个『杠精』,没啥意义哈。
结合类加载器的自底向上的委托关系总结:
假设一个类处于ClassPath下,版本是JDK8,默认使用应用类加载器进行加载。
1)当应用类加载器收到了类加载的请求,会把这个请求委派给它的父类(扩展类)加载器去完成。
2)扩展类加载器收到类加载的请求,会把这个请求委派给它的父类(引导类)加载器去完成。
3)引导类加载器收到类加载的请求,查找下本身的特定库是否能加载该类,即在rt.jar、tools.jar...包中的类。发现不能呀!返回给扩展类加载器结果。
4)扩展类加载器收到返回结果,查找下本身的扩展目录下是否能加载该类,发现不能啊!返回给应用类加载器结果。
5)应用类加载器收到结果,额!都没有加载成功,那只能本身加载这个类了,发如今ClassPath中找到了,加载成功。
你对并发很感兴趣,本身建立了个跟JDK同样的全限定名类LongAdder, java.util.concurrent.atomic.LongAdder
,而后程序启动交给类加载器去加载,能成功吗?
固然不能!这个LongAdder是 Doug Lea 大神写的,贡献到JDK并发包下的,而且被安排在rt.jar包中了,所以是由 Bootstrap ClassLoader 类加载器优先加载的,别人谁写一样的类,那就是故意跟JDK做对,是绝对不允许的。
即便你写了一样的类,编译能够经过,可是永远不会被加载运行,被JDK直接忽略掉。
双亲委派模型在JDK中内部是如何实现的?
JDK中提供了一个抽象的类加载器 ClassLoader,其中提供了三个很是核心的方法。
public abstract class ClassLoader { //每一个类加载器都有个父加载器 private final ClassLoader parent; public Class<?> loadClass(String name) { //查找一下这个类是否是已经加载过了 Class<?> c = findLoadedClass(name); //若是没有加载过 if( c == null ){ //先委托给父加载器去加载,注意这是个递归调用 if (parent != null) { c = parent.loadClass(name); }else { // 若是父加载器为空,查找Bootstrap加载器是否是加载过了 c = findBootstrapClassOrNull(name); } } // 若是父加载器没加载成功,调用本身的findClass去加载 if (c == null) { c = findClass(name); } return c; } protected Class<?> findClass(String name){ //1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存 ... //2. 调用defineClass将字节数组转成Class对象 return defineClass(buf, off, len); } // 将字节码数组解析成一个Class对象,用native方法实现 protected final Class<?> defineClass(byte[] b, int off, int len){ ... } }
参见ClassLoader核心代码注释,提取和印证几个关键信息:
1)JVM 的类加载器是分层次的,它们有父子关系,每一个类加载器都有个父加载器,是parent字段。
2)loadClass() 方法是 public 修饰的,说明它才是对外提供服务的接口。根据源码可看出这是一个递归调用,父子关系是一种组合关系,子加载器持有父加载器的引用,当一个类加载器须要加载一个 Java 类时,会先委托父加载器去加载,而后父加载器在本身的加载路径中搜索 Java 类,当父加载器在本身的加载范围内找不到时,才会交还给子加载器加载,这就是所谓的『双亲委托模型』。
3)findClass() 方法的主要职责就是找到 .class 文件,可能来自磁盘或者网络,找到后把.class文件读到内存获得byte[]字节码数组,而后调用 defineClass() 方法获得 Class 对象。
4)defineClass() 是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 经过 JNI 机制调用。
JDK8以及以前的JDK版本都是以下三层类加载器实现方式。
1)启动类加载器(Bootstrap ClassLoader),这个类加载器是由C++实现的,负载加载$JAVA_HOME/jre/lib目录下的jar文件,好比 rt.jar、tools.jar,或者-Xbootclasspath系统环境变量指定目录下的路径。它是个超级公民,即便开启了Security Manager的时候,它也能拥有加载程序的全部权限,使用null做为扩展类加载器的父类。
同时,启动类加载器在JVM启动后也用于加载扩展类加载器和系统类加载器。
2)扩展类加载器(Extension ClassLoader),这个类加载器由sun.misc.Launcher$ExtClassLoader
来实现,负责加载$JAVA_HOME/jre/lib/ext目录中,或者java.ext.dirs系统变量指定路径中全部的类库,容许用户将具有通用性的类库能够放到ext目录下,扩展Java SE功能。在JDK 9以后,这种扩展机制被模块化带来的自然的扩展能力所取代。
3)应用类加载器(App/System ClassLoader),也称做为系统类加载器,这个类加载器由sun.misc.Launcher$AppClassLoader
来实现。 它负责加载用户应用类路径(ClassPath)上全部的类库,开发者一样能够直接在代码中使用这个类加载器。若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。
##### JDK9中的类加载器有哪些变化?
1)扩展类加载器被重命名为平台类加载器(Platform ClassLoader),部分不须要 AllPermission 的 Java 基础模块,被降级到平台类加载器中,相应的权限也被更精细粒度地限制起来。
2) 扩展类加载器机制被移除。这会带来什么影响呢?就是说若是咱们指定 java.ext.dirs 环境变量,或者 $JAVA_HOME/jre/lib/ext目录存在,JVM会返回错误。 建议解决办法就是将其放入 classpath 里。部分不须要 AllPermission 的 Java 基础模块,被降级到平台类加载器中,相应的权限也被更精细粒度地限制起来。
3)在$JAVA_HOME/jre/lib路径下的 rt.jar 和 tools.jar 一样是被移除了。JDK 的核心类库以及相关资源,被存储在 jimage 文件中,并经过新的 JRT 文件系统访问,而不是原有的 JAR 文件系统。
4)增长了 Layer 的抽象, JVM 启动默认建立 BootLayer,开发者也能够本身去定义和实例化 Layer,能够更加方便的实现相似容器通常的逻辑抽象。
新增的Layer的抽象,去内部的BootLayer做为内建类加载器,包括了 BootStrap Loader、Platform Loader、Application Loader,其余 Layer 内部有自定义的类加载器,不一样版本模块能够同时工做在不一样的 Layer。
结合了 Layer,目前最新的 JVM 内部结构以下图所示:
由于JDK里的类加载器ClassLoader是抽象类,若是你自定义类加载器能够重写 findClass() 方法,重写 findClass() 方法仍是会按照既定的双亲委派机制运做的。
而咱们发现loadClass()方法也是public修饰的,说明也是容许重写的,重写loadClass()方法就能够『随心所欲』了,不按照既定套路出牌了,不遵循双亲委派模型。
典型的就是Tomcat应用容器,就是自定义WebAppClassLoader类加载器,打破了双亲委派模型。
WebAppClassLoader 类加载器具体实现是重写了 ClassLoader 的两个方法:loadClass() 和 findClass()。其大体工做过程:首先类加载器本身尝试去加载某个类,若是找不到再委托代理给父类加载器,其目的是优先加载 Web 应用本身定义的类。
这也正是一个Tomcat可以部署多个应用实例的根本缘由。
接下来,咱们分析下源码实现:
loadClass() 重写方法的源码实现,仅保留最核心的代码便于理解:
// 重写了 loadClass() 方法 public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 使用了synchronized同步锁 synchronized (getClassLoadingLock(name)) { Class<?> clazz = null; //1)先在本地缓存中,查找该类是否已经加载过 clazz = findLoadedClass0(name); if (clazz != null) { if (resolve) // 本地缓存找到,链接该类 resolveClass(clazz); return clazz; } //2) 从系统类加载器的缓存中,查找该类是否已经加载过 clazz = findLoadedClass(name); if (clazz != null) { if (resolve) // 从系统类加载器缓存找到,链接该类 resolveClass(clazz); return clazz; } // 3)尝试用ExtClassLoader类加载器类加载 ClassLoader javaseLoader = getJavaseClassLoader(); try { clazz = javaseLoader.loadClass(name); if (clazz != null) { if (resolve) // 从扩展类加载器中找到,链接该类 resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } // 4)尝试在本地目录查找加载该类 try { clazz = findClass(name); if (clazz != null) { if (resolve) // 从本地目录找到,链接该类 resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } // 5) 尝试用系统类加载器来加载 try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (resolve) // 从系统类加载器中找到,链接该类 resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } //6. 上述过程都加载失败,抛出异常 throw new ClassNotFoundException(name); }
loadClass() 重写的方法实现上会复杂些,毕竟打破双亲委派机制就在这里实现的。
主要有以下几个步骤:
1)先在本地缓存 Cache 查找该类是否已经加载过,即 Tomcat 自定义类加载器 WebAppClassLoader 是否已加载过。
2)若是 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。
3)若是系统类加载器也没有加载过,此时,会让 ExtClassLoader 扩展类加载器去加载,很关键,其目的防止 Web 应用本身的类覆盖 JRE 的核心类。
由于 Tomcat 须要打破双亲委托机制,假如 Web 应用里有相似上面举的例子自定义了 Object 类,若是先加载这些JDK中已有的类,会致使覆盖掉JDK里面的那个 Object 类。
这就是为何 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,由于 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,JRE里的类由BootstrapClassLoader安全加载,而后返回给 Tomcat 的类加载器。
这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
4)若是 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。
5)若是本地目录下没有这个类,说明不是 Web 应用本身定义的类,那么由系统类加载器去加载。这里请你注意:Web 应用是经过Class.forName调用交给系统类加载器的,由于Class.forName的默认加载器就是系统类加载器。
6)若是上述加载过程所有失败,抛出 ClassNotFoundException 异常。
findClass() 重写方法的源码实现,仅展现最核心代码便于理解:
// 重写了 findClass 方法 public Class<?> findClass(String name) throws ClassNotFoundException { ... Class<?> clazz = null; try { //1) 优先在本身Web应用目录下查找类 clazz = findClassInternal(name); } catch (RuntimeException e) { throw e; } if (clazz == null) { try { //2) 若是在本地目录没有找到当前类,则委托代理给父加载器去查找 clazz = super.findClass(name); } catch (RuntimeException e) { throw e; } //3) 若是父类加载器也没找到,则抛出ClassNotFoundException if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }
在 findClass() 重写的方法里,主要有三个步骤:
1)先在 Web 应用本地目录下查找要加载的类。
2)若是没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。
3)如何父加载器也没找到这个类,抛出 ClassNotFoundException 异常。
咱们都知道Jdbc是一个标准,那么具体数据库厂商会根据Jdbc标准提供本身的数据库实现,既然Jdbc是一个标准,这些类原生的会存在JDK中了,好比Connection、Statement,并且是位于rt.jar包中的,他们在启动的时候是由BootstrapClassLoader加载的。
那么怎么具体加载厂商的实现呢?
确定是经过厂商提供相应的jar包,而后放到咱们应用的ClassPath下,这样的话,厂商所提供的jar中的确定不是由启动类加载器去加载的。
因此,厂商的具体驱动的实现是由应用类加载器进行加载的 。
Connection是一个接口,它是由启动类加载器加载的,而它具体的实现启动类加载器没法加载,由系统类加载器加载的。这样会存在什么样的问题?
根据类加载原则:
SPI(Service Provider Interface)
父ClassLoader可使用当前线程Thread.currentThread().getContextClassLoader()所指定的classloader加载的类。
这就改变了父ClassLoader不能使用子ClassLoader或是其余没有直接父子关系的ClassLoader所加载类的状况,即改变了双亲委托模型。
线程上下文类加载器就是当前线程的Current Classloader。
在双亲委托模型下,类加载器是由下而上,即下层的类加载器会委托上层进行加载。可是对于SPI来讲,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不一样jar包(厂商提供),Java的启动类加载器是不会加载其余来源的jar包,这样传统的双亲委托模型就没法知足SPI的要求。
而经过给当前线程设置上下文类加载器,就能够由设置的上下文类加载器来实现对于接口实现类的加载。
线程上下文类加载器的通常使用模式:
获取 ---> 使用 --> 还原
ClassLoader classloader = Thread.currentThread().getContextClassLoader(); try { // 将目标类加载器设置到上下文类加载器 Thread.currentThread().setContextClassLoader(targetTccl); // 在该方法中使用设置的上下文类加载器加载所需的类 doSomethingUsingContextClassLoader(); } finally { // 将原来的classloader设置到上下文类加载器 Thread.currentThread().setContextClassLoader(classloader); }
doSomethingUsingContextClassLoader()方法中则调用了 Thread.currentThread().getContextClassLoader() ,获取当前线程的上下文类加载器作某些事情。
若是一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载的(若是该依赖类以前没有被加载过的话)。
在SPI的接口代码当中,就能够经过上下文类加载器成功的加载到SPI的实现类。所以,上下文类加载器在不少的SPI的实现中都会获得大量的应用。
当高层提供了统一的接口让低层(好比Jdbc各个厂商提供的具体实现类)去实现,同时又要在高层加载(或实例化)低层的类时,就必需要经过线程上下文类加载器来帮助高层的类加载器并加载该类(本质上,高层的类加载器与低层的类加载器是不同的)
通常状况下,咱们没有修改过线程上下文类加载器,默认的就是系统类加载器。因为是运行期间是设置的上下文类加载器,因此,无论当前程序在什么地方,在启动类的加载器的范围内仍是扩展类加载器的范围内,那么咱们在任何有须要的时候都是能够经过Thread.currentThread().getContextClassLoader()获取设置的上下文类加载器来完成操做。
这个也有点像ThreadLocal的类,若是借助于ThreadLocal的话就没有必要同步,由于每个线程都有相应的数据副本,这些数据副本之间是互不干扰的,他们只能被当前的线程所使用和访问,既然每一个线程都有数据副本,每一个线程固然操做的是副本,因此线程之间就不须要同步、锁就能够处理并发。ThreadLocal本质上是用空间换时间的概念,由于咱们将数据拷贝多份会占用必定的内存空间,每一个线程中去使用。
限于篇幅,本文主要对类的初始化时机,类的加载过程当中最重要的类加载器机制进行了分析,对其中的双亲委派模型,以及Tomcat是如何打破双亲委派模型的,结合源代码进行了深刻剖析,对上下文类加载器是如何改变双亲委派模型进行了分析。
总结一下:
一个类都是经过主动使用
的方式加载到JVM当中的,到目前为止一共总结了八种状况,除此以外的都属于被动使用
,被动使用的列举了代码示例,结合示例能够更为清晰的理解。
详细介绍了双亲委派模型的工做过程,JDK8和JDK9版本中类加载器层次关系,类加载器的结果本质上并非一种树形结构,而是一种包含关系。
同时,也介绍了Tomcat是如何打破双亲委派机制的,经过源码透视打破规则的全过程。
最后,对上下文类加载器根据Jdbc的例子,进一步分析了使用模式,如何改变双亲委派机制作到父类加载器,能够加载和使用各个厂商提供的实现类的。
另外,回到最初的图示,一个类要想顺利进入到JVM内存结构中,除了类的加载阶段外,还有验证、准备、解析、初始化四个阶段完成后,才算真正完成类的初始化操做。
在JVM中某个类的Class对象再也不被引用,即不可触及,Class对象就会结束生命周期,该类在方法区内的数据会被卸载,从而技术该类的整个生命周期。
一个类什么时候结束生命周期,取决于表明它的Class对象什么时候结束生命周期。
可是,JVM自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面已经介绍过,JVM自带的类加载器包括引导类加载器、扩展类加载器和系统类加载器(应用类加载器)。Java虚拟机自己会始终引用这些类加载器,而这些类加载器会始终引用它们所加载的类的Class对象,所以这些Class对象是始终可触及的。
在以下状况下,JVM将结束生命周期。
执行了System.exit()
程序正常执行结束
程序在执行过程当中遇到了异常或者错误而异常终止
因为操做系统出现错误而致使Java虚拟机进程终止
你们如何以为本文有收获关个注呗,码字不易,文章不妥之处,欢迎留言斧正。本号不按期会发布精彩原创文章。
参考资料:
深刻理解Java虚拟机
极客时间课程
欢迎关注个人公众号,扫二维码关注得到更多精彩文章,与你一同成长~