ClassLoader(二)- 加載過程

本文源代碼在Githubhtml

本文僅爲我的筆記,不該做爲權威參考。java

原文git

在前一篇文章初步瞭解ClassLoader裏提到了委託模型(又稱雙親委派模型),解釋了ClassLoader hierarchy(層級)處理類加載的過程。那麼class文件是如何變成Class對象的呢?github

Class的加載過程

Class加載分爲這幾步:bootstrap

  1. 建立和加載(Creation and Loading)
  2. 連接(Linking)數組

    1. 驗證(Verification)
    2. 準備(Preparation)
    3. 解析(Resolution),此步驟可選
  3. 初始化(Initialization)

注: 前面說了數組類是虛擬機直接建立的,以上過程不適用於數組類。安全

建立和加載(Creation and Loading)

什麼時候會觸發一個類的加載?oracle

Java Language Specification - 12.1.1. Load the Class Testapp

The initial attempt to execute the method main of class Test discovers that the class Test is not loaded - that is, that the Java Virtual Machine does not currently contain a binary representation for this class. The Java Virtual Machine then uses a class loader to attempt to find such a binary representation.

也就是說,當要用到一個類,JVM發現尚未包含這個類的二進制形式(字節)時,就會使用ClassLoader嘗試查找這個類的二進制形式。jvm

咱們知道ClassLoader委託模型,也就是說實際觸發加載的ClassLoader和真正加載的ClassLoader可能不是同一個,JVM將它們稱之爲initiating loaderdefining loaderJava Virtual Machine Specification - 5.3. Creation and Loading):

A class loader L may create C by defining it directly or by delegating to another class loader. If L creates C directly, we say that L defines C or, equivalently, that L is the defining loader of C.

When one class loader delegates to another class loader, the loader that initiates the loading is not necessarily the same loader that completes the loading and defines the class. If L creates C, either by defining it directly or by delegation, we say that L initiates loading of C or, equivalently, that L is an initiating loader of C.

那麼當A類使用B類的時候,B類使用的是哪一個ClassLoader呢?

Java Virtual Machine Specification - 5.3. Creation and Loading

The Java Virtual Machine uses one of three procedures to create class or interface C denoted by N:

  • If N denotes a nonarray class or an interface, one of the two following methods is used to load and thereby create C:

    • If D was defined by the bootstrap class loader, then the bootstrap class loader initiates loading of C (§5.3.1).
    • If D was defined by a user-defined class loader, then that same user-defined class loader initiates loading of C (§5.3.2).
  • Otherwise N denotes an array class. An array class is created directly by the Java Virtual Machine (§5.3.3), not by a class loader. However, the defining class loader of D is used in the process of creating array class C.

注:上文的C和D都是類,N則是C的名字。

也就說若是D用到C,且C尚未被加載,且C不是數組,那麼:

  1. 若是D的defining loader是bootstrap class loader,那麼C的initiating loader就是bootstrap class loader。
  2. 若是D的defining loader是自定義的class loader X,那麼C的initiating loader就是X。

再總結一下就是:若是D用到C,且C尚未被加載,且C不是數組,那麼C的initiating loader就是D的defining loader。

用下面的代碼觀察一下:

// 把這個項目打包而後放到/tmp目錄下
public class CreationAndLoading {
  public static void main(String[] args) throws Exception {
    // ucl1的parent是bootstrap class loader
    URLClassLoader ucl1 = new NamedURLClassLoader("user-defined 1", new URL[] { new URL("file:///tmp/classloader.jar") }, null);
    // ucl1是ucl2的parent
    URLClassLoader ucl2 = new NamedURLClassLoader("user-defined 2", new URL[0], ucl1);
    Class<?> fooClass2 = ucl2.loadClass("me.chanjar.javarelearn.classloader.Foo");
    fooClass2.newInstance();
  }
}

public class Foo {
  public Foo() {
    System.out.println("Foo's classLoader: " + Foo.class.getClassLoader());
    System.out.println("Bar's classLoader: " + Bar.class.getClassLoader());
  }
}

public class NamedURLClassLoader extends URLClassLoader {
  private String name;
  public NamedURLClassLoader(String name, URL[] urls, ClassLoader parent) {
    super(urls, parent);
    this.name = name;
  }
  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    System.out.println("ClassLoader: " + this.name + " findClass(" + name + ")");
    return super.findClass(name);
  }
  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    System.out.println("ClassLoader: " + this.name + " loadClass(" + name + ")");
    return super.loadClass(name);
  }
  @Override
  public String toString() {
    return name;
  }
}

運行結果是:

ClassLoader: user-defined 2 loadClass(me.chanjar.javarelearn.classloader.Foo)
ClassLoader: user-defined 1 findClass(me.chanjar.javarelearn.classloader.Foo)
ClassLoader: user-defined 1 loadClass(java.lang.Object)
ClassLoader: user-defined 1 loadClass(java.lang.System)
ClassLoader: user-defined 1 loadClass(java.lang.StringBuilder)
ClassLoader: user-defined 1 loadClass(java.lang.Class)
ClassLoader: user-defined 1 loadClass(java.io.PrintStream)
Foo's classLoader: user-defined 1
ClassLoader: user-defined 1 loadClass(me.chanjar.javarelearn.classloader.Bar)
ClassLoader: user-defined 1 findClass(me.chanjar.javarelearn.classloader.Bar)
Bar's classLoader: user-defined 1

能夠注意到Foo的initiating loader是user-defined 2,可是defining loader是user-defined 1。而Bar的initiating loader與defining loader則直接是user-defined 1,繞過了user-defined 2。觀察結果符合預期。

連接

驗證(Verification)

驗證類的二進制形式在結構上是否正確。

準備(Preparation)

爲類建立靜態字段,而且爲這些靜態字段初始化默認值。

解析(Resolution)

JVM在運行時會爲每一個類維護一個run-time constant pool,run-time constant pool構建自類的二進制形式裏的constant_pool表。run-time constant pool裏的全部引用一開始都是符號引用(symbolic reference)(見Java Virutal Machine Specification - 5.1. The Run-Time Constant Pool)。符號引用就是並不是真正引用(即引用內存地址),只是指向了一個名字而已(就是字符串)。解析階段作的事情就是將符號引用轉變成實際引用)。

Java Virutal Machine Specification - 5.4. Linking

This specification allows an implementation flexibility as to when linking activities (and, because of recursion, loading) take place, provided that all of the following properties are maintained:

  1. A class or interface is completely loaded before it is linked.
  2. A class or interface is completely verified and prepared before it is initialized.

也就是說僅要求:

  1. 一個類在被連接以前得是徹底加載的。
  2. 一個類在被初始化以前得是被徹底驗證和準備的。

因此對於解析的時機JVM Spec沒有做出太多規定,只說瞭如下JVM指令在執行以前須要解析符號引用:_anewarray_, checkcast_, _getfield_, _getstatic_, _instanceof_, _invokedynamic_, _invokeinterface_, _invokespecial_, _invokestatic_, _invokevirtual_, _ldc_, _ldc_w_, _multianewarray_, _new_, _putfieldputstatic

看不懂不要緊,大體意思就是,用到字段、用到方法、用到靜態方法、new類等時候須要解析符號引用。

初始化

若是直接賦值的靜態字段被 final 所修飾,而且它的類型是基本類型或字符串時,那麼該字段便會被 Java 編譯器標記成常量值(ConstantValue),其初始化直接由 Java 虛擬機完成。除此以外的直接賦值操做,以及全部靜態代碼塊中的代碼,則會被 Java 編譯器置於同一方法中,並把它命爲 <clinit>class init)。

JVM 規範枚舉了下述類的初始化時機是:

  1. 當虛擬機啓動時,初始化用戶指定的主類;
  2. new 某個類的時候
  3. 調用某類的靜態方法時
  4. 訪問某類的靜態字段時
  5. 子類初始化會觸發父類初始化
  6. 用反射API對某個類進行調用時
  7. 一個接口定義了default方法(原文是non-abstract、non-static方法),某個實現了這個接口的類被初始化,那麼這個接口也會被初始化
  8. 初次調用 MethodHandle 實例時

注意:這裏沒有提到new 數組的狀況,因此new 數組的時候不會初始化類。

同時類的初始化過程是線程安全的,下面是一個利用上述時機4和線程安全特性作的延遲加載的Singleton的例子:

public class Singleton {
  private Singleton() {}
  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
  }
  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
}

這種作法被稱爲Initialization-on-demand holder idiom

類加載常見異常

ClassNotFoundException

Java Virutal Machine Specification - 5.3.1. Loading Using the Bootstrap Class Loader

If no purported representation of C is found, loading throws an instance of ClassNotFoundException.

Java Virutal Machine Specification - 5.3.2. Loading Using a User-defined Class Loader

When the loadClass method of the class loader L is invoked with the name N of a class or interface C to be loaded, L must perform one of the following two operations in order to load C:

  1. The class loader L can create an array of bytes representing C as the bytes of a ClassFile structure (§4.1); it then must invoke the method defineClass of class ClassLoader. Invoking defineClass causes the Java Virtual Machine to derive a class or interface denoted by N using L from the array of bytes using the algorithm found in §5.3.5.
  2. The class loader L can delegate the loading of C to some other class loader L'. This is accomplished by passing the argument N directly or indirectly to an invocation of a method on L' (typically the loadClass method). The result of the invocation is C.

In either (1) or (2), if the class loader L is unable to load a class or interface denoted by N for any reason, it must throw an instance of ClassNotFoundException.

因此,ClassNotFoundException發生在【加載階段】:

  1. 若是用的是bootstrap class loader,則當找不到其該類的二進制形式時拋出ClassNotFoundException
  2. 若是用的是用戶自定義class loader,無論是本身建立二進制(這裏包括從文件讀取或者內存中建立),仍是代理給其餘class loader,只要出現沒法加載的狀況,都要拋出ClassNotFoundException

NoClassDefFoundError

Java Virtual Machine Specification - 5.3. Creation and Loading

If the Java Virtual Machine ever attempts to load a class C during verification (§5.4.1) or resolution (§5.4.3) (but not initialization (§5.5)), and the class loader that is used to initiate loading of C throws an instance of ClassNotFoundException, then the Java Virtual Machine must throw an instance of NoClassDefFoundError whose cause is the instance of ClassNotFoundException.

(A subtlety here is that recursive class loading to load superclasses is performed as part of resolution (§5.3.5, step 3). Therefore, a ClassNotFoundException that results from a class loader failing to load a superclass must be wrapped in a NoClassDefFoundError.)

Java Virtual Machine Specification - 5.3.5. Deriving a Class from a class File Representation

Otherwise, if the purported representation does not actually represent a class named N, loading throws an instance of NoClassDefFoundError or an instance of one of its subclasses.

Java Virtual Machine Specification - 5.5. Initialization

If the Class object for C is in an erroneous state, then initialization is not possible. Release LC and throw a NoClassDefFoundError.

因此,NoClassDefFoundError發生在:

  1. 【加載階段】,因其餘類的【驗證】or【解析】觸發對C類的【加載】,此時發生了ClassNotFoundException,那麼就要拋出NoClassDefFoundError,cause 是ClassNotFoundException
  2. 【加載階段】,在【解析】superclass的過程當中發生的ClassNotFoundException也必須包在NoClassDefFoundError裏。
  3. 【加載階段】,發現找到的二進制裏的類名和要找的類名不一致時,拋出NoClassDefFoundError
  4. 【初始化階段】,若是C類的Class對象處於錯誤狀態,那麼拋出NoClassDefFoundError

追蹤類的加載

能夠在JVM啓動時添加-verbose:class來打印類加載過程。

參考資料

廣告

clipboard.png

相關文章
相關標籤/搜索