理解Java的类加载过程

原文地址:https://blogs.oracle.com/sundararajan/understanding-java-class-loading

理解Java的类加载过程

你在考虑写类加载器吗?或者你正在面对不在预期之内的ClassCastException或者带有”loader constraint violation”的LinkageError。那么,是时候仔细观察一下了Java类加载过程。

什么是ClassLoader以及它是如何进行加载工作的

一个Java类是通过一个java.lang.ClassLoader实例进行加载的。java.lang.ClassLoader本身是一个抽象类,因此类加载器实例只能是java.lang.ClassLoader的一个子类。如果是这样的话,哪个加载器会加载java.lang.ClassLoader本身呢(经典的“谁将加载加载器”的问题)?事实证明,在JVM中有一个内置的引导类加载器。引导类加载器加载java.lang.ClassLoader和很多其他的Java平台类。

加载特定的Java类,比如说com.acme.Foo,JVM会调用java.lang.ClassLoaderloadClass方法(事实上,JVM会寻找loadClassInternal方法,如果找到这个方法就使用它,如果找不到则使用loadClass,同时loadClassInternal方法会调用loadClass方法)。loadClass方法接收类的名字然后返回名字所代表的类的java.lang.Class实例。事实上,loadClass方法找到class字节文件之后调用defineClass方法从字节数组当中定义出java.lang.Class。载有loadClass的加载器叫做初始加载器(例如,JVM初始加载的时候使用的那个加载器)。但是,初始类加载器并不直接找到类的字节文件,事实上,初始类加载器会代理其他类加载器(例如,父加载器)的加载过程,被代理的加载器本身有可能会代理其他类加载器等等。最终,这个类加载器的代理链通过调用defineClass方法最终加载目标类(com.acme.Foo)。最终加载的com.acme.Foo的加载器被称为定义类加载器。在运行时,Java类通过类的全量限定名称和加载它的定义类加载器两个指标进行唯一确定。如果相同名称的类被两个不同的加载器加载,那么他们是不同的,即使class字节码是从同一个位置加载而来的相同文件。

有多少类加载器 它们是从哪里加载的

即使在最古老简单的“hello world” Java程序当中,也存在至少3个类加载器。

  1. bootstrap loader(初始类加载器)
    1. 加载平台类(例如java.lang.Object,java.lang.Thread等待)
    2. 加载rt.jar($JRE_HOME/lib/rt.jar)当中的类
    3. 可以通过-Xbootclasspath设置设置加载类的位置
      1. Xbootclasspath/p: 和 -Xbootclasspath/a: 可以用来预加载或者追加初始加载的位置(必须谨慎的进行处理),在大多数场景下,要避免设置初始加载类的位置。
    4. 在Sun的实现当中,只读的sun.boot.class.path用来指定要初始加载的类的位置。注意,如果你修改了这个值,但是没有生效,那你也不能在程序运行时修改这个属性。
    5. 这个加载器在Java当中表现为null,例如,java.lang.Object.class.getClassLoader()返回的是null(同其他初始加载的类如java.lang.Integer,java.awt.Frame,java.sql.DriverManager等等)
  2. extension class loader(扩展类加载器)
    1. 从可选安装包当中加载类
    2. 加载$JRE_HOME/lib/ext目录下打类
    3. 可以通过设置-Djava.ext.dirs命令来修改java.ext.dirs属性,从而改变扩展加载目录
    4. 在Sun的实现当中,扩展类加载器就是sun.misc.Launcher$ExtClassLoader实例(事实上他是sun.misc.Launcher的内部类)
    5. 编程过程当中,可以通过读取java.ext.dirs系统属性获那些目录被用作扩展目录。注意,如果你修改了这个值,但是没有生效,那你也不能在程序运行时修改这个属性。
  3. application class loader(应用类加载器)
    1. 加载应用类目录下的类
    2. 应用类目录可以通过以下形式进行设置
      1. CLASSPATH环境变量
      2. Java启动选项-cp或者-classpath
      3. 如果CLASSPATH和-cp都没有设置,那么“.”(当前目录)会被使用
    3. 只读的系统属性java.class.path当中有应用类的路径。注意,如果你修改了这个值,但是没有生效,那你也不能在程序运行时修改这个属性。
    4. java.lang.ClassLoader.getSystemClassLoader() 返回这个加载器
    5. 这个加载器也被叫做“系统类加载器“,注意不要跟加载Java系统初始类加载器混淆。
    6. 就是这个加载器加载了你的Java应用程序当中“main”类(含有main方法的类)。在Sun的实现当中,应用类加载器就是sun.misc.Launcher$AppClassLoader实例(事实上他是sun.misc.Launcher的内部类)
    7. 默认应用类加载器使用扩展类加载器作为其父加载器。
    8. 你可以通过-Djava.system.class.loader修改应用类加载器。这个值指定了java.lang.ClassLoader子类的名称。默认应用程序加载器加载指定的类(该类必须在CLASSPATH或-CP中)并创建它的一个实例。新创建的加载器用于加载应用程序主类。

典型类加载的过程

加载我们正在运行一个叫做“hello world”的Java应用程序。我们分解一下类加载过程。JVM使用“application class loader”加载主类。如果你运行一下程序

class Main {
    public static void main(String[] args) {
        System.out.println(Main.class.getClassLoader());

        javax.swing.JFrame f = new javax.swing.JFrame();
        f.setVisible(true);

        SomeAppClass s = new SomeAppClass();
    }
}

它打印如下:

sun.misc.Launcher$AppClassLoader@17943a4
  1. 每当解析Main类中其他类的引用时,则JVM使用类的定义加载器(应用程序类加载器)作为初始加载器。在上面的例子当中,为了加载javax.swing.JFrame类,JVM使用应用类加载器作为初始类加载器,例如,JVM会调用应用类加载器当中的loadClass()(loadClassInternal)方法。
  2. 应用类加载器会代理给扩展类加载器。扩展类加载器会检查是否被加载的类是启动类加载器加载的类(使用ClassLoader.findBootstrapClass私有方法),启动类加载器通过加载rt.jar包定义这个类。
  3. 当解析SomeAppClass时,JVM会执行以下类似流程,
    1. 使用应用类加载器作为初始类加载器。
    2. 应用类加载器代理给扩展类加载器。
    3. 扩展类加载器使用启动类加载器检查这个类。
    4. 启动类加载找不到SomeAppClass
    5. 扩展类加载器检查SomeAppClass是否在扩展jar包当中,然而并没有发现
    6. 应用类加载器检查应用的CLASSPATH目录当中的.class文件。如果发现了,就会定义它。如果没偶遇发现就会抛出NoClassDefFoundError

总结

  1. 通过定义类加载器和全量限定名唯一确认一个类(Class)。
  2. 如果定义类加载器不同,那么类就是不同的,即使这个类是从相同的位置的相同的class文件当中加载的。
  3. 类加载器代理给其父加载器。
  4. 为了加载Bar当中的Foo,JVM使用Bar定义类加载器作为初始加载器,JVM会调用Bar定义类加载器当中的loadClass("Foo")
  5. JVM缓存会记录每次加载过程。JVM在后面的解析过程当中会使用缓存,就是说,loadClass并不会每次都会被调用。为了保证随着时间进行而不变,就是说,禁止类加载器加载不同字节码但是类名称相同的情况。可以通过缓存实现上面的禁止。编写良好的类加载器,必须通过调用ClassLoader.findLoadedClass()检查缓存。

你可能感兴趣的:(Java)