深入理解Java类加载

本文翻译自:https://blogs.oracle.com/sundararajan/entry/understanding_java_class_loading

理解Java类加载

你正在思考写一个类加载器吗?还是你正在面对一个意想不到的ClassCastException 异常或者是一个带有奇怪信息“loader constraint violation”的链接错误。好吧,是时候仔细研究一下Java类加载的过程了。

加载器是什么和它是怎么加载的?

java.lang.ClassLoader 类的实例用来加载Java类。java.lang.ClassLoader 是抽象类,它的具体实现类只能有一个唯一的实例。如果是这种情况的话,那么哪个类加载器会去加载java.lang.ClassLoader它呢?(classic “who will load the loader” bootstrap issue).事实证明,有一个启动类加载器内置在JVM上。它去加载java.lang.ClassLoader和许多其它的Java基础类。

比方说加载一个指定的Java类com.acme.Foo,JVM调用java.lang.ClassLoader的loadClass方法(实际上是JVM寻找loadClassInternal方法–如果找到了就用这个方法,否则就用loadClass方法。实际上loadClassInternal方法的内部也是调用了loadClass方法)。loadClass方法接收一个类名和返回一个java.lang.Class的实例代表被加载的类。实际上,loadClass方法是去读取.class文件(或URL)的实际字节然后调用defineClass方法用字节数组构造java.lang.Class。带有这个loadClass方法的类加载器被叫做初始加载器(也就是说,JVM开始加载是用的这个加载器)。但是这个初始加载器不是直接去读取.class文件加载类的 - 而是可能委托另一个类加载器去加载类的(例如,它的父类加载器) - 它自己也可能委派给另一个加载器去加载。最终委托链中的某些类加载器对象调用defineClass方法去加载有关的类(com.acme.Foo)。这个特殊的加载器被称作com.acme.Foo的明确加载器。运行时,一个Java类通过类的全限定名和加载它的明确加载器确定其唯一性。如果命名相同的两个类(也就是说,相同的全限定名)通过两个不同的加载器被加载,那么这两个类是不相同的 - 即使.class中的字节码是相同的和相同的加载位置(URL)。

有多少种加载器和它们分别从哪里加载类?

即使一个简单的“Hello World”程序,也有至少三个类加载器。

  1. 启动类加载器

    • 加载基础类(例如:java.lang.Object, java.lang.Thread etc)
    • 加载rt.jar中的类($JRE_HOME/lib/rt.jar)
    • -Xbootclasspath参数被用作更改启动类路径。-Xbootclasspath/p: 和 -Xbootclasspath/a:用作前置/追加额外的启动类目录 - 要十分小心的去做这样的事情。在大多数的情况下,你应该避免乱动启动类路径。
    • 在Sun公司的实现中,只读系统属性sun.boot.class.path设置成指向启动类路径。你不能在运行时改变这个属性 - 如果你改变这个值也不会生效。
    • 在Java中的NULL代表这个加载器。例如,java.lang.Object.class.getClassLoader()的返回值是NULL(还有其它的类,例如:java.lang.Integer, java.awt.Frame, java.sql.DriverManager etc)。
  2. 扩展类加载器

    • 加载已安装的可选包中的类
    • 加载$JRE_HOME/lib/ext目录下jar文件中的类
    • 系统属性java.ext.dirs可以被设置改变扩展目录,用-Djava.ext.dirs命令行设置
    • 在Sun公司的实现中,它是sun.misc.Launcher$ExtClassLoader的实例(实际上它是sun.misc.Launcher的内部类)
    • 以编写代码的方式,你可以读取(只读!)系统属性java.ext.dirs去找到哪个目录被用作扩展目录。你不能在运行时改变这个属性 - 如果你改变这个值也不会生效。
  3. 应用程序类加载器

    • 加载应用类路径中的类

    • 设置应用类路径

      • 环境变量 CLASSPATH
      • Java启动程序中的-cp 或者 -classpath选项

      如果CLASSPATH和-cp都找不到,“.” (当前的目录)被用

    • 只读系统属性java.class.path的值是应用类路径。你不能在运行时改变这个属性 - 如果你改变这个值也不会生效。

    • java.lang.ClassLoader.getSystemClassLoader()返回值是这个加载器

    • 这个加载器也被叫做“系统类加载器” - 不要与加载Java系统类的启动加载器相混淆。

    • 这个加载器加载你Java应用的“main”类(带有main方法的类)。在Sun公司的实现中,它是sun.misc.Launcher$AppClassLoader的实例(实际上它是sun.misc.Launcher的内部类)

    • 缺省情况下应用加载器用扩展加载器作为它的父加载器。

    • 你可以改变应用类加载器通过命令行设置 -Djava.system.class.loader。这个值指定java.lang.ClassLoader的子类的名字。

    • 首先缺省的应用加载器加载已经命名的类(这个类在CLASSPATH或者-cp)和创建一个它的实例。新创建的这个类的实例加载器被用作加载应用“main”类。

典型的类加载流程

假定你正在运行"hello world"程序。我们将知道类加载过程。JVM用应用程序类加载器加载主函数所在的类。如果你运行下面这个程序:

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

每当一些其它的类引用在Main类中被解析时,JVM用Main类的明确加载器-应用程序类加载器-作为初始加载器。在上面这个例子中,加载javax.swing.JFrame这个类,JVM将用应用程序类加载器作为初始加载器。也就是说,JVM调用应用程序类加载器中的loadClass()(loadClassInternal())方法。应用程序类加载器接着分派给扩展类加载器。扩展类加载器检查它是否为启动类(用一个私有的方法 - ClassLoader.findBootstrapClass),启动类加载器是否从rt.jar加载过它。当SomeAppClass的引用被解析时,JVM有着相同的过程 - 用应用程序类加载器作为初始加载器。应用程序类加载器接着分派给扩展类加载器。扩展类加载器委派给启动类加载器。启动类加载器没有发现"SomeAppClass"。接着扩展类加载器检查"SomeAppClass"是否在扩展的类库中,也没有找到。接着应用类加载器检查在应用类路径下的.class字节。如果发现了,加载这个类。如果没有,将抛出NoClassDefFoundError错误。

总结

  • 类被唯一标识通过明确加载器和全限定名
  • 即使从文件系统中同一位置的相同的.class字节码加载的类,如果它们的类加载器是不同的,那么它们也是不同的。
  • 类加载器委托它的父类去加载
  • 加载“Bar”类中的“Foo”类引用,JVM使用Bar的明确加载器作为初始加载器。JVM将调用Bar的明确加载器中的loadClass(“Foo”) 方法。
  • JVM缓存 -> 运行时的类每次被初始加载都将被记录。JVM将用缓存对于其后的解析。换句话说,loadClass方法不能对于每一次引用都被调用。这将保证时间不变性 - 也就是换句话说,一个类加载器不允许加载相同类名但字节码不同的类。这是由于缓存的原因。一个写的好的类加载器必须通过调用ClassLoader.findLoadedClass()方法来检查缓存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值