说一说JVM类加载子系统使用的各种类加载器
之前我们在《从JVM层面谈一谈类的加载过程》这篇文章中,详细的介绍了类加载的过程。我们说一个类,从class字节码二进制数据流转化为类模板加载到系统内存中,这一过程是由类加载子系统实现的,确切的说是由类加载子系统中的各种类加载器实现的。这篇文章就来谈一谈都有哪些类加载器以及它们的作用都是什么。
所有的类都是由ClassLoader进行加载的,Classloader负责通过各种方式将类信息的二进制数据流读入到JVM内部的方法区,形成一个类模板数据结构,进而转换为一个与该类对应的Class对象实例,然后交给JVM进行链接、初始化等操作。所以ClassLoader只参与了加载阶段,不会影响到类的链接和初始化,更不会决定这个类是否可以运行,而决定这个类是否可以运行是由JVM中的执行引擎决定的。
之前我们说过Java程序对类的使用分为主动使用和被动使用两种,那么与之对应的就可以把类加载分为显式加载和隐式加载。显式加载和隐式加载是指JVM加载class文件到内存的方式。
-
显式加载 指的是在代码中通过调用类加载器的方法加载Class对象。比如直接使用Class.forName(name) 或者loadClass() 方法加载Class对象 -
隐式加载 **不直接在代码中调用类加载器的方法来加载Class对象,而是通过JVM自动加载到内存中。**比如JVM启动时所必需的一些类,或者在加载某个类的Class文件时,该类的Class文件中又引用了另一个类的对象,此时额外引用的类将会通过JVM自动加载到内存中。
加载到内存中的类必须保证唯一性,否则当我们使用一个类的发现有多个目标类,该使用哪一个,这不就乱套了吗。那么什么是类的唯一性呢?对于一个类来说,都需要由加载它的类加载器和这个类本身一同决定该类在JVM内部的唯一性! **每一个类加载器都有一个独立的类名称空间,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义!**也就是说即使这两个类来自于同一个Class文件,但是被不同的类加载器加载到内存中,那么这两个类可以说是不相等的。这就好比不允许同一班级中有两个叫张三的同学,但是在不同班级之间,可以有多个叫张三的同学。
类加载器所实现的类加载机制有3个基本特征:
-
双亲委派模型 并不一定所有类加载都必须遵守这个双亲委派模型,JDK提供了类加载的扩展机制,可以在标准API框架上自定义实现加载机制。 -
可见性 子类加载器可以访问父类加载加载的所有类,但是反过来就不行了,这是因为有命名空间逻辑的存在,才实现由类加载器和类本身共同决定类的唯一性。 -
单一性 由于父类加载器加载过的类对于子加载器是可见的,所以子加载器不会重复加载父类加载器已经加载过的类
正是由于命名空间逻辑的存在,才保证了父类加载器和子类加载器之间可见性,进而保证一个类只加载一次的单一性,可见性和单一性也是实现双亲委派模型的基础。
类加载器的分类
从JVM规范给出的定义来看,将类加载器分为了两个大类:引导类加载器和自定义类加载器
这里自定义类加载器又分为了扩展类加载器和系统类加载器以及用户自定义类加载器,我们说提到了类加载器,它们之间都不是继承关系而是包含关系,所谓了“父类”和“子类”,实际上是子类加载器中包含着父类加载器的引用,可以通过引用来获取到父类加载器。之所以JVM规范把类加载器分为了两大类,是因为引导类加载器是由C/C++编写的,而自定义类加载器都是由Java语言编写的,虽然扩展类加载器和系统类加载器是JDK开发人员使用Java语言来编写的,所以也被称为自定义类加载器。可以认为引导类加载器是“根”,不能够改动,除了“根”之外,其它都是派生于抽象类加载器ClassLoader 的类加载器,称之为自定义类加载器。派生出来的自定义类又可以根据作用做进一步的细分,分为扩展类加载器和系统类加载器以及用户自定义的类加载器。
- 引导类加载器
- 引导类加载器是由C/C++编写的,嵌套在JVM内部
- 用来加载Java核心库,比如rt.jar、java/javax/sun开头的类,以及类路径下面的核心类,用来提供JVM自身启动所需要的类
- 引导类并不继承于
java.lang.ClassLoader 这个抽象类,所以引导类没有父加载器 - 加载扩展类加载器和系统类加载器,并指定为它们的父类加载器
- 扩展类加载器
- 扩展类加载器是由Java语言编写的,由
sun.misc.Launcher$ExtClassLoader 实现 - 派生于
ClassLoader 类 - 父类加载器为引导类加载器
- 从
java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会由扩展类加载器自动加载 - 无法通过扩展类加载器获得引导类加载器,因为引导类加载器由C/C++编写,获取返回的值是null
- 系统类加载器
- java语言编写, 由
sun.misc.Launcher$AppClassLoader 实现。 - 派生于
ClassLoader 类 - 父类加载器为引导类加载器
- 它负责加载环境变量
classpath 或系统属性 java.class.path 指定路径下的类库 - 该类加载器是程序中默认的类加载器(也叫作应用程序类加载器),一般来说,java应用的类都是由它来完成加载
- 通过
ClassLoader.getSystemClassLoader() 方法可以获取到该类加载器 - 用户自定义类加载器
- 一般在程序开发过程中,使用最多的就是上面的3种类加载器,当然在必要时,可以由用户自定义类加载器,来实现特殊的类加载机制。
- 用户自定义类加载器实现了类库的动态加载
- 通过用户自定义类加载器可以实现一些插件机制
- 通过用户自定义类加载器还能够实现应用隔离,不同的类加载器将隔离各自加载的不同组件模块
- 用户自定义类加载器也需要继承于
java.lang.ClassLoader 这个抽象类
我们说这几个不同的类加载器是包含的关系,那么它们之间到底是怎样的引用关系呢?我们下面举个例子测试一下
ClassLoader源码解析
我们说出来引导类加载器,扩展类加载器、系统类加载器、用户自定义类加载器都是继承于了ClassLoader这个抽象类的。接下来就来看一看这个抽象类的源码实现。
ClassLoader的主要方法
-
getParent()方法 返回该类加载器的父类加载器 public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
-
loadClass()方法 返回需要加载的目标类对应的Class实例对象,实现逻辑就是双亲委派模型。 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) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
-
findClass()方法 protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这个方法在loadClass()方法中被调用,当loadClass()方法中父类加载器加载失败之后,则会调用当前类加载器自己的findClass()方法来完成加载,这样就保证了遵循双亲委派模型。 需要注意的是,这个方法在ClassLoader类中没有实现具体代码逻辑,findClass()方法通常和defineClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载逻辑,取得要加载的类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。 -
defineClass()方法 protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象,findClass()方法通常和defineClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载逻辑,取得要加载的类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。 -
resolveClass()方法 调用这个方法可以使得类的Class对象创建完成的同时也被解析,我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置默认初始化,以及进行解析将字节码文件中的符号引用转换为直接引用。调用这个方法相当于将链接阶段的解析环节提前了。 -
findLoadClass()方法 根据name查找判断是否这个类已经被加载过了,相当于从缓存中返回Class对象。
ClassLoader抽象类的子结构
实际上扩展类加载器和系统类加载器并不是直接继承于这个ClassLoader抽象类的,而是直接继承于ClassLoader这个类的子类URLClassLoader
-
SecureClassLoader 这是个安全类加载器,它扩展了ClassLoader,增加了与安全和权限验证的几个方法 -
URLClassLoader 之前说过ClassLoader是一个抽象类,很多方法像是findClass()方法都没有具体的实现,而URLClassLoader这个实现类为这些方法提供了具体的实现,我们一般在自义定类加载器的时候,都是直接继承于URLClassLoader这个类,可以省去自己编写findClass()方法以及获取字节码流的方式。 -
APPClassLoader和ExtClassLoader 这两个类都是直接继承于URLClassLoader,我们可以看到ExtClassLoader并没有重写loadClass()方法,而APPClassLoader重写了 loadClass()方法,但是最终还是父类的loadClass()方法,所以它们之间还是遵循双亲委派模型的。
Class.forName()与ClassLoader.loadClass()
-
class.forName() 这是一个静态方法,可以根据闯入的类的全限定名返回一个Class对象。这个方法在将Class文件加载到内存的同时,会执行类的初始化。我们之前在讲类加载过程的初始化阶段的时候讲过,这是属于利用反射机制,主动使用一个类,会调用< clinit > 方法对这个类进行初始化 -
classLoader.loadClass() 这个方法是一个实例方法,需要一个ClassLoader对象来调用这个方法。这个方法在Class文件加载到内存的时候,并不会执行类初始化,直到这个类第一次使用时才进行初始化。我们之前在讲类加载过程的初始化阶段的时候讲过的,这个方法是被动使用一个类,不会调用< clinit > 方法对这个类进行初始化,但是是可以提前解析的。
双亲委派模型
我们一直在说类的加载遵循双亲委派模型,那这个双亲委派模型到底是什么意思?
如果一个类加载器需要加载一个类时,它首先不会自己尝试去加载这个类,而是将加载类的任务委派给它的父类加载器尝试去加载,依次向上委派给父类加载器,如果父类加载器可以加载这个类,就直接返回,只有当父类加载器无法加载这个类时,才会自己尝试去加载这个类。这就是双亲委派模型的定义!所以说双亲委派模型的本质就是规定了类加载的顺序,引导类加载器先加载,如果无法加载,就交给扩展类加载器去加载,如果扩展类加载器还无法加载,才会轮到系统类加载器或者自定义的类加载器去加载。之前我们讲的主要方法中的loadClass()方法在加载一个类时,就遵循了双亲委派模型。
遵循双亲委派模型必然有其优势:
- 避免了重复加载,确保一个类的全局唯一性。一个类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父类加载器已经加载过了这个类时,子类加载器也就不用再加载一次了。
- 保护程序安全,防止核心API被随意修改。重要的、最核心的一些类,交给JVM内部一层层的向上委派,不允许用户中途随意加载。
但是,有利就有弊,双亲委派模型也存在着一些弊端:**双亲委派的过程是单向的,这就导致上层的类加载器无法访问到底层的类加载器所加载过的类。**通常情况下,引导类加载器加载的类都是系统的核心类,包括一些重要的系统接口等等,而系统类加载器加载的都已程序需要用到的一些应用类。按照双亲委派模型,应用类可以访问到系统类,但是系统类无法访问到应用类。
需要注意的是,JVM**只是建议类的加载遵循双亲委派模型,但是没有规定必须遵循!**就比如Tomcat服务器的类加载过程,当类加载器加载一个类时,首先会自己尝试加载,如果自己加载失败,才会向上委托给父类加载器去加载,这正好了双亲委派模型相反。
既然双亲委派模型并不是一定要遵循的,就会出现破坏双亲委派机制的情况:
-
双亲委派机制是在JDK 1.2之后才引入的,但是类加载器和抽象类ClassLoader早就在之前出现了,所以在JDK 1.2之前,类的加载就是没有遵循双亲委派模型。 -
双亲委派的过程是单向的,这就导致上层的类加载器无法访问到底层的类加载器所加载过的类,这就使得一些核心系统类无法回调用类中的一些代码。为了解决这个弊端,引入了线程上下文类加载器。这个类加载器可以实现父类加载器访问子类加载器加载过的类,相当于打通了双亲委派模型的层次结构,来逆向的使用类加载器,有违反双亲委派模型的原则。但是到了JDK 6,提供了ServiceLoader这个类,采用配置信息+责任链的模式,这才算给实现了相对合理的双向访问。 -
为了实现程序动态性,一些代码热替换、模块热部署(OSGi)等技术由于自定义了类加载机制,导致破坏了双亲委派模型,类加载器不再是双亲委派模型那样的树状结构,而是变成了网状结构,为了更加灵活、更加动态性,相当于由纵向发展成了横向。
自定义类的加载器
最后我们再来说说自定义的类加载器。你可能会想难道JDK提供的类加载器不够用吗,为什么还需要自己定义类加载器?主要有以下几个原因:
-
隔离加载类 在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。 -
修改类加载的方式 并不一定按照双亲委派机制进行类的加载,可以按照实际需要,在某个时间点动态的加载类。 -
扩展加载源 我们之前说过,通过class字节码文件加载类的二进制数据信息只是一种方式,还可以通过数据库,网络传输等等来加载类。 -
防止源码泄漏 Java代码容易被编译和篡改,我们可以进行编译加密,那么类的加载也需要根据加密的实际情况,自定义类加载器进行解密。
既然可以自定义类加载器,那么我们如何去实现自定义的类加载器呢?
首先所有用户自定义的类加载器都应该直接或者间接继承ClassLoader这个抽象类,在自定义类加载器的时候,我们可以选择重写loadClass()方法,但是最好还是选择重写findClass()方法,这两种方式本质上差不多,因为loadClass()方法在执行过程中也会调用findClass()方法,loadClass()方法内部加载逻辑遵循了双亲委派模型,所以我们最好不要改动双亲委派模型,直接重写findClass()方法来自定义类的加载就好了,这样也就遵循了双亲委派模型。当编写好自定义类加载器后,在程序中调用loadClass()方法就可以按照自定义的方式加载所需的类了。有一点需要注意的是,用户自定义的类加载器的父类加载器是系统类加载器!
|