Java源码解读篇之ClassLoader
我们知道任何程序都需要加载到内存中才能与CPU 进行交流。
字节码.class 文件同样需要加载到内存中,才可以实例化类。
而将.class 文件加载到内存中的就是ClassLoader.
ClassLoader 是一个类加载器,主要用于将*.class 信息加载到JVM 的不同区域中.
但是这都是我们道听途说对它的了解,实际上真实的它到底是怎么样的呢?
它是怎么查找和加载*.class 的呢?
今天我们就一起尝试阅读下这个ClassLoader 源码,来从源码层面认识下它.
1.1 什么是Class Loader?
- Java 世界中,万事万物皆为对象,一个类加载器(Class Loader)也是一个对象,它主要负责加载 classes.
- 类加载是一个将.class 字节码文件实例化成Class 对象并进行相关初始化的过程。
- 值得注意的是通过查看源码我们可以看到这个ClassLoader类其实是一个抽象类,而不是印象中的普通类.
public abstract class ClassLoader {
...
}
1.2 Java类加载之双亲委派模型
提起Classs Loader 的类加载机制就不得不说说它的“双亲委派模型“,当然叫“溯源委派加载模型”更加贴切。
1.2.1 什么是双亲委派模型?
那么什么是“双亲委派模型”呢?
一图胜千言
如上图所示,类加载器是存在严格的等级制度,一层一重天。
- 最高一层是BootStrap Class Loader
它是在JVM 启动时候创建的,负载装载核心的Java 类, 比如Object , System String 等,主要位于jre/lib/rt.jar
中。- 第二层是Platform ClassLoader
即平台类加载器,负责装载扩展系统类,比如XMl, 加密,压缩相关的功能类,主要位于jre/lib/ext/*.jar
- 第三层类是Application ClassLoader
即应用类加载器,主要加载用户自定义的ClassPath 路径下的类,主要位于ClassPath 中的jar.
1.2.2 为什么叫双亲委派模型?
- 距离探索的真相似乎又近了一步,那么为什么叫双亲委派模型呢?
这是因为如果我们的Application ClassLoader 如果想加载一个未知的类,它需要问下Platform ClassLoader和BootStrap ClassLoader的意见,他们两个父加载器都没有加载过这个类,并且告知可以加载它,我们的Application ClassLoader才可以加载它。
- 多学一招
BootStrap 加载的路径可以追加但是不建议修改或删除原有加载路径,添加如下启动参数可添加新的加载路径:
Xbootclasspath/a:/Users/administrator/myProject/src/main/java
如果想启动时观察加载了哪个jar 包中的哪个类,可以添加如下启动参数:
-XX:+TraceClassLoading
1.3 Class Loader 类加载器之如何加载字节码
1.3.1 Class Loader 如何加载本地字节码?
一个Class Loader 查找过程是究竟是怎么样的呢?
我们知道每个类都有自己的类加载器,但是如此同时,又和父类的类加载器存在那么一丝联系.
当一个类开始查找class 和resources 的时候,会委托为它的父类的类加载器去搜索和查找.
ClassLoader 有一个抽象方法如下:
Class<?> loadClass(String name)
其实Class Loader 类加载器不止可以加载本地的classs 还支持加载远程服务器上 的classes.
1.3.2 Class Loader 如何加载网络中的字节码?
如何加载一个远程的class 呢? 首先需要一个URL地址,其次是端口, 然后是资源名称路径.
然后我们需要将其下载到一个路径下,然后再去加载.
ClassLoader 提供相关的两个方法如下
URL getResource(String name)
上面方法的含义是:查找具有给定名称的资源,资源就是一些数据(图像,音频,文本等)可以由类代码以某种方式访问与代码的位置无关,路径分割符号为
/
URL findResource(String name)
name
:资源的名称 返回资源所在的URL地址对象- 查找具有给定名称的资源。 类加载器实现应该重写此方法以指定在何处查找资源
1.3.3 如何使用并行功能加载器加快加载速度?
上面我们很显然可以看出Class Loader 的加载需要一个过程,那么如果想提高它的加载速度.
那么使用并行功能类加载器似乎是一个不错的方法.
支持并发加载类的类加载器称为并行功能类加载器, 这种加载器类似我们之前学习的并发编程一样,如果加载的过程是一个漫长的阻塞过程,那么使用并行无疑可以提高一定的加载速度.
类加载器需要具有并行功能,否则类加载会导致死锁,因为在类加载过程loadClass
方法的持续时间内会保持加载器锁
也就是说一旦我们使用了并行功能类加载器加载class,那么就可能会发生共享资源的争抢问题,甚至有可能发生死锁问题.
计算机的虚拟世界是公平的世界, 给你一个优点的同时就会给你一个缺点.
多线程是把双刃剑,用好了可以提高效率,用不好就会引入新的问题.
- 给出一个类的二进制名称,类加载器(class Loader)会尝试查找或生成构成类定义的数据。
- 一种比较经典的做法是将类的名称转换成一个class 文件,然后从文件系统中读取这个class文件。
- 每一个对象都包含一个类加载的引用,比如顶级父类具有
Object.class.getClassLoader();
方法
1.4 自定义ClassLoader
那么你又没有好奇什么时候需要自定义类加载器,如果想自定义类加载器又该怎么做呢?
1.4.1 什么时候需要自定义类加载器?
当出现如下几种场景的时候可以考虑使用类加载器。
- 隔离加载类
有一个项目A, 需要引入某一个组件B, 组件B 传递依赖必须用组件C 的1.x 版本,而项目A必须用组件C 2.x版本。
- 修改类加载方式
- 扩展加载源
比如需要从数据库,网络,甚至电视机顶盒进行加载
- 防止源码泄漏
java 代码很容易被反编译,因此可以进行编译加密,通过自定义类加载器的方式可以实现这一目的。
1.4.2 如何自定义一个类加载器?
那么如果我们想自定义一个类加载器该怎么做呢?
自定义ClassLoader 的方法很简单,继承自抽象类ClassLoader 即可。
public class CustomizeClassLoader extends ClassLoader {
}
1.4.3 ClassLoader支持重写覆盖的方法有哪些?
接下来我们重点看看支持重写哪些方法
我这里汇总了一张表格,除了基础的构造方法之外支持重写的方法如下:
方法定义 | 参数解释 | 方法解释 |
---|---|---|
Class<?> loadClass(String name) | name指当前类二进制的名称 | 根据当前类的二进制名称加载类 |
Class<?> loadClass(String name, boolean resolve) | name指当前类的二进制名称,resolve 指是否解析该类,如果为true 则解析该类 | 告诉类加载器是否解析这个类 |
Object getClassLoadingLock(String className) | className指待加载类的名称,返回类加载操作的锁 | 回用于类加载操作的锁对象。 |
Class<?> findClass(String name) | name:类的二进制名称,返回类的对象 | 查找具有指定的二进制名称的类 |
URL getResource(String name) | name:资源的名称 | 查找具有给定名称的资源,资源就是一些数据(图像,音频,文本等)可以由类代码以某种方式访问与代码的位置无关,路径分割符号为/ |
Enumeration<URL> getResources(String name) |
name:资源的名称 | 查找具有给定名称的所有资源 |
URL findResource(String name) | name:资源的名称 返回资源所在的URL地址对象 | 查找具有给定名称的资源。 类加载器实现应该重写此方法以指定在何处查找资源 |
Enumeration<URL> findResources(String name) |
资源 的 java.net.URL的枚举URL对象 | |
InputStream getResourceAsStream(String name) | name:资源的名称 | 用于读取资源的输入流,如果找不到资源则为null |
Package definePackage(String name, String specTitle, String specVersion, String specVendor, String implTitle, String implVersion, String implVendor, URL sealBase) | name:包的名称; specTitle:规格名称;specVersion: 规格版本;specVendor 规格供应商 implTitle : 实现标题 implVersion:实现版本;implVendor:实现供应商;sealBase:如果不为null,则使用关于给定代码源java.net.URL URL对象。 否则,package将不被密封。 | 返回新定义的Package对象 |
Package getPackage(String name) | package:包的名称 | 返回指定包名下类加载定义的package |
Package[] getPackages() | 返回这个类加载器下的所有package数组 | |
String findLibrary(String libname) | libname:类库的名称,返回本地库的绝对路径 | |
void setDefaultAssertionStatus(boolean enabled) | enabled:true 表示如果此类加载器加载的类将此后默认启用断言,如果默认情况下将禁用断言则设置为false | |
void setPackageAssertionStatus(String packageName, boolean enabled) | packageName:要设置其软件包默认断言状态的软件包的名称,如果为null,则表示未命名的程序包是当前的,enabled:如果此类加载器和属于指定软件包或其任何子软件包的默认情况下启用断言,false表示默认情况下禁用断言 | |
void setClassAssertionStatus(String className, boolean enabled) | className:要设置其声明状态的顶级类的标准类名称。enabled:如果类初始化的时候这个类有断言,设置为true,否则设置为false 禁用断言 | 此为命名的顶级类设置所需的断言状态.如果命名的类不是顶级类,则此调用将对任何类的实际断言状态没有影响 |
void clearAssertionStatus() | 该方法将此类加载器的默认断言状态设置为false,并丢弃任何程序包默认值或类断言与类加载器关联的状态设置. 这个方法存在的意义是可以使类加载器忽略任何命令行 |
1.5 总结
汇总本篇博文所学内容如下:
1.6 参考资料
- JDK 源码
- 《码出高效 Java 开发手册》