JVM 真香系列:轻松理解 class 文件到虚拟机(下)
关注“Java 后端技术全栈”
回复“000”获取大量电子书
类加载器
类加载器是很多人认为很硬的骨头。其实也没那么可怕,请听老田慢慢道来。
在装载(Load)阶段,通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载 Class 文件的。
上面我们自定义一个 String 出了问题,问题在于JVM
不知道我们想用哪个类,于是JVM
就定义了个规范。
把这种类装载器分成几类。
Bootstrap ClassLoader
负责加载$JAVA_HOME中 jre/lib/rt.jar
里所有的 class 或Xbootclassoath
选项指定的 jar 包。由 C++实现,不是ClassLoader
子类。
Extension ClassLoader
负责加载Java
平台中扩展功能的一些 jar 包,包括 $JAVA_HOME 中jre/lib/*.jar
或 -Djava.ext.dirs
指定目录下的 jar 包。
App ClassLoader
负责加载classpath
中指定的 jar 包及 Djava.class.path
所指定目录下的类和 jar 包。
Custom ClassLoader
通过java.lang.ClassLoader
的子类自定义加载 class,属于应用程序根据自身需要自定义的ClassLoader
,如tomcat
、jboss
都会根据j2ee
规范自行实现ClassLoader
。
图解类加载
加载原则
检查某个类是否已经加载:顺序是自底向上,从Custom ClassLoader
到BootStrap ClassLoader
逐层检查,只要某个Classloader
已加载,就视为已加载此类,保证此类只所有ClassLoader
加载一次。
加载的顺序:加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
ClassLoader 类分析
java.lang.ClassLoader
中很重要的三个方法:
loadClass
方法
findClass
方法
defineClass
方法
loadClass 方法
正如loadClass
方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()
方法去加载(关于findClass()
稍后会进一步介绍)。
从loadClass
实现也可以知道,如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className")
,这样就可以直接调用ClassLoader
的loadClass
方法获取到 class 对象。
findClass 方法
在JDK1.2
之前,在自定义类加载时,总会去继承ClassLoade
r 类并重写loadClass
方法,从而实现自定义的类加载类,但是在JDK1.2
之后已不再建议用户去覆盖loadClass
()方法,而是建议把自定义的类加载逻辑写在findClass()
方法中。
从前面的分析可知,findClass()
方法是在loadClass
()方法中被调用的,当loadClass
()方法中父加载器加载失败后,则会调用自己的findClass()
方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
需要注意的是,ClassLoader
类中并没有实现findClass()
方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException
异常。同时应该知道的是,findClass
方法通常是和defineClass
方法一起使用的。
defineClass 方法
defineClass()
方法是用来将 byte 字节流解析成JVM
能够识别的 Class 对象。
通过这个方法不仅能够通过 class 文件实例化 class 对象,也可以通过其他方式实例化 class 对象,如通过网络接收一个类的字节码,然后转换为 byte 字节流创建对应的 Class 对象 。
如何自定义类加载器
用户根据需求自己定义的。需要继承自ClassLoader
,重写方法findClass()
。
如果想要编写自己的类加载器,只需要两步:
继承
ClassLoader
类覆盖
findClass(String className)
方法
**ClassLoader**
超类的loadClass
方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用findClass
方法。
如果要实现该方法,必须做到以下几点:
1.为来自本地文件系统或者其他来源的类加载其字节码。
2.调用ClassLoader
超类的defineClass
方法,向虚拟机提供字节码。
浅谈双亲委派模型
这个在面试中也是频率相当高。
如果一个类加载器在接到加载类的请求时,先查找是否已经加载过,如果没有被加载过,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归。
如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
优势
Java 类随着加载它的类加载器一起,具备了一种带有优先级的层次关系。
比如,Java 中的 Object 类,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 在各种类加载环境中都是同一个类。
如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的 Object 类。
打破双亲委派模型的案例
tomcat
tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。简单看一下 tomcat 类加载器的层次结构。
对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。这个加载器用来隔绝不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。
如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。
那么 tomcat 是怎么打破双亲委派机制的呢?
可以看图中的 WebAppClassLoader,它加载自己目录下的 .class 文件,并不会传递给父类的加载器。但是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。
但是你自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。
OSGi
OSGi 曾经非常流行,Eclipse 就使用 OSGi 作为插件系统的基础。
OSGi 是服务平台的规范,旨在用于需要长运行时间、动态更新和对运行环境破坏最小的系统。
OSGi 规范定义了很多关于包生命周期,以及基础架构和绑定包的交互方式。这些规则,通过使用特殊 Java 类加载器来强制执行,比较霸道。
比如,在一般 Java 应用程序中,classpath 中的所有类都对所有其他类可见,这是毋庸置疑的。但是,OSGi 类加载器基于 OSGi 规范和每个绑定包的 manifest.mf 文件中指定的选项,来限制这些类的交互,这就让编程风格变得非常的怪异。但我们不难想象,这种与直觉相违背的加载方式,肯定是由专用的类加载器来实现的。
随着 jigsaw 的发展(旨在为 Java SE 平台设计、实现一个标准的模块系统),我个人认为,现在的 OSGi,意义已经不是很大了。
OSGi 是一个庞大的话题,你只需要知道,有这么一个复杂的东西,实现了模块化,每个模块可以独立安装、启动、停止、卸载,就可以了。
SPI
Java 中有一个 SPI 机制,全称是 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。
后面会再专门针对这个写一篇文章,这里就不细说了。
推荐阅读:
关注公众号“Java 后端技术全栈”
免费获取 500G 最新学习资料
版权声明: 本文为 InfoQ 作者【田维常】的原创文章。
原文链接:【http://xie.infoq.cn/article/e59033376e4a1fe07a2751bfe】。文章转载请联系作者。
评论