类加载机制是 Java 语言的一大亮点,使得 Java 类能够被动态加载到 Java 虚拟机中。java
此次咱们抛开术语和概念,从例子入手,由浅入深地讲解 Java 的类加载机制。git
本文涉及知识点:双亲委托机制、BootstrapClassLoader、ExtClassLoader、AppClassLoader、自定义网络类加载器等github
文章涉及代码:GitHub地址apache
个人更多文章:《Android 开发进阶》bash
Java 虚拟机通常使用 Java 类的流程为:首先将开发者编写的 Java 源代码(.java文件)编译成 Java 字节码(.class文件),而后类加载器会读取这个 .class 文件,并转换成 java.lang.Class 的实例。有了该 Class 实例后,Java 虚拟机能够利用 newInstance 之类的方法建立其真正对象了。服务器
ClassLoader 是 Java 提供的类加载器,绝大多数的类加载器都继承自 ClassLoader,它们被用来加载不一样来源的 Class 文件。markdown
上文提到了 ClassLoader 能够去加载多种来源的 Class,那么具体有哪些来源呢?网络
首先,最多见的是开发者在应用程序中编写的类,这些类位于项目目录下;app
而后,有 Java 内部自带的核心类
如 java.lang
、java.math
、java.io
等 package 内部的类,位于 $JAVA_HOME/jre/lib/
目录下,如 java.lang.String
类就是定义在 $JAVA_HOME/jre/lib/rt.jar
文件里;ide
另外,还有 Java 核心扩展类
,位于 $JAVA_HOME/jre/lib/ext
目录下。开发者也能够把本身编写的类打包成 jar 文件放入该目录下;
最后还有一种,是动态加载远程的 .class 文件。
既然有这么多种类的来源,那么在 Java 里,是由某一个具体的 ClassLoader 来统一加载呢?仍是由多个 ClassLoader 来协做加载呢?
实际上,针对上面四种来源的类,分别有不一样的加载器负责加载。
首先,咱们来看级别最高的 Java 核心类
,即$JAVA_HOME/jre/lib
里的核心 jar 文件。这些类是 Java 运行的基础类,由一个名为 BootstrapClassLoader
加载器负责加载,它也被称做 根加载器/引导加载器
。注意,BootstrapClassLoader
比较特殊,它不继承 ClassLoader
,而是由 JVM 内部实现;
而后,须要加载 Java 核心扩展类
,即 $JAVA_HOME/jre/lib/ext
目录下的 jar 文件。这些文件由 ExtensionClassLoader
负责加载,它也被称做 扩展类加载器
。固然,用户若是把本身开发的 jar 文件放在这个目录,也会被 ExtClassLoader
加载;
接下来是开发者在项目中编写的类,这些文件将由 AppClassLoader
加载器进行加载,它也被称做 系统类加载器 System ClassLoader
;
最后,若是想远程加载如(本地文件/网络下载)的方式,则必需要本身自定义一个 ClassLoader,复写其中的 findClass()
方法才能得以实现。
所以能看出,Java 里提供了至少四类 ClassLoader
来分别加载不一样来源的 Class。
那么,这几种 ClassLoader 是如何协做来加载一个类呢?
String 类是 Java 自带的最经常使用的一个类,如今的问题是,JVM 将以何种方式把 String class 加载进来呢?
咱们来猜测下。
首先,String 类属于 Java 核心类,位于 $JAVA_HOME/jre/lib
目录下。有的朋友会立刻反应过来,上文中提过了,该目录下的类会由 BootstrapClassLoader
进行加载。没错,它确实是由 BootstrapClassLoader
进行加载。但,这种回答的前提是你已经知道了 String 在 $JAVA_HOME/jre/lib
目录下。
那么,若是你并不知道 String 类究竟位于哪呢?或者我但愿你去加载一个 unknown
的类呢?
有的朋友这时会说,那很简单,只要去遍历一遍全部的类,看看这个 unknown
的类位于哪里,而后再用对应的加载器去加载。
是的,思路很正确。那应该如何去遍历呢?
好比,能够先遍历用户本身写的类,若是找到了就用 AppClassLoader
去加载;不然去遍历 Java 核心类目录,找到了就用 BootstrapClassLoader
去加载,不然就去遍历 Java 扩展类库,依次类推。
这种思路方向是正确的,不过存在一个漏洞。
假如开发者本身伪造了一个 java.lang.String
类,即在项目中建立一个包java.lang
,包内建立一个名为 String
的类,这彻底能够作到。那若是利用上面的遍历方法,是否是这个项目中用到的 String 不是都变成了这个伪造的 java.lang.String
类吗?如何解决这个问题呢?
解决方法很简单,当查找一个类时,优先遍历最高级别的 Java 核心类,而后再去遍历 Java 核心扩展类,最后再遍历用户自定义类,并且这个遍历过程是一旦找到就当即中止遍历。
在 Java 中,这种实现方式也称做 双亲委托
。其实很简单,把 BootstrapClassLoader
想象为核心高层领导人, ExtClassLoader
想象为中层干部, AppClassLoader
想象为普通公务员。每次须要加载一个类,先获取一个系统加载器 AppClassLoader
的实例(ClassLoader.getSystemClassLoader()),而后向上级层层请求,由最上级优先去加载,若是上级以为这些类不属于核心类,就能够下放到各子级负责人去自行加载。
以下图所示:
双亲委托
方式进行类加载吗?下面经过几个例子来验证上面的加载方式。
AppClassLoader
加载吗?在项目中建立一个名为 MusicPlayer
的类文件,内容以下:
package classloader; public class MusicPlayer { public void print() { System.out.printf("Hi I'm MusicPlayer"); } } 复制代码
而后来加载 MusicPlayer
。
private static void loadClass() throws ClassNotFoundException { Class<?> clazz = Class.forName("classloader.MusicPlayer"); ClassLoader classLoader = clazz.getClassLoader(); System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName()); } 复制代码
打印结果为:
ClassLoader is AppClassLoader
复制代码
能够验证,MusicPlayer
是由 AppClassLoader
进行的加载。
AppClassLoader
的双亲真的是 ExtClassLoader 和 BootstrapClassLoader 吗?这时发现 AppClassLoader
提供了一个 getParent()
的方法,来打印看看都是什么。
private static void printParent() throws ClassNotFoundException { Class<?> clazz = Class.forName("classloader.MusicPlayer"); ClassLoader classLoader = clazz.getClassLoader(); System.out.printf("currentClassLoader is %s\n", classLoader.getClass().getSimpleName()); while (classLoader.getParent() != null) { classLoader = classLoader.getParent(); System.out.printf("Parent is %s\n", classLoader.getClass().getSimpleName()); } } 复制代码
打印结果为:
currentClassLoader is AppClassLoader
Parent is ExtClassLoader
复制代码
首先能看到 ExtClassLoader
确实是 AppClassLoader
的双亲,不过却没有看到 BootstrapClassLoader
。事实上,上文就提过, BootstrapClassLoader
比较特殊,它是由 JVM 内部实现的,因此 ExtClassLoader.getParent() = null
。
$JAVA_HOME/jre/lib/ext
目录下会发生什么?上文中说了,ExtClassLoader
会加载$JAVA_HOME/jre/lib/ext
目录下全部的 jar 文件。那来尝试下直接把 MusicPlayer
这个类放到 $JAVA_HOME/jre/lib/ext
目录下吧。
利用下面命令能够把 MusicPlayer.java 编译打包成 jar 文件,并放置到对应目录。
javac classloader/MusicPlayer.java jar cvf MusicPlayer.jar classloader/MusicPlayer.class mv MusicPlayer.jar $JAVA_HOME/jre/lib/ext/ 复制代码
这时 MusicPlayer.jar 已经被放置与 $JAVA_HOME/jre/lib/ext
目录下,同时把以前的 MusicPlayer
删除
,并且这一次刻意
使用 AppClassLoader
来加载:
private static void loadClass() throws ClassNotFoundException { ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); // AppClassLoader Class<?> clazz = appClassLoader.loadClass("classloader.MusicPlayer"); ClassLoader classLoader = clazz.getClassLoader(); System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName()); } 复制代码
打印结果为:
ClassLoader is ExtClassLoader
复制代码
说明即便直接用 AppClassLoader
去加载,它仍然会被 ExtClassLoader
加载到。
双亲委托
加载机制上面已经经过一些例子了解了双亲委托
的一些特性了,下面来看一下它的实现代码,加深理解。
打开 ClassLoader
里的 loadClass()
方法,即是须要分析的源码了。这个方法里作了下面几件事:
代码以下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 检查是否曾加载过 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 优先让 parent 加载器去加载 c = parent.loadClass(name, false); } else { // 如无 parent,表示当前是 BootstrapClassLoader,调用 native 方法去 JVM 加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // 若是 parent 均没有加载到目标class,调用自身的 findClass() 方法去搜索 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; } } // BootstrapClassLoader 会调用 native 方法去 JVM 加载 private native Class<?> findBootstrapClass(String name); 复制代码
看完实现源码相信可以有更完整的理解。
前面提到了 Java 自带的加载器 BootstrapClassLoader
、AppClassLoader
和ExtClassLoader
,这些都是 Java 已经提供好的。
而真正有意思的,是 自定义类加载器
,它容许咱们在运行时
能够从本地磁盘或网络
上动态加载自定义类。这使得开发者能够动态修复某些有问题的类,热更新代码。
下面来实现一个网络类加载器
,这个加载器能够从网络上动态下载 .class 文件并加载到虚拟机中使用。
后面我还会写做与 热修复/动态更新
相关的文章,这里先学习 Java 层 NetworkClassLoader
相关的原理。
NetworkClassLoader
,它首先要继承 ClassLoader
;ClassLoader
内的 findClass()
方法。注意,不是loadClass()
方法,由于ClassLoader
提供了loadClass()
(如上面的源码),它会基于双亲委托
机制去搜索某个 class,直到搜索不到才会调用自身的findClass()
,若是直接复写loadClass()
,那还要实现双亲委托
机制;findClass()
方法里,要从网络上下载一个 .class 文件,而后转化成 Class 对象供虚拟机使用。具体实现代码以下:
/** * Load class from network */ public class NetworkClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = downloadClassData(name); // 从远程下载 if (classData == null) { super.findClass(name); // 未找到,抛异常 } else { return defineClass(name, classData, 0, classData.length); // convert class byte data to Class<?> object } return null; } private byte[] downloadClassData(String name) { // 从 localhost 下载 .class 文件 String path = "http://localhost" + File.separatorChar + "java" + File.separatorChar + name.replace('.', File.separatorChar) + ".class"; try { URL url = new URL(path); InputStream ins = url.openStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); // 把下载的二进制数据存入 ByteArrayOutputStream } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } return null; } public String getName() { System.out.printf("Real NetworkClassLoader\n"); return "networkClassLoader"; } } 复制代码
这个类的做用是从网络上(这里是本人的 local apache 服务器 http://localhost/java 上)目录里去下载对应的 .class 文件,并转换成 Class<?> 返回回去使用。
下面咱们来利用这个 NetworkClassLoader
去加载 localhost 上的 MusicPlayer
类:
MusicPlayer.class
放置于 /Library/WebServer/Documents/java
(MacOS)目录下,因为 MacOS 自带 apache 服务器,这里是服务器的默认目录;String className = "classloader.NetworkClass"; NetworkClassLoader networkClassLoader = new NetworkClassLoader(); Class<?> clazz = networkClassLoader.loadClass(className); 复制代码
http://localhost/java/classloader/MusicPlayer.class
成功。能够看出 NetworkClassLoader
能够正常工做,若是读者要用的话,只要稍微修改 url 的拼接方式便可自行使用。
类加载方式是 Java 上很是创新的一项技术,给将来的热修复技术提供了可能。本文力求经过简单的语言和合适的例子来说解其中双亲委托机制
、自定义加载器
等,并开发了自定义的NetworkClassLoader
。
固然,类加载是颇有意思的技术,很难覆盖全部知识点,好比不一样类加载器加载同一个类,获得的实例却不是同一个等等。
谢谢。
wingjay