1、楔子
在读取Jar包中的资源文件时,一般会使用this.getClass().getResourceAsStream
方法,使用ClassLoader以流的形式来读取资源文件,那究竟什么是ClassLoader?本文就来一探究竟。
2、什么是ClassLoader?
ClassLoader是一个抽象类,我们用它的实例对象来装载类 (Java默认提供的三个ClassLoader),它负责将 Java 字节码(class文件)装载到 JVM 中 , 并使其成为 JVM 一部分。
但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存也会崩溃。
类装载就是寻找一个类或是一个接口的字节码文件并通过解析该字节码来构造代表这个类或是这个接口的 class 对象的过程 。
JVM 的类动态装载技术能够在运行时刻动态地加载或者替换系统的某些功能模块,而不影响系统其他功能模块的正常运行。
3、ClassLoader装载过程
在 Java 中,类装载器把一个类装入 Java 虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:
- 装载:查找和导入类或接口的字节码;
- 链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
2.1 校验:检查导入类或接口的二进制数据的正确性;
2.2 准备:给类的静态变量分配并初始化存储空间;
2.3 解析:将符号引用转成直接引用; - 初始化:激活类的静态变量的初始化 Java 代码和静态 Java 代码块。
4、Java默认提供的三个ClassLoader
4.1 java中的类大致分为三种:
1.系统类 2.扩展类 3.由程序员自定义的类
4.2 三个自带ClassLoader
所以Java系统自带有三个类加载器,用以分工读取,各自负责各自的区块。
Java中的类装载器实质上也是类,功能是把类载入jvm中,值得注意的是jvm的类装载器并不是一个,而是三个,层次结构如下:
这三个类装载器存在父子层级关系,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认情况下使用AppClassLoader装载应用程序的类
为什么要有三个类加载器,一方面是分工,各自负责各自的区块,另一方面为了实现委托模型,下面会谈到该模型.
4.2.1 BootStrap ClassLoader
称为启动类加载器
,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等
另外需要注意的是可以通过启动jvm时指定-Xbootclasspath
和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path
被指定的文件追加到默认的bootstrap路径中。
可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:
private static void getBootstrapClass()
{
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
}
输出为:
D:\Program Files\Java\jdk1.8.0_71\jre\lib\resources.jar;
D:\Program Files\Java\jdk1.8.0_71\jre\lib\rt.jar;
D:\Program Files\Java\jdk1.8.0_71\jre\lib\sunrsasign.jar;
D:\Program Files\Java\jdk1.8.0_71\jre\lib\jsse.jar;
D:\Program Files\Java\jdk1.8.0_71\jre\lib\jce.jar;
D:\Program Files\Java\jdk1.8.0_71\jre\lib\charsets.jar;
D:\Program Files\Java\jdk1.8.0_71\jre\lib\jfr.jar;
D:\Program Files\Java\jdk1.8.0_71\jre\classes
其实上述结果也是通过查找sun.boot.class.path
这个系统属性所得知的
//输出与上面的是一致的
System.out.println(System.getProperty("sun.boot.class.path"));
4.2.2 Extension ClassLoader
称为扩展类加载器
,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/
目下的所有jar。
还可以加载-D java.ext.dirs
选项指定的目录。
4.2.3 App ClassLoader
称为系统类加载器
,负责加载应用程序classpath目录
下的所有jar和class文件。
4.2.4 三者之间的关系
注意: 除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,它本身是虚拟机的一部分,所以它并不是一个JAVA类,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后(加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载),并构造Extension ClassLoader和App ClassLoader类加载器。
5、加载顺序
从下而上依次检查,从由上至下依次加载,加载顺序为:Bootstrap => Extension => App => 发起者
ClassLoader使用的是双亲委托模型
来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。
当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
上图用序列描述一下:
- 1.一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
- 2.递归,重复第1部的操作。
- 3.如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
- 4.Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
- 5.ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。
我说过上面用时序图画的让自己不满意,现在用框图,最原始的方法再画一次。
代码验证:
private static void getClassLoaderSort()
{
ClassLoader c = ClassLoaderTest.class.getClassLoader(); //获取ClassLoaderTest类的类加载器
System.out.println(c);
ClassLoader c1 = c.getParent(); //获取c这个类加载器的父类加载器
System.out.println(c1);
ClassLoader c2 = c1.getParent();//获取c1这个类加载器的父类加载器
System.out.println(c2);
}
输出结果:
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@5e481248
null
可以看出ClassLoaderTest是由AppClassLoader
加载器加载的,AppClassLoader的Parent 加载器是ExtClassLoader
,但是ExtClassLoader的Parent为 null 是怎么回事呵,朋友们留意的话,前面有提到Bootstrap Loader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,所以在java程序代码里试图打印出其内容时,我们就会看到输出为null
6、全盘负责委托机制
Java装载类使用“全盘负责委托机制”。
“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入;
“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全方面考虑的,试想如果一个人写了一个恶意的基础类(如java.lang.String)并加载到JVM将会引起严重的后果,但有了全盘负责制,java.lang.String永远是由根装载器来装载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
除了JVM默认的三个ClassLoder以外,第三方可以编写自己的类装载器,以实现一些特殊的需求。
类文件被装载解析后,在JVM中都有一个对应的java.lang.Class对象,提供了类结构信息的描述。数组,枚举及基本数据类型,甚至void都拥有对应的Class对象。Class类没有public的构造方法,Class对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构造的
7、类的装载方式
类装载方式,有两种:
- 1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
- 2.显式装载, 通过class.forname()等方法,显式加载需要的类
8、类加载的动态性
一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现
9、JVM搜索类时,如何判定两个class相同
9.1 判定规则
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。
就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。
9.2 实例验证
新建如下的测试类:
package com.sino.daily.code_2019_3_26;
/**
* Created on 2019/3/27 20:40.
*
* @author caogu
*/
public class NetClassLoaderSimple {
private NetClassLoaderSimple instance;
public void setNetClassLoaderSimple(Object obj) {
this.instance = (NetClassLoaderSimple) obj;
}
}
com.sino.daily.code_2019_3_26.NetClassLoaderSimple类的setNetClassLoaderSimple方法接收一个Object类型参数,并将它强制转换成com.sino.daily.code_2019_3_26.NetClassLoaderSimple类型。
把这个类编译为class文件放置到本地的一个位置,例如:
代码中无NetClassLoaderSimple类
然后编写一个自定义从本地/网络读取class的类加载器NetworkClassLoader:
package com.sino.daily.code_2019_3_26;
/**
* 加载网络class的ClassLoader
* Created on 2019/3/27 20:50.
* @author caogu
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;//this.findLoadedClass(name); // 父类已加载
//if (clazz == null) { //检查该类是否已被加载过
byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组
if (classData == null) {
throw new ClassNotFoundException();
}
clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例
//}
return clazz;
}
private byte[] getClassData(String name) {
InputStream is = null;
try {
String path = classNameToPath(name);
URL url = new URL(path);
byte[] buff = new byte[1024*4];
int len = -1;
is = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while((len = is.read(buff)) != -1) {
baos.write(buff,0,len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private String classNameToPath(String name) {
return rootUrl + "\\" + name.replace(".", "\\") + ".class";
}
}
最后编写测试两个类是否相同的代码如下:
package com.sino.daily.code_2019_3_26;
/**
* Created on 2019/3/27 20:41.
*
* @author caogu
*/
public class NetClassLoaderTest {
public static void main(String[] args) {
try {
//测试加载网络中的class文件
// String rootUrl = "file:///C:\\Users\\jieniyimiao\\Desktop\\java-daily-code\\out\\production\\classes";
String rootUrl = "file:///D:";
String className = "com.sino.daily.code_2019_3_26.NetClassLoaderSimple";
NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
Class<?> clazz1 = ncl1.loadClass(className);
Class<?> clazz2 = ncl2.loadClass(className);
Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();
clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
// System.out.println(clazz1.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
首先获得网络/本地上一个class文件的二进制名称,然后通过自定义的类加载器NetworkClassLoader创建两个实例,并根据网络地址分别加载这份class,并得到这两个ClassLoader实例加载后生成的Class实例clazz1和clazz2,最后将这两个Class实例分别生成具体的实例对象obj1和obj2,再通过反射调用clazz1中的setNetClassLoaderSimple方法。
结果如下:
结论:从结果中可以看出,虽然是同一份class字节码文件,但是由于被两个不同的ClassLoader实例所加载,所以JVM认为它们就是两个不同的类。
10.定义自已的ClassLoader
既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。
定义自已的类加载器分为两步:
1、继承java.lang.ClassLoader
2、重写父类的findClass方法
3、在findClass()方法中调用defineClass()
。
defineClass()
这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。
读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写findClass方法?
因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。
注意点:
一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。
在上小结9.2中我们已经自定义了一个类加载器NetworkClassLoader来实现本地或者网络class文件的加载。
我们编写如下的测试类:
package com.sino.daily.code_2019_3_26;
import java.lang.reflect.Method;
/**
* Created on 2019/3/27 22:56.
*
* @author caogu
*/
public class CustomizeClassLoaderTest {
public static void main(String[] args) {
try {
String rootUrl = "file:///D:";
String className = "com.sino.daily.code_2019_3_26.NetClassLoaderSimple";
NetworkClassLoader networkClassLoader = new NetworkClassLoader(rootUrl);
Class clazz = networkClassLoader.loadClass(className);
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
System.out.println(m.getName());
}
System.out.println(clazz.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果如下:
目前常用web服务器中都定义了自己的类加载器,用于加载web应用指定目录下的类库(jar或class),如:Weblogic、Jboss、tomcat等,下面我以Tomcat为例,展示该web容器都定义了哪些个类加载器:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
ClassLoader loader = this.getClass().getClassLoader();
while(loader != null) {
out.write(loader.getClass().getName()+"<br/>");
loader = loader.getParent();
}
out.write(String.valueOf(loader));
out.flush();
out.close();
}
11.自定义ClassLoader还能做什么?
突破了JDK系统内置加载路径的限制之后,我们就可以编写自定义ClassLoader,然后剩下的就叫给开发者你自己了。你可以按照自己的意愿进行业务的定制,将ClassLoader玩出花样来。
玩出花之Class解密类加载器
常见的用法是将Class文件按照某种加密手段进行加密,然后按照规则编写自定义的ClassLoader进行解密,这样我们就可以在程序中加载特定了类,并且这个类只能被我们自定义的加载器进行加载,提高了程序的安全性。
下面,我们编写代码。
1.定义加密解密协议
加密和解密的协议有很多种,具体怎么定看业务需要。在这里,为了便于演示,我简单地将加密解密定义为异或运算。当一个文件进行异或运算后,产生了加密文件,再进行一次异或后,就进行了解密。
2.编写加密工具类
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtils {
public static void test(String path){
File file = new File(path);
try {
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(path+"en");
int b = 0;
int b1 = 0;
try {
while((b = fis.read()) != -1){
//每一个byte异或一个数字2
fos.write(b ^ 2);
}
fos.close();
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
编写自定义classloader,DeClassLoader
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class DeClassLoader extends ClassLoader {
private String mLibPath;
public DeClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = getFileName(name);
File file = new File(mLibPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
byte b = 0;
try {
while ((len = is.read()) != -1) {
//将数据异或一个数字2进行解密
b = (byte) (len ^ 2);
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".classen";
}else{
return name.substring(index+1)+".classen";
}
}
}
测试
我们可以在ClassLoaderTest.java中的main方法中如下编码:
DeClassLoader diskLoader = new DeClassLoader("D:\\lib");
try {
//加载class文件
Class c = diskLoader.loadClass("com.frank.test.Test");
if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
12.部分源码分析
接下来我们看loadClass方法的实现方式:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
大致内容如下:
使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类:
调用findLoadedClass(String)方法检查这个类是否被加载过
使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机内置的加载器调用findClass(String)方法装载类,
如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。
ClassLoader的子类最好覆盖findClass(String)而不是这个方法。
除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)。
synchronized (getClassLoadingLock(name)) 看到这行代码,我们能知道的是,这是一个同步代码块,那么synchronized的括号中放的应该是一个对象。我们来看getClassLoadingLock(name)方法的作用是什么:
private final ConcurrentHashMap<String, Object> parallelLockMap;
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
以上是getClassLoadingLock(name)方法的实现细节,我们看到这里用到变量parallelLockMap ,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个putIfAbsent(className, newLock);方法来给刚刚创建好的对象赋值