类加载器
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。
类加载器
jvm支持两种类型的加载器,分别是引导类加载器和 自定义加载器
- 引导类加载器是由c/c++实现的
- 自定义加载器是由java实现的。
jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
上图中的加载器划分为包含关系而并非继承关系
启动类加载器
这个类加载器使用c/c++实现,嵌套再jvm内部,用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。并不继承自java.lang.ClassLoader,没有父加载器
扩展类加载器
java语言编写,由sun.misc.Launcher$ExtClassLoader实现。从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载;派生于 ClassLoader。 父类加载器为启动类加载器
系统类加载器
java语言编写,由 sun.misc.Lanucher$AppClassLoader 实现。该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库;派生于 ClassLoader 。父类加载器为扩展类加载器 。通过ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。
1 | public class ClassLoaderTest { |
双亲委派模型
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载。
为什么需要双亲委派模型?
假设没有双亲委派模型,试想一个场景:
黑客自定义一个 java.lang.String 类,该 String 类具有系统的 String 类一样的功能,只是在某个函数稍作修改。比如 equals 函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到 JVM 中。此时,如果没有双亲委派模型,那么 JVM 就可能误以为黑客自定义的java.lang.String 类是系统的 String 类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的 java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的 java.lang.String 类,最终自定义的类加载器无法加载 java.lang.String 类。
或许你会想,我在自定义的类加载器里面强制加载自定义的 java.lang.String 类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在 JVM 中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
举个简单例子:
ClassLoader1 ClassLoader2 都加载 java.lang.String 类,对应Class1、Class2对象。那么 Class1对象不属于 ClassLoad2 对象加载的 java.lang.String 类型。
如何实现双亲委派模型?
双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实 ClassLoader 类默认的 loadClass 方法已经帮我们写好了,我们无需去写。
几个重要函数
loadClass 默认实现如下:
1 | public Class<?> loadClass(String name) throws ClassNotFoundException { |
再看看 loadClass(String name, boolean resolve) 函数:
1 | protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
从上面代码可以明显看出, loadClass(String, boolean) 函数即实现了双亲委派模型!整个大致过程如下:
- 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用 parent.loadClass(name, false); ).或者是调用 bootstrap 类加载器来加载。
- 如果父加载器及 bootstrap 类加载器都没有找到指定的类,那么调用当前类加载器的 findClass 方法来完成类加载。
换句话说,如果自定义类加载器,就必须重写 findClass 方法!
findClass 的默认实现如下:
1 | protected Class<?> findClass(String name) throws ClassNotFoundException { |
可以看出,抽象类 ClassLoader 的 findClass 函数默认是抛出异常的。而前面我们知道, loadClass 在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的 findeClass 函数,因此我们必须要在 loadClass 这个函数里面实现将一个指定类名称转换为 Class 对象.
如果是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为 Class 对象呢?很简单,Java 提供了 defineClass 方法,通过这个方法,就可以把一个字节数组转为Class对象
defineClass 主要的功能是:
将一个字节数组转为 Class 对象,这个字节数组是 class 文件读取后最终的字节数组。如,假设 class 文件是加密过的,则需要解密后作为形参传入 defineClass 函数。
defineClass 默认实现如下:
1 | protected final Class<?> defineClass(String name, byte[] b, int off, int len) |
自定义加类加载器
为什么要自定义类加载器
- 隔离加载类
模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器,内部自定义了好几中类加载器,用于隔离web应用服务器上的不同应用程序。
- 修改类加载方式
除了Bootstrap加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。扩展加载源比如还可以从数据库、网络、或其他终端上加载
- 防止源码泄漏
java代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。
自定义类加载器实现
实现方式:
所有用户自定义类加载器都应该继承ClassLoader类
在自定义ClassLoader的子类是,我们通常有两种做法:
- 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
- 重写findClass方法 (推荐)
首先,我们定义一个待加载的普通 Java 类: Test.java 。
1 | public class ClassLoaderTest { |
接下来就是自定义的类加载器:
1 | import java.io.*; |
最后运行结果如下:
1 | 我是由 class Main$MyClassLoader 加载进来的 |