深入理解 ClassLoader
ClassLoader源码初探
类加载器用于加载Java类到虚拟机中,其实现类java.lang.ClassLoader
是一个抽象类,其职责是通过指定类的完全限定名(binary name
),找到或生成这个类对应的字节码,这些字节码中包含类的定义数据,通过字节码就可以构造出一个java.lang.Class
对象。
每个Class对象都包含一个定义它的类加载器的引用,而数组的Class对象不是用类加载器创建的,而是在Java运行时根据需要自动创建的,如果调用数组的Class对象的getClassLoader()
方法返回的类加载器与其元素类型的类加载器相同,如果数组元素是基本数据类,则没有类加载,返回空。比如:
应用程序可以继承ClassLoader
类来扩展虚拟机动态加载类的方式。典型的策略是将类的binary name转化为文件名,然后从文件系统中读取对应的class文件,下面的代码简单的实现了这一策略。
运行上面的代码得知,想要加载的类并不是由自定义类加载器加载的,而是由AppClassLoader
加载的。为什么会这样?去loadClass()
方法看看:
查看loadClass()
源码可知,在加载类时,会首先把加载请求委托给自己的父加载器,父加载在委托给爷爷加载器,直到没有再没有父亲为止,这就是类的双亲委托机制。而我们写的自定义类加载器的父加载器是哪个呢?由于其继承自ClassLoader
类,看下ClassLoader
的无参构造方法:
默认是系统类加载器,即AppClassLoader
,与上面运行结果一致。
类加载器的双亲委托机制
类加载器分类
Java中的类加载器大致可以分为两类,一是系统提供的加载器,另外一类是自定义类加载器。其中,系统提供的类加载主要有以下三个:
启动类加载器(Bootstrap Class Loader):负责加载存放在JAVA_HOME/lib/目录,或被-Xbootclasspath参数所指定路径中存放的,且能够被JVM识别的类库。它由C++实现,非ClassLoader的子类。
扩展类加载器(Extension Class Loader):负责加载存放在JAVA_HOME/lib/ext/目录,或被java.ext.dirs系统变量所指定的路径中所有的类库。
系统类加载器(Application Class Loader):负责加载用户类路径(ClassPath)上所有的类库,由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
在JDK8以及之前的JDK版本中,可以通过以下代码分别打印3个类加载的加载路径。
除了启动类加载器之外,所有的类加载器都有一个父加载器。对于系统提供的类加载器来说,系统类加载器的父加载器是扩展类加载器,而扩展类加载器的父加载器是引导类加载器。对于开发者编写的自定义类加载器来说,其父加载器是加载此类的类加载器。因为自定义加载器的实现类如同其它的Java类一样,也是要由类加载器来加载的。一般来说,开发者自定义类加载器的父加载器是系统类加载器。
类加载器通过这种方式组织起来,形成树状结构,树的根结点就是启动类加载器,其大致示意图如下所示,箭头指向其父加载器:
类加载器的双亲委托机制
前面的实例中已经提到,类加载器在尝试加载某个类时,它首先不会自己去尝试加载这个类,而是先把加载请求委托给父加载器,由父加载器先尝试去加载这个类,以此类推,因此,所有的加载请求,最终都会被传送到最顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。其大致的流程示意图如下所示。
在回到前面的例子,自定义类加载器加载的Demo类在classpath目录下,因此,会被application class loader
加载,而如果我们把类放到非自定义目录下,结果又如何?修改前面的实例代码测试下:
请在以下两种情况下运行程序:
在运行实例代码前,先运行Demo类
将Demo.class移动到自己创建的目录中,比如在桌面创建文件夹/com/hicsc/classloader
先思考一下,再看运行结果:
在情景1中,classpath目录下已经有Demo.class文件,在加载Demo类时,会直接被系统类加载器加载;而情景2中,由于classpath中没有Demo类,最终会由自定义类加载器从指定路径中加载该类。
再来看另外一个有趣的例子,还是相同的类加载器代码,创建两个自定义类加载器,同时去加载同一个类,看看得到的Class对象是否相同:
还是在情景1和情景2中分别运行,正常情况下会得到如下结果:
在分析结果之前,再看一眼loadClass方法的源码:
如果一个类已经被加载过了,那么再次加载时,会直接返回已有的Class对象。在情景1中,不管是loader1还是loader2,都会先把加载请求委托给父加载器,得到的Class对象肯定是一致的。而在情景2中,loader1和loader2加载同一个类,却得到两个不同的Class对象,这又是为什么呢?
在运行期,任意一个Java类是由加载它的类加载和这个类本身一起共同确立其在JVM中的唯一性,每个类加载器,都拥有一个独立的类命名空间。简单来说,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个位置的同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。
这就是为什么两个不同的类加载器loader1和loader2去加载同一个class文件,却得到两个不同的Class对象的原因。
最后,说说类加载为什么要使用双亲委派机制?总结起来也就一句话:类加载器的双亲委托机制可以避免一个类被重复加载,也能够避免Java核心类被篡改,造成安全性问题。
JDK9之后的类加载器架构
JDK9中引入了Java模块化系统,JDK被重新组织成90多个模块,Java应用可以只引入自己所依赖的模块,而不必引入整个JDK,当然模块化最主要的目标还是为了实现可配置的封装隔离机制:
可配置:提供一种机制来显示的声明模块间的依赖关系,而应用程序可以通过这个依赖路径找到自己所需的所有模块
封装隔离:模块化机制要求模块中的包只有在显式的导出后才可以被其它模块使用,并且其它的模块必须显式地声明它需要这个模块中的包以后才能使用这些包。也就是说,提供方要声明哪些包可以被依赖,而使用方要声明需要哪些包。这种机制可以提高安全性,攻击者能够访问的类越少也就越安全。这也有助于我们思考如何组织代码才能获得更简洁、合理的设计。
模块导出的包:使用
exports
可以声明模块对其他模块所导出的包。包中的public
和protected
类型,以及这些类型的public
和protected
成员可以被其他模块所访问。没有声明为导出的包相当于模块中的私有成员,不能被其他模块使用。
模块的依赖关系:使用
requires
可以声明模块对其他模块的依赖关系。使用requires transitive
可以把一个模块依赖声明为传递的。传递的模块依赖可以被依赖当前模块的其他模块所读取。如果一个模块所导出的类型的结构中包含了来自它所依赖的模块的类型,那么对该模块的依赖应该声明为传递的。
JDK9为了在实现模块化的同时,还兼容以前的JDK版本,并没有从根本上改动三层类加载器架构以及双亲委派模型。但为了模块化系统的顺利实施,类加载器仍然发生了一些变动,主要有以下几个方面。
首先,扩展类加载器被平台类加载器(Platform Class Loader)取代。既然整个JDK都基于模块化进行构建,天然地满足了可扩展的需求,自然也就无需保留JAVA_HOME/lib/ext/
目录,此前使用的这个目录和java.ext.dirs
系统变量来扩展JDK功能的机制也就没有继续存在的价值了。类似地,新版本JDK中也取消了JAVA_HOME/jre
目录,因为随时可以组合构建出程序运行所需的JRE来,比如只想使用java.base
模块中的类型,可通过以下命令来构建出一个”JRE”:
其次,平台类加载器和系统类加载器均不再继承java.net.URLClassLoader
,如果有程序直接依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能会在JDK9以及更高版本的JDK中崩溃。现在启动类加载器、平台类加载器、系统类加载器全都继承于jdk.internal.loader.BuiltinClassLoader
,在BuiltinClassLoader中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。
最后,JDK9中虽然仍然维持着三层类加载器和双亲委派的架构,但类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。在JDK9以后的三层类加载器的委派关系如下图所示。
Java模块化系统了明确规定了三个类加载器各自负责加载的模块,比如启动类加载器负责加载java.base
、java.net
、java.prefs
等模块,而平台类加载器负责加载java.se
、java.sql
等模块。
更多关于JDK9模块化的内容可参考:深入理解Java虚拟机(第三版)第7章第5小节。
类的卸载
当一个类Sample被加载、连接和初始化后,它的生命周期就开始了,而当Sample类的Class对象不再被引用时,Class对象的生命周期也就结束了,其在方法区中的数据也会被卸载,从而结束Sample类的生命周期。
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。JVM自带的类加载器包括启动类加载器、扩展类加载器、系统类加载器,JVM会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此,这些Class对象始终是可达的,只有被用户自定义类加载所加载的类才可以被卸载。
来看一个简单的例子:
在运行时增加JVM参数来追踪类卸载信息,在JDK8中可以使用-XX:+TraceClassUnloading
,在较新版本的JDK中,这个参数已被废弃,使用参数-Xlog:class+unload=info
代替,程序运行后输出:
原因也很简单,类加载器、Class对象等都被新的对象替换,虚拟机就不再持有旧的类加载器的引用,同样地,旧的Class对象也就变成不可达对象,当JVM执行垃圾回收时,类自然而然的就会被卸载,方法区中的内存也会被回收。
深入理解JVM系列的第2篇,从目录阅读请移步:深入理解JVM系列文章目录
参考资料
版权声明: 本文为 InfoQ 作者【NORTH】的原创文章。
原文链接:【http://xie.infoq.cn/article/b1e7b584d48baa89379ab87e6】。未经作者许可,禁止转载。
评论