JVM学习(四)---类加载过程、类加载器和双亲委派机制的原理
(四)对象的创建过程
HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。
【1】对象的创建
Java 对象的创建过程
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
内存分配并发问题:
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
【2】对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
【3】对象的访问定位
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:
句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
【1】基本介绍
一个类的完整生命周期如下:
【2】类加载的过程
我们自己写的java源文件到最终运行,必须要经过编译和类加载两个阶段。
(1)编译的过程就是把.java文件编译成.class文件。
(2)类加载的过程,就是把class文件装载到JVM内存中,装载完成以后就会得到一个Class对象,我们就可以使用new关键字来实例化这个对象。
Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
(2.1)加载
类加载过程的第一步,主要完成下面3件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
虚拟机规范多上面这3点并不具体,因此是非常灵活的。比如:“通过全类名获取定义此类的二进制字节流” 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)等等。
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
类加载器、双亲委派模型也是非常重要的知识点,这部分内容会在后面的文章中单独介绍到。
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
(2.2)验证
(2.3)准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
- 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。
基本数据类型的零值:
(2.4)解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
(2.5)初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 ()方法的过程。
对于() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。
对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
- 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。
- 当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。
- 当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用时如Class.forname(“…”),newInstance()等等。 ,如果类没初始化,需要触发其初始化。
- 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
- MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。
【3】卸载
卸载类即该类的Class对象被GC。
卸载类需要满足3个要求:
- 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被GC
所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
只要想通一点就好了,jdk自带的BootstrapClassLoader,PlatformClassLoader,AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
【4】类加载器
(1)类加载器的作用
类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过以下步骤:
(1)装载:查找和导入Class文件;
(2)链接:把类的二进制数据合并到JRE中;
1-校验:检查载入Class文件数据的正确性;
2-准备:给类的静态变量分配存储空间;
3-解析:将符号引用转成直接引用;
(3)初始化:对类的静态变量,静态代码块执行初始化操作
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存。
(2)类与类加载器
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均
由 Java 实现且全部继承自java.lang.ClassLoader:
(1)BootstrapClassLoader(启动类加载器)
最顶层的加载类,由C++实现,负责加载支撑JVM运行的位于 %JAVA_HOME%/lib 目录下的jar包和类,或者是被 -Xbootclasspath参数指定的路径中的所有类(如rt.jar、tools.jar、charsets.jar等,名字不符合的类库即使放到lib目录下也不会被加载)。
(2)ExtensionClassLoader(扩展类加载器)
主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包,是一种Java系统类库的扩展机制。
(3)AppClassLoader(应用程序类加载器)
面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。如应用程序中没有默认自己的类加载器,则使用应用程序加载器为默认加载器。
(3)什么是双亲委派机制?
如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类加载器的 loadClass() 处理,父类加载器还存在父类加载器则进一步向上委托,依次递归,请求最终到达顶层启动类加载器BootstrapClassLoader中。如果父类加载器可以完成类加载任务,就成功返回,父类加载器无法完成加载任务,则子类加载器会尝试自己去加载。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。
(4)为什么要使用双亲委派机制?
(1)jvm认定两个对象同属于一个类型的条件
jvm如何认定两个对象同属于一个类型,必须同时满足下面两个条件:
(1)都是用同名的类完成实例化的。
(2)两个实例各自对应的同名的类的加载器必须是同一个。
比如两个相同名字的类,一个是用系统加载器加载的,一个扩展类加载器加载的,两个类生成的对象将被jvm认定为不同类型的对象。
(2)如果没有双亲委派会有哪些问题
java虚拟机只会在不同的类的类名相同且加载该类的加载器均相同的情况下才会判定这是一个类。如果没有双亲委派机制,同一个类可能就会被多个类加载器加载,如此类就可能会被识别为两个不同的类,相互赋值时问题就会出现。
双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。
没有双亲委派模型,让所有类加载器自行加载的话,假如用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,系统就会出现多个不同的Object类,Java类型体系中基础行为就无法保证,应用程序就会变得一片混乱。
为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器加载一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
(3)双亲委派的例子:自定义一个类ArrayList
创建一个包,名为java.util,在这个包名下创建一个类ArrayList,写一段静态代码,然后在main方法中饮用ArrayList这个类,运行看看结果:
public class ClassLoaderDemo {
static {
System.out.println("ClassLoaderDemo is load!");
}
public static void main(String[] args){
Class clazz = ClassLoaderDemo.class;
ClassLoader loader = clazz.getClassLoader();
System.out.println("loader's Name : "+loader.toString());
Class listClass = ArrayList.class;
ClassLoader listLoader = listClass.getClassLoader();
System.out.println("listLoader's Name : "+listLoader.toString());
}
}
自己创建的ArrayList
package java.util;
public class ArrayList {
static {
System.out.println("ArrayList is load!");
}
}
可以看到并没有加载工程里自定义的ArrayList,最后加载的是JDK里的ArrayList,即使创建的是和JDK里一样的ArrayList,也不会被加载。因为到父加载器的时候就会判断出来已存在对象,然后就会返回。
(5)双亲委派的好处
(1)避免类的重复加载
采用双亲委派模式的好处就是,让Java类和它的类加载器一样具备有优先级的层次关系,因为当父加载器加载到该类的时候就会返回,就不会让子加载器再加载一遍,这种层级关系可以避免类的重复加载。
(2)保证java核心api不会被随意替换
考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派机制传递到启动类加载器,二启动类加载器在核心Java核心API发现这个名字的类,发现该类已经被加载了,就不会重新加载网络传递过来的java.lang.Integer的类,而直接返回已经加载过的Integer的类,这样就可以防止核心API库被随意的更改了。
(6)如何打破双亲委派机制?
(1)如何自定义一个类加载器
(1)当我们想要实现一个自定义类加载器加载一个类的时候,需要:继承ClassLoader,重写findClass
(2)如果不想自定义加载器打破双亲委派模型,那么只需要重写findClass。也就是说类加载还是先经过双亲委派,最后无法被父类加载器加载的类才会通过这个方法被加载。
(3)如果想自定义加载器打破双亲委派模型,那么就重写整个loadClass方法,设定自己的类加载逻辑,类加载也就不会经过双亲委派加载了,会直接通过这个方法加载。
(2)自定义的类加载器如何打破双亲委派机制
双亲委派机制原则在loadclass方法中,只需要绕开loadclass方法中即可。有两种方式:
(1)自定义类加载器 ,继承ClassLoader抽象类,重写loadclass方法,在这个方法可以自定义要加载的类使用的类加载器。。典型的打破双亲委派模型的框架和中间件有tomcat与osgi
(2)SPI机制绕开loadclass 方法。使用线程上下文加载器,可以通过java.lang.Thread类的setContextClassLoader()方法来设置当前类使用的类加载器类型。典型的有JDBC
(3)注意点
当然这里要注意一下,Object.class这是对象的顶级类,改变类的类加载器的时候要注意,如果全部改了,Object.class就找不到了,加载不了了。所以呢,这里重写的时候,要注意分类解决,把你想要通过自定义类加载器加载的和想通过默认类加载器加载的分隔开。
(7)什么情况下要打破双亲委派机制?
这个打破的意思,就是类加载器可以加载不属于当前作用范围的类。
(1)如果我们有一个类想要通过自定义的类加载器来加载这个类,而不是通过系统默认的类加载器,说白了就是不走双亲委派那一套,而是走自定义的类加载器
(2)由于加载范围的限制,顶层的ClassLoader无法访问底层ClassLoader所加载的类,此时需要破坏双亲委派模型。
因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由应用类加载器加载,这个时候就需要启动类加载器来委托子类应用类加载器来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。
为了解决这个问题,在JVM中引入了线程上下文类加载器,它可以把原本需要启动类加载器加载的类,由应用类加载器进行加载。
(8)打破双亲委派机制的案例?
(1)认识工具:线程上下文类加载器
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。
Java 应用运行的初始线程的上下文类加载器是应用类加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。
线程上下文类加载器从根本解决了一般应用不能违背 双亲委派模式 的问题,使得java类加载体系显得更灵活。上面所提到的问题正是线程上下文类加载器的拿手好菜。如果不做任何的设置,Java应用的线程上下文类加载器默认就是系统类加载器。因此,在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。
(2)JNDI、JDBC、JCE、JAXB 和 JBI 等
JNDI服务,它的代码是由启动类加载器BootstrapClassLoader去加载的,但是JNDI的目的就是对资源进行几种管理和查找,它需要调用独立厂商实现并部署在应用程序的classpath下的JNDI接口提供者(SPI)的代码,但是启动类加载器BootstrapClassLoader只能%JAVA_HOME%/lib 目录下的jar包和类,不能“认识”这些代码,该怎么办?
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器
这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的 SPI 代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。
Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
(3)JDBC打破双亲委派机制
(1)不使用java SPI时,以如下方式加载驱动实现类:
在Driver类中像DriverManager注册对应的驱动实现类。
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.
getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
(2)使用java SPI
JDBC4.0以后,开始支持使用SPI的方式来注册Driver,具体做法:在Mysql的jar包中的META-INF/services/java.sql.Driver文件中指明当前使用的Driver是哪个。
SPI:策略模式,根据配置决定运行时的接口实现类是哪个。当使用不同驱动时候,我们不需要手动通过Class.forName加载驱动类,只需要引入相应jar即可。
Connection conn = DriverManager.
getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");
驱动类何时加载?
(1)从META-INF/services/java.sql.Driver文件中获取具体的实现类“com.mysql.jdbc.Driver”
(2)通过Class.forName(“com.mysql.jdbc.Driver”)将这个类加载进来
(3)DriverManager在rt.jar包中,所以DriverManager是通过启动类加载器加载进来的。而Class.forName()加载的调用者是ClassLoader,所以如果用启动类加载器加载com.mysql.jdbc.Driver 肯定是加载不到的。
解决方法就是破坏双亲委派,让顶层类加载器加载底层类加载器。
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
// 省略部分代码
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 根据配置文件加载驱动实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// 省略部分代码
}
}
(4)Tomcat打破双亲委派机制
Tomcat容器,也存在破坏双亲委派的情况,来实现不同应用之间的资源隔离。
Tomcat中可以部署多个web项目,为了保证每个web项目互相独立,所以不能都由AppClassLoader加载,所以自定义了类加载器WebappClassLoader,WebappClassLoader继承自URLClassLoader,重写了findClass和loadClass,并且WebappClassLoader的父类加载器设置为AppClassLoader。
WebappClassLoader.loadClass中会先在缓存中查看类是否加载过,没有加载,就交给ExtClassLoader,ExtClassLoader再交给BootstrapClassLoader加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。
重写loadClass:
/**
* Load the class with the specified name, searching using the following
* algorithm until it finds and returns the class. If the class cannot
* be found, returns <code>ClassNotFoundException</code>.
* <ul>
* <li>Call <code>findLoadedClass(String)</code> to check if the
* class has already been loaded. If it has, the same
* <code>Class</code> object is returned.</li>
* <li>If the <code>delegate</code> property is set to <code>true</code>,
* call the <code>loadClass()</code> method of the parent class
* loader, if any.</li>
* <li>Call <code>findClass()</code> to find this class in our locally
* defined repositories.</li>
* <li>Call the <code>loadClass()</code> method of our parent
* class loader, if any.</li>
* </ul>
* If the class was found using the above steps, and the
* <code>resolve</code> flag is <code>true</code>, this method will then
* call <code>resolveClass(Class)</code> on the resulting Class object.
*
* @param name The binary name of the class to be loaded
* @param resolve If <code>true</code> then resolve the class
*
* @exception ClassNotFoundException if the class was not found
*/
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
// (0) Check our previously loaded local class cache 本地缓存
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);//链接classloader todo
return clazz;
}
// (0.1) Check our previously loaded class cache
clazz = findLoadedClass(name);//校验jvm 的appclassloader的缓存中是否存在
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();//系统的bootstrap classloader 的classloader
boolean tryLoadingFromJavaseLoader;
try {
// Use getResource as it won't trigger an expensive
// ClassNotFoundException if the resource is not available from
// the Java SE class loader. However (see
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
// details) when running under a security manager in rare cases
// this call may trigger a ClassCircularityError.
// See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
// details of how this may trigger a StackOverflowError
// Given these reported errors, catch Throwable to ensure any
// other edge cases are also caught
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// Swallow all exceptions apart from those that must be re-thrown
ExceptionUtils.handleThrowable(t);
// The getResource() trick won't work for this class. We have to
// try loading it directly and accept that we might get a
// ClassNotFoundException.
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
//利用javaser的加载方式 ,即双亲委派的模式,核心类库
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
//是否使用委托方式,即利用父类加载器加载该类的方式
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
//不使用父类加载器的方式,直接重写findclass,
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
protected void checkStateForClassLoading(String className) throws ClassNotFoundException {
// It is not permitted to load new classes once the web application has
// been stopped.
try {
checkStateForResourceLoading(className);
} catch (IllegalStateException ise) {
throw new ClassNotFoundException(ise.getMessage(), ise);
}
}
重写findClass:
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug(" findClass(" + name + ")");
checkStateForClassLoading(name);
// (1) Permission to define this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
if (log.isTraceEnabled())
log.trace(" securityManager.checkPackageDefinition");
securityManager.checkPackageDefinition(name.substring(0,i));
} catch (Exception se) {
if (log.isTraceEnabled())
log.trace(" -->Exception-->ClassNotFoundException", se);
throw new ClassNotFoundException(name, se);
}
}
}
// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class<?> clazz = null;
try {
if (log.isTraceEnabled())
log.trace(" findClassInternal(" + name + ")");
try {
if (securityManager != null) {
PrivilegedAction<Class<?>> dp =
new PrivilegedFindClassByName(name);
clazz = AccessController.doPrivileged(dp);
} else {
//从本地的具体类名查找 内部方式 ;//webapps lib
clazz = findClassInternal(name);
}
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
if ((clazz == null) && hasExternalRepositories) {
try {
//调用父类的加载方式
clazz = super.findClass(name);
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
if (clazz == null) {
if (log.isDebugEnabled())
log.debug(" --> Returning ClassNotFoundException");
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
if (log.isTraceEnabled())
log.trace(" --> Passing on ClassNotFoundException");
throw e;
}
// Return the class we have located
if (log.isTraceEnabled())
log.debug(" Returning class " + clazz);
if (log.isTraceEnabled()) {
ClassLoader cl;
if (Globals.IS_SECURITY_ENABLED){
cl = AccessController.doPrivileged(
new PrivilegedGetClassLoader(clazz));
} else {
//如果父类再加载不到的化,
cl = clazz.getClassLoader();
}
log.debug(" Loaded by " + cl.toString());
}
return clazz;
}
Web应用默认的类加载顺序是(打破了双亲委派规则):
(1)先从JVM的BootStrapClassLoader中加载。
(2)加载Web应用下/WEB-INF/classes中的类。
(3)加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。
(4)加载上面定义的System路径下面的类。
(5)加载上面定义的Common路径下面的类。
如果在配置文件中配置了``,那么就是遵循双亲委派规则,加载顺序如下:
(1)先从JVM的BootStrapClassLoader中加载。
(2)加载上面定义的System路径下面的类。
(3)加载上面定义的Common路径下面的类。
(4)加载Web应用下/WEB-INF/classes中的类。
(5)加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。
【5】双亲委派的补充说明
(1)通过代码来了解各个类加载器之间的关系
(1)第一步:获取当前类对象的加载器【AppClassLoader】
getClassLoader方法得到一个ClassLoader对象,那么运行一下看看这个ClassLoader对象的名字是什么
//使用反射获取类的对象
Class mainClass = TestClassLoader.class;
//类的对象调用方法获得该Class对象的类装载器
ClassLoader classLoader = mainClass.getClassLoader();
System.out.println("类加载器的名称:"+classLoader.toString());
输出结果为
类加载器的名称:sun.misc.Launcher$AppClassLoader@18b4aac2
类是AppClassLoader,路径是sun.misc.Launcher下的
(2)第二步:查看ClassLoader的源码
声明一个ClassLoader类的对象parent
并且提供了方法getParent,返回值是一个ClassLoader
@CallerSensitive
public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Check access to the parent class loader
// If the caller's class loader is same as this class loader,
// permission check is performed.
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
(3)第三步:打印一下parent对应的ClassLoader的名字,AppClassLoader的parent得到的是ExtClassLoader
ClassLoader parentLoader = classLoader.getParent();
System.out.println("类加载器classLoader的父加载器的名称:"+parentLoader.toString());
//类加载器的父加载器的名称:sun.misc.Launcher$ExtClassLoader@29453f44
输出结果
类加载器classLoader的父加载器的名称:sun.misc.Launcher$ExtClassLoader@29453f44
打印一下ExtClassLoader的加载路径
URL[] mUrlsExt = ((URLClassLoader)parentLoader).getURLs();
for (URL url:mUrlsExt) {
System.out.println(url);
//ExtClassLoader的加载路径加载的是jre/lib/ext/目录下的扩展包
}
可以看到输出结果,ExtClassLoader的加载路径加载的是jre/lib/ext/目录下的扩展包
(4)第四步:对parentLoader调用getParent方法打印它对父加载器,ExtClassLoader的parent得到的是BootstrapClassLoader(理论上是)
ClassLoader parentLoader2 = parentLoader.getParent();
System.out.println("类加载器parentLoader的父加载器的名称:"+parentLoader2);
//输出为null,因为BootstrapClassLoader是C++实现的,所以它没有对应的Java类
输出结果
类加载器parentLoader的父加载器的名称:null
可以看到这里打印出来的结果为空。因为BootstrapClassLoader是C++实现的,所以它没有对应的Java类。
采取一些特殊手段来获取它的加载路径,前面我们发现AppClassLoader和ExtClassLoader都是Launcher这个类的内部类,而且Launcher提供了一个方法getBootstrapClassPath来获取BootstrapClassLoader的加载路径。
try {
Class launcheClass = Class.forName("sun.misc.Launcher");
Method methodGetClassPath = launcheClass.getDeclaredMethod("getBootstrapClassPath",null);
if(null!=methodGetClassPath){
methodGetClassPath.setAccessible(true);
Object object = methodGetClassPath.invoke(null,null);
if(null!=object){
Method methodGetUrls = object.getClass().getDeclaredMethod("getURLs",null);
if(null!=methodGetUrls){
methodGetUrls.setAccessible(true);
URL[] mUrlBoot = (URL[]) methodGetUrls.invoke(object,null);
print(mUrlBoot);
//BootstrapClassLoader的加载路径是jre/lib目录,加载的是jre/lib/目录下的核心库
}
}
}
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
输出结果
可以看到BootstrapClassLoader的加载路径是jre/lib目录,加载的是jre/lib/目录下的核心库,和开头说的也是一致的。
(2)双亲委派模型介绍
每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。
每个类加载都有一个父类加载器,我们通过下面的程序来验证。
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
}
}
Output
ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586
The GrandParent of ClassLodarDemo's ClassLoader is null
AppClassLoader的父类加载器为ExtClassLoader ExtClassLoader的父类加载器为null,null并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader 。
其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 Mother ClassLoader 和一个 Father ClassLoader 。另外,类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定。官方API文档对这部分的描述如下:
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
(3)了解双亲委派机制的原理
(1)一个类加载器是如何加载一个类的
Class listClass = List.class;
ClassLoader listLoader = listClass.getClassLoader();
System.out.println("listLoader's Name : "+listLoader.toString());
List的classLoader为空,因为像List是属于jdk中的东西,而jdk其实是放在一个rt.jar包中,而这个包的路径是:jre/lib/目录下,通过上面说的,jre/lib/这个目录的jar包应该是由BootstrapClassLoader负责加载的,而这个BootstrapClassLoader类加载器是C++实现的,没有对应的Java类,所以打印出的结果为null。
(2)查看一下Java中ClassLoader这个类的源码是如何加载一个类的。
在ClassLoader源码中有一个loadClass方法,调用的是重载方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
我们需要重点关注红框部分的1-4这些代码,接下来捋一下逻辑:
1.第一步:走到代码1检查是否已经加载过这个类,如果加载过就直接返回,不走c==null逻辑里的代码,流程结束。
2.第二步:如果没加载过则进入到c == null的逻辑判断里,判断parent是否为空,如果不为空,就交由parent执行loadClass操作,否则执行findBootstrapClassOrNull方法。
这里我们以之前的例子来一步步看:
1)如果是AppClassLoader类加载器,执行loadClass方法时,parent不为空,parent是ExtClassLoader,现在由ExtClassLoader执行loadClass方法也就是代码2,继续走到里面后,判断parent是否为null,因为ExtClassLoader的parent==null,所以会走到代码3。
2)走到代码3后,执行findBootstrapClassOrNull方法,在该方法中调用findBootstrapClass方法,注意一个修饰符native,说明这个是native方法,因为BootstrapClassLoader是C++实现的,所以这里可以理解了。
3)如果前面代码1,2,3都执行完了,执行过程中出现了异常,这个时候c == null,会走到代码4中,执行findClass方法。findClass方法中什么都没做,只是抛出一个ClassNotFoundException异常。
(3)整个的loadClass流程已经执行完了,用一张图来总结一下流程
(4)双亲委派模型实现源码分析
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相关代码如下所示。
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
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;
}
}
【6】自定义加载器,打破双亲委派
(1)直接自定义类加载器加载
写一个自定义的类加载器TestClassLoader,继承ClassLoader,并重写了findClass和loadClass:
public class TestClassLoader extends ClassLoader {
public TestClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1、获取class文件二进制字节数组
byte[] data = null;
try {
System.out.println(name);
String namePath = name.replaceAll("\\.", "\\\\");
String classFile = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\" + namePath + ".class";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(new File(classFile));
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
data = baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 2、字节码加载到 JVM 的方法区,
// 并在 JVM 的堆区建立一个java.lang.Class对象的实例
// 用来封装 Java 类相关的数据和方法
return this.defineClass(name, data, 0, data.length);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException{
Class<?> clazz = null;
// 直接自己加载
clazz = this.findClass(name);
if (clazz != null) {
return clazz;
}
// 自己加载不了,再调用父类loadClass,保持双亲委托模式
return super.loadClass(name);
}
}
(2)测试
初始化自定义的类加载器,需要传入一个parent,指定其父类加载器,那就先指定为加载TestClassLoader的类加载器为TestClassLoader的父类加载器吧:
public static void main(String[] args) throws Exception {
// 初始化TestClassLoader,被将加载TestClassLoader类的类加载器设置为TestClassLoader的parent
TestClassLoader testClassLoader = new TestClassLoader(TestClassLoader.class.getClassLoader());
System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());
// 加载 Demo 类
Class clazz = testClassLoader.loadClass("study.stefan.classLoader.Demo");
System.out.println("Demo的类加载器:" + clazz.getClassLoader());
}
运行如下测试代码,发现报错了:
找不到java\lang\Object.class,我加载study.stefan.classLoader.Demo类和Object有什么关系呢?
转瞬想到java中所有的类都隐含继承了超类Object,加载study.stefan.classLoader.Demo,也会加载父类Object。Object和study.stefan.classLoader.Demo并不在同个目录,那就找到Object.class的目录(将jre/lib/rt.jar解压),修改TestClassLoader#findClass如下:
遇到前缀为java.的就去找官方的class文件。
运行测试代码:
还是报错了!!!
报错信息为:Prohibited package name: java.lang。
跟了下异常堆栈:
TestClassLoader#findClass最后一行代码调用了java.lang.ClassLoader#defineClass,
java.lang.ClassLoader#defineClass最终调用了如下代码:
看意思是java禁止用户用自定义的类加载器加载java.开头的官方类,也就是说只有启动类加载器BootstrapClassLoader才能加载java.开头的官方类。
得出结论,因为java中所有类都继承了Object,而加载自定义类study.stefan.classLoader.Demo,之后还会加载其父类,而最顶级的父类Object是java官方的类,只能由BootstrapClassLoader加载
(3)进一步修改(跳过AppClassLoader和ExtClassLoader)
既然如此,先将study.stefan.classLoader.Demo交由BootstrapClassLoader加载即可。
由于java中无法直接引用BootstrapClassLoader,所以在初始化TestClassLoader时,传入parent为null,也就是TestClassLoader的父类加载器设置为BootstrapClassLoader:
package com.stefan.DailyTest.classLoader;
public class Test {
public static void main(String[] args) throws Exception {
// 初始化TestClassLoader,并将加载TestClassLoader类的类加载器
// 设置为TestClassLoader的parent
TestClassLoader testClassLoader = new TestClassLoader(null);
System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());
// 加载 Demo
Class clazz = testClassLoader.loadClass("com.stefan.DailyTest.classLoader.Demo");
System.out.println("Demo的类加载器:" + clazz.getClassLoader());
}
}
双亲委派的逻辑在 loadClass,由于现在的类加载器的关系为TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中无需重写loadClass。
运行测试代码:
成功了,Demo类由自定义的类加载器TestClassLoader加载的,双亲委派模型被破坏了。
如果不破坏双亲委派,那么Demo类处于classpath下,就应该是AppClassLoader加载的,所以真正破坏的是AppClassLoader这一层的双亲委派。