ClassLoader 工作机制

关于这方面的介绍,网上有很多。
在这里记录一篇,参考《深入分析JavaWeb》的第六章。

ClassLoader 类结构分析

1
public abstract class ClassLoader {

class loader 是一个负责加载 classes 的对象,ClassLoader 类是一个抽象类,需要给出类的二进制名称,class loader 尝试定位或者产生一个 class 的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。

以下是 ClassLoader 常用到的几个方法及其重载方法:

  • protected final Class<?> defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是java.lang.Class类的实例。
  • protected Class<?> findClass(String name) throws ClassNotFoundException查找名称为 name的类,返回的结果是java.lang.Class类的实例
  • public Class<?> loadClass(String name) throws ClassNotFoundException加载名称为 name的类,返回的结果是java.lang.Class类的实例

ClassLoader 等级加载机制

Java默认提供的三个ClassLoader

BootStrap ClassLoader

称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库

下面的代码显示了BootStrap最开始加载class类的路径

1
2
3
4
5
6
public static void main(String[] args) {
java.net.URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
}

结果:

1
2
3
4
5
6
7
8
file:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/resources.jar
file:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/rt.jar
file:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/sunrsasign.jar
file:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/jsse.jar
file:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/jce.jar
file:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar
file:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/jfr.jar
file:/usr/lib/jvm/java-8-openjdk-amd64/jre/classes

Extension ClassLoader

称为扩展类加载器,负责加载Java的扩展类库,Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。默认加载JAVA_HOME/jre/lib/ext/目下的所有jar

App ClassLoader

称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。

除了系统提供的类加载器以外,开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

它们之前的关系

BootStrap ClassLoader是一个特殊的加载器,可以说它不是一个纯java的类加载器。
它是由jvm的平台代码实现的类加载器。它来加载核心库,然后加载Extension ClassLoader,让Extension ClassLoader完成剩下的加载工作。Extension ClassLoaderSystem.getProperty("java.ext.dirs"); 中的类加载之后,又把剩下的加载工作交给App ClassLoader

在这里显示一段类加载器的父子关系

1
2
3
4
5
6
public static void main(String[] args) {
ClassLoader load = ClassLoader.getSystemClassLoader();
System.out.println(load);
System.out.println(load.getParent());
System.out.println(load.getParent().getParent());
}

结果为:

1
2
3
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null

除了引导类加载器之外,所有的类加载器都有一个父类加载器。 给出的 getParent()方法可以得到。对于
系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。

双亲委托模型

ClassLoader使用的是双亲委托模型来 搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个 类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出 ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实 例对象。

如何加载Class文件

JVM加载class文件的两种方法:

  • 隐式加载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
  • 显式加载, 通过class.forname()、this.getClass.getClassLoader().loadClass()等方法显式加载需要的类,或者我们自己实现的 ClassLoader 的 findlass() 方法。

类加载的动态性体现:
一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现

阶段

第一阶段找到 .class 文件并把这个文件包含的字节码加载到内存中。
第二阶段中分三步,字节码验证;class 类数据结构分析及相应的内存分配;最后的符号表的链接。
第三阶段是类中静态属性和初始化赋值,以及静态块的执行等。

验证与分析

  • 字节码验证,类装入器对于类的字节码要做很多检测,以确保格式正确,行为正确。
  • 类装备,准备代表每个类中定义的字段、方法和实现接口所必须的数据结构。
  • 解析,装入器装入类所引用的其他所有类

常见的加载类错误分析

ClassNotFoundException

ClassNotFoundExecption 异常是平常碰到的最多的。这个异常通常发生在显示加载类的时候。
通常是自己要的那个类不在classpath中。表现最多的就是jar包没有加进来。

NoClassDefFoundError

最多的情况就是把类别打错了,比如xx.xx.Main,只输入了Main。就出现这样的情况。

UnsatisfiedLinkError

通常加载本地库时,没有加载成功。

ClassCastException

该错误通常出现强制类型转换时出现这个错误。
JVM 在做类型转换时的规则:

  • 对于普通对象,对象必须是目标类的实例或目标类的子类的实例。如果目标类是接口,那么会把它当作实现了该接口的一个子类。
  • 对于数组类型,目标类必须是数组类型或 java.lang.Object、java.lang.Cloneable、java.io.Serializable。

如果不满足上面的规则,JVM 就会报错

ExceptionInInititalizerError

这个错误在JVM规范中是这样定义的:

  • 如果是因为出现Out-of-Memory-Error而无法创建新实例时,就抛出OutOfMemoryError对象做为代替。
  • 如果初始化器抛出了一些Exception,而且Exception不是Error或它的某个了子类,那就创建ExceptionInInititalizerError对象。

自定义ClassLoader

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢? 因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载 网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足 我们的需求了,所以需要定义自己的ClassLoader。

定义自已的类加载器分为两步:

* 继承java.lang.ClassLoader
* 重写父类的findClass方法

父类有那么多方法,为什么偏偏只重写findClass方法? 因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类 时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass 搜索类的算法。

从上面的分析可知,利用ClassLoader可以动态加载类,不仅是从本地,还可以从远程服务器上加载。

JVM如何判断两个class是否相同

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。否则,即使是同一个class文件,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同的类。

参考