在 JVM 综述里面,咱们说,JVM 作了三件事情,Java 程序的内存管理,
Java Class 二进制字节流的加载(ClassLoader),Java 程序的执行(执行引擎)。咱们也说,咱们大部分状况下只关注前2个。在前面的文章中,咱们已经分析了内存关系相关的,包括运行时数据区,GC 相关。今天咱们要讲的就是类加载器。java
在 JVM 综述 里,咱们已经大体分析了一些概念。而今天的文章将详细的阐述类加载器。程序员
首先,咱们要了解类加载器,固然,了解的目的是为了更好的开发,经过对类加载器的解读,看看咱们能不能作些什么,好比修改类加载器的加载逻辑,好比加入自定义的类加载器等等功能。框架
让咱们开始吧!ide
对于 Java 虚拟机来讲,Class 文件是一个重要的接口,不管使用何种语言进行软件开发,只要能将源文件编译为正确的 Class 文件,那么这种语言就能够在 Java 虚拟机上运行。能够说,Class 文件就是虚拟机的基石。this
如图所示:spa
从上图能够看出,虚拟机不拘泥于 Java 语言,任何一个源文件只要能编译成 Class 文件的格式,就能够在JVM 上运行!Class 文件格式就像是一个接口,只要遵照这个接口,就可以在 JVM 上运行。线程
Class 文件一般是以文件的方式存在(任何二进制流均可以是 Class 类型),但只有能被 JVM 加载后才能被使用,才能运行编译后的代码。系统装在 Class 类型能够分为加载,连接和初始化三个步骤。其中,连接也可分为验证,准备和解析3步骤。如图所示:设计
其中,只有加载过程是程序员可以控制的,后面的几个步骤都是有虚拟机自动运行的。所以,咱们的关注点主要放在加载阶段。code
上面说了,类加载器3个流程中,惟一能让程序员 “作手脚” 的就是加载过程,上面是加载过程呢?其主要做用就是从系统外部得到 Class 二进制数据流。orm
JVM 不会无端装载 Class 文件,只有在必要的时候才装载,哪几个时候呢?
以上6种状况属于主动调用,主动调用会触发初始化,还有一种状况是被动调用,则不会引发初始化。
Java 类加载器的具体实现就在 java.lang.ClassLoader,该类是一个抽象类,而且提供了一些重要的接口,用于自定义Class 的加载流程和加载方式。主要方法以下:
public Class<?> loadClass(String name) throws ClassNotFoundException
给定一个类名,加载一个雷,返回表明这个类的 Class 实例,若是找不到类,则返回异常。
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
根据给定的字节码流 b 定义一个类,off 表示位置,len 表示长度。该方法只有子类可使用。
protected Class<?> findClass(String name) throws ClassNotFoundException
查找一个类,也是只能子类使用,这是重载 ClassLoader 时,最重要的系统扩展点。这个方法会被 loadClass 调用,用于自定义查找类的逻辑,若是不须要修改类加载默认机制,只是想改变类加载的形式,就能够重载该方法。
protected final Class<?> findLoadedClass(String name)
一样的,这个方法也只有子类可以使用,他会去寻找已经加载的类,这个方法是 final 方法,没法被修改。
同时,在该类中,还有一个字段很是重要:parent,他也是一个 ClassLoader 的实例,这个字段所表示的 ClassLoader 也称为这个 ClassLoader 的双亲,在类加载的过程当中,ClassLoader 可能会将某些请求交给本身的双亲处理。
在标准的 Java 程序中,从虚拟机的角度讲,只有2种类加载器:
从程序员的角度讲,虚拟机会建立 3 中类加载器,分别是:Bootstrap ClassLoader(启动类加载器),Extension ClassLoader(扩展类加载器)和 APPClassLoader(应用类加载器,也称为系统类加载器)。此外,每个应用程序还能够拥有自定义的 ClassLoader,扩展 Java 虚拟机获取 Class 数据的能力。
而这 3 个类加载器有着层次关系。
先来看一个著名的图:
如图所示:从 ClassLoader 的层次自顶向下为启动类加载器,扩展类加载器,应用类加载器和自定义类加载器,当系统须要适用一个类时,在判断类是否已经被加载时,会先从当前底层类加载器进行判断,但系统须要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功。
注意,咱们没法访问启动类加载器,当试图获取启动类加载器的时候,返回 null,所以,若是返回的是 null,并不意味没有类加载器为它服务,而是指哪一个类为启动类加载器。
那么这些类加载路径是哪些呢?
BootStrap 类加载器负责加载
扩展类加载器有 sun.misc.Launcher$ExtClassLoader 实现,负责加载
应用类加载器由 sun.misc.Launcher$AppClassLoader 实现,因为这个类是 ClassLoader 中的 getSystemClassLoader 方法的返回值,也称为系统类加载器,负载加载用户类路径(ClassPath)上所指定的类库,开发者能够直接使用这个类加载器。通常状况下,这个就是程序中默认的类加载器。
自定义类加载器用于加载一些特殊途径的类,通常也是用户程序类。
系统中的 ClassLoader 在协同工做时,默认会使用双亲委托模式,即在类加载的时候,系统会判断当前类是否已经被加载,若是已经加载,则直接返回,不然就尝试加载,在尝试加载时,会先请求双亲处理,若是双亲查找事变,则本身加载。代码以下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded 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) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. 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; } }
代码中,若是双亲是 null,则使用启动类加载器加载,若是事变,则使用当前类加载器加载。
双亲为 null 通常有2种状况,1. 双亲是启动类加载器。2. 本身就是启动类加载器。
其中加载类的逻辑有2个注意的地方。
判断是否已经加载?当判断类是否须要加载时,是从底层开始判断,若是底层已经加载了,则再也不请求双亲。
当系统准备加载一个类时。会先从双亲加载,也就是最顶层的启动类加载器,逐层向下,直到找到该类。和上面的是相反的。
双亲模型当然有着优势,可以让整个系统保持了类的惟一性。但在有些场合,却不适合,也就是说,顶层的启动类加载器的代码没法访问到底层的类加载器。如 rt.jar 没法中代码没法访问到应用类加载器。
你确定要问,为何须要访问呢?
在 Java 平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,一般能够称为 Service Provider Interface,即 SPI。
在 rt.jar 中的抽象类须要加载继承他们的在应用层的子类实现,可是以目前的双亲机制是没法实现的。
所以 JDK 引用了一个不太优雅的设计,上下文类加载器。也就是讲类加载放在线程上下文变量中。经过 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 这两个方法获取和设置 ClassLoader,这样,rt.jar 中的代码就能够获取到底层的类加载了。
双亲模式是虚拟机的默认行为,但并不是必须这么作,经过重载 ClassLoader 能够修改该行为。事实上,不少框架和软件都修改了,好比 Tomcat,OSGI。具体实现则是经过重写 loadClass 方法,改变类的加载次序。好比先使用自定义类加载器加载,若是加载不到,则交给双亲加载。
咱们知道:由不一样的 ClassLoader 加载的同名类属于不一样的类型,不能相互转化和兼容。
而这个特性就是咱们实现热替换的关键。过程如图所示:
好了,到这里,基本的类加载器就介绍结束了。咱们总结了类加载的工做流程,包括加载,链接,初始化。而后咱们重点介绍了加载,由于加载阶段是咱们程序员惟一有所做为的地方。而后介绍了加载阶段的一些细节,好比双亲委派,而后说了双亲委派的缺点和补充,而后探讨了如何修改默认的类加载方式,最后经过类加载的特性实现了热替换。固然也看了核心类 ClassLoader 的源码。不过,这确定不是类加载器的所有。咱们将在后面的文章中将类加载的其余特性一一解开。
good luck!!!!