JVM 的类加载器
Java 的类加载,就是把字节码格式“.class”文件加载到 JVM 的方法区,并在 JVM 的堆区建立一个
java.lang.Class
对象的实例,用来封装 Java 类相关的数据和方法。那 Class 对象又是什么呢?你可以把它理解成业务类的模板,JVM 根据这个模板来创建具体业务类对象实例。JVM 并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。
JVM 类加载是由类加载器来完成的,JDK 提供一个抽象类 ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。
从上面的代码我们可以得到几个关键信息:
- JVM 的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个 parent 字段,指向父加载器。
- defineClass 是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 通过 JNI 机制调用。
- findClass 方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用 defineClass 方法得到 Class 对象。
- loadClass 是个 public 方法,说明它才是对外提供服务的接口,具体实现也比较清晰:
首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。
请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java 类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索 Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。
JDK 中有哪些默认的类加载器?它们的本质区别是什么?为什么需要双亲委托机制?JDK 中有 3 个类加载器,另外你也可以自定义类加载器,它们的关系如下图所示。
- BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如
rt.jar
、resources.jar
等。
- ExtClassLoader 是扩展类加载器,用来加载
\jre\lib\ext
目录下 JAR 包。
- AppClassLoader 是系统类加载器,用来加载 classpath 下的类,应用程序默认用它来加载类。
- 自定义类加载器,用来加载自定义路径下的类。
这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。
双亲委托机制是为了保证一个 Java 类在 JVM 中是唯一的,假如你不小心写了一个与 JRE 核心类同名的类,比如 Object 类,双亲委托机制能保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。
这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。
这里请你注意,类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。
类加载器的分类
有两种类型的类加载器
- java虚拟机自带的加载器
- 根加载器(
BootStrap
) - 扩展加载器(Extension)
- 系统(应用)类加载器(System)
根加载器没有父加载器.它负责加载虚拟机的核心类库,如
Java.lang.*
等。根加载器从系统属性sun.boot.class.path
所指定的目录中加载类库根加载器的实现依赖于底层操作系统,属于虚拟机的实现一部分,并没有继承
java.lang.ClassLoader
类它的父加载器为根类加载器.它从
java.ext.dirs
系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext
子目录下加载类库.如果把用户创建的jar文件文件放在这个目录下,也会自动由扩展类加载器加载,扩展类加载器是纯java类,是
java.lang.ClassLoader
的子类父加载器为扩展类加载器,它是从环境变量
classpath
或者系统属性java.class.path
所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器是纯java类,**是
java.lang.ClassLoader
的子类- 用户自定义的类加载器
- 必须继承
java.lang.ClassLoader
- 用户可以自定义定制类的加载方式
案例
类加载器加载类的时机
JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,
如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(
LinkageError
错误)直接删除.class文件不行,会报
ClassNotFindException
如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
添加虚拟机参数追踪类加载过程
MyChild1没有主动使用,但是类加载器任然加载了它,因为
MyChild1.str
存在引用关系,编译器先去解析Mychild1,将符号引用解析为直接引用,会加载子类,
这也是阿里java开发规范中类变量必须用所在类名去引用的原因,减少了
jvm
不必要的开销类加载器的双亲委派模型
类加载器用来把类加载到java虚拟机中,从JDK1.2版本开始,类的加载过程采用双亲委托机制,
这种机制能更好地保证java平台的安全.在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器.
当Java程序请求加载器loader1加载
Samle
类时,loader1首先委托自己的父加载器去加载Sample类,如果父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载总结,自下向上委托请求,自上向下尝试加载
实际上是包含关系,而不是继承关系
案例1
类加载器的双亲委托模型的优点
- 可以确保java核心库的类型安全问题:所有的java应用都至少会引用
java.lang.Object
类,也就是在运行期,java.lang.Object
这个类会被加载到java虚拟机中;如果这个加载过程是由java应用自己的类加载器所完成的,那么很可能会在JVM中存在多个版本(多个命名空间)的java.lang.Object
类,而且这些类之间还是不兼容的,相互不可见的.借助于双亲委托机制,java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了java应用所使用的都是同一个版本的java核心类库,他们之间是互相兼容的
- 可以确保java核心类库所提供的类不会被自定义的类所替代
- 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用不同的类加载器来加载他们即可.不同类加载器所加载的类之间是不兼容的,这就相当于在java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用.
类加载器的获取方式
- 获得当前类的
ClassLoader : clazz.getClassLoader();
- 获取当前线程上下文的
ClassLoader:Thread.currentThread.getContexClassLoader()
- 获取系统的
ClassLoader: ClassLoader.getSystemClassLoader()
- 获得调用者的
ClassLoader: DriverManager.getCallerClassLoader()
案例2
不同类型元素的类加载器
- 基本类型无类加载器
- JDK自带的类(如String) 根类加载器
- 自定义类包括引入的外部jar包 系统类加载器
- 数组类型
- 数组的Class对象不是由类加载器创建的,而是java运行期根据需要创建的
Class.getClassLoader()
返回的数组类的类加载器与其元素类型的类加载器相同;
如果元素类型是原始类型,则数组类没有类加载器。
类加载器的命名空间
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
变式3 删除
claspath
下的ClassA.class
将loader1作为loader2的父加载器从这个变式中,loader1和loader2是同一个类的两个不同实例,但是loader1可以作为loader2的父类加载器,所以所谓的
parentClassLoader
并不是java中的继承关系,而是包含关系,通过构造方法传入父加载的一个引用变式4 rebuild项目,恢复
classpath
下的ClassA.class
变式5 将loader2作为loader3的父加载器
变式6 复杂类加载情况的运行分析
变式7
将
MySample
和MyCat
的字节码移动到桌面rebuild项目,删除
classpath
下的myCat
再次运行rebuild项目,
classpath
下删除MySample
类,再次运行修改
MyCat
类 在其中显示地引用MySample.class
还原
MyCat
类,修改MySample
类,,显示地引用MyCat
类变式8 类加载器的加载目录
更改定义类的加载路径
在jre下新建classes目录
再次执行程序,返回class loader结果为null
更改扩展类加载器默认加载路径为classes目录
扩展类加载器注意,要用扩展类加载器加载类,类必须要打成jar包
第一行更改扩展类加载器默认加载目录为classes目录,扩展类加载器会从classes目录下的test jar包中加载MyTest1
第二行更改默认加载路径为根目录,找不到test jar 会由系统类加载器加载
启动类加载器注意
- 启动类加载器是内嵌于vm中的c++代码,启动时加载,会加载
java.lang.ClassLoader
以及其他的java平台类,当jvm
启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器
- 启动类加载器并不是java类,而其他的加载器则都是Java类,启动类加载器是特定于平台的机器指令,它负责整个加载过程
- 所有类加载器(除了启动类加载器)都被实现为java类,不过,总归要有一个组件来加载第一个java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯java类加载器就是启动加载器的职责
- 启动类加载器还会负责提供JRE正常运行所需要的基本组件,包括
java.util
与java.lang
包中的类
更换默认的系统类加载器
获得系统类加载器的官方文档
在自定义类加载器MyTest16中添加一个单个参数为ClassLoader类型的构造方法
在命令行中替换
java.system.class.loader
的默认属性在控制台中运行(idea运行不行)
不同类加载器的命名空间关系
- 同一个命名空间内的类是相互可见的.子加载器的命名空间包含所有父加载器的命名空间.
因此由子加载器加载的类能看见父加载器加载的类,例如系统类加载器加载的类能看见根类加载器加载的类,由父加载器加载的类不能看见子加载器加载的类
- 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见
- 在运行期间,一个java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的,如果同样名字(即相同的完全限定名)的类是由两个不同的类加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此
类加载器命名空间与反射实例
删除classpath下的myPerson,将其复制到桌面
Launcher类源码分析(使用openJDK
)
自定义类加载器深入解析
系统类加载器默认从
classpath
下加载类,自定义类加载器将加载类的请求委托其父类加载器(默认为系统类加载器),类加载又将请求向上传递,然后从根类加载器自上而下尝试加载,最终系统类加载器从
classpath
下加载ClassA
变式1 将
ClassA.class
复制到windows路径C:\Users\wangj\Desktop\com\google\it\ClassA
,并保留classpath
下(即out文件夹下的ClaassA.class
)变式2 rebuild 恢复
classpath
下的ClassA.class
创建自定义类加载器loader2自定义类加载器的使用场景
- 隔离加载类。 在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。 比如, 阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。
- 修改类加载方式: 类的加载模型并非强制, 除Bootstrap外, 其他的加载并非一定要引入, 或者根据实际情况在某个时间点进行按需进行动态加载。
- 扩展加载源: 比如从数据库、 网络, 甚至是电视机机顶盒进行加载.
- 防止源码泄漏: Java代码容易被编译和篡改, 可以进行编译加密。 那么类加载器也需要自定义, 还原加密的字节码。
线程上下文类加载器
当前类加载器(Current Classloader)
- 每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类)
- 如果
ClassX
引用了ClassY
,那么ClassX
的类加载器就会去加载ClassY
(前提是ClassY
尚未被加载)
线程上下文类加载器(Context ClassLoader
)
- 线程上下文类加载器是从JDK1.2开始引入的,类Tread中的
getContextLoader()
与setContextClassLoader(ClassLoader cl)
分别用来获取和设置上下文类加载器
- 如果没有通过
setContextClassLoader(ClassLoader cl)
进行设置的话,线程将继承上下文类加载器.
- java应用运行时的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器类与资源
线程上下文加载器的重要性
SPI(Service Provider Interface)关系中存在的问题
JDBC驱动加载
Connection类在java系统路径是由根类加载器加载,右边是由厂商提供的驱动jar包在
classpath
下无法由根类加载器加载父
ClassLoader
可以使用当前线程Thread.currentThread().getContextLoader()
的classloader
加载的类,这就改变了父
ClassLoader
不能使用子ClassLoader
或者其他没有直接父子关系的ClassLoader
加载的类的情况,即改变了双亲委托模型线程上下文类加载器就是当前线程的
Current Classloader
.在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行类加载,但是对于SPI来说,有些接口是java核心类库提供的,而java核心库是由启动类加载器来加载的,
而这些接口的实现却来自于不同的jar包(厂商提供),java的启动类加载器是不会加载其他来源的jar包.
因此传统的双亲委派模型无法满足SPI的要求,而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现接口实现类的加载
打破传统类加载器模式
其他打破传统类加载器模型的案例:tomcat的类加载器, spring 类加载器
线程上下文类加载器一般使用模式
获取=>使用=>还原
类加载器案例
tomcat的类加载器
在Tomcat目录结构中,有四组目录(
/common/
,/server/
/shared/
和Webapp/WEB-INF/*
)可以存放java类库- 放置在
/common/
目录:类库可被tomcat和所有Web应用程序共同使用.
- 放置在
/server
目录中,类库可被Tomcat使用,对所有的Web应用程序都不可见
- 放置在
/shared
目录中:类库可被所有的web应用程序共同使用,但是对Tomcat自己不可见
- 放置在
Webapp/WEB-INF/*
目录:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见
为了实现这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现.
CommonCiassLoader 、 CatalinaClassLoader、 SharedClassLoader 和WebappClassLoader 则是 Tomcat 自己定义的类加载器, 它们分别加载
/common/*
、 /server/
、 /shared/
和 /WebApp/WEB-fNF /*
中 Java 类库的逻辑。如果有10个Web应用程序都是用Spring来进行组织和管理的话, 可以把Spring放到Shared目录下让这些程序共享。
Spring要对用户程序的类进行管理, 自然要 能访问到用户程序的类, 而用户的程序显然是放在 /WebApp/WEB-INF目录中的。 那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?