ClassLoader(二)- 載入過程
本文原始碼在Github 。
本文僅為個人筆記,不應作為權威參考。
在前一篇文章初步瞭解ClassLoader 裡提到了委託模型(又稱雙親委派模型),解釋了ClassLoader hierarchy(層級)處理類載入的過程。那麼class檔案是如何變成Class物件的呢?
Class的載入過程
Class載入分為這幾步:
- 建立和載入(Creation and Loading)
-
連結(Linking)
- 驗證(Verification)
- 準備(Preparation)
- 解析(Resolution),此步驟可選
- 初始化(Initialization)
注: 前面說了陣列類是虛擬機器直接建立的,以上過程不適用於陣列類。
建立和載入(Creation and Loading)
何時會觸發一個類的載入?
Java Language Specification - 12.1.1. Load the Class Test :
The initial attempt to execute the methodmain
of classTest
discovers that the classTest
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嘗試查詢這個類的二進位制形式。
我們知道ClassLoader委託模型,也就是說實際觸發載入的ClassLoader和真正載入的ClassLoader可能不是同一個,JVM將它們稱之為initiating loader
和defining loader
(Java Virtual Machine Specification - 5.3. Creation and Loading
):
L
may create C by defining it directly or by delegating to another class loader. IfL
creates C directly, we say thatL
defines C or, equivalently, thatL
is thedefining 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. IfL
creates C, either by defining it directly or by delegation, we say thatL
initiates loading of C or, equivalently, thatL
is aninitiating 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不是陣列,那麼:
- 如果D的defining loader是bootstrap class loader,那麼C的initiating loader就是bootstrap class loader。
- 如果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:
- A class or interface is completely loaded before it is linked.
- A class or interface is completely verified and prepared before it is initialized.
也就是說僅要求:
- 一個類在被連結之前得是完全載入的。
- 一個類在被初始化之前得是被完全驗證和準備的。
所以對於解析的時機JVM Spec沒有作出太多規定,只說了以下JVM指令在執行之前需要解析符號引用:_anewarray_,checkcast_, _getfield_, _getstatic_, _instanceof_, _invokedynamic_, _invokeinterface_, _invokespecial_, _invokestatic_, _invokevirtual_, _ldc_, _ldc_w_, _multianewarray_, _new_, _putfield 和putstatic 。
看不懂沒關係,大致意思就是,用到欄位、用到方法、用到靜態方法、new類等時候需要解析符號引用。
初始化
如果直接賦值的靜態欄位被 final 所修飾,並且它的型別是基本型別或字串時,那麼該欄位便會被 Java 編譯器標記成常量值(ConstantValue),其初始化直接由 Java 虛擬機器完成。除此之外的直接賦值操作,以及所有靜態程式碼塊中的程式碼,則會被 Java 編譯器置於同一方法中,並把它命為<clinit>
(cl
assinit
)。
JVM 規範枚舉了下述類的初始化時機是:
- 當虛擬機器啟動時,初始化使用者指定的主類;
- new 某個類的時候
- 呼叫某類的靜態方法時
- 訪問某類的靜態欄位時
- 子類初始化會觸發父類初始化
- 用反射API對某個類進行呼叫時
- 一個介面定義了default方法(原文是non-abstract、non-static方法),某個實現了這個介面的類被初始化,那麼這個介面也會被初始化
- 初次呼叫 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 ofClassNotFoundException
.
Java Virutal Machine Specification - 5.3.2. Loading Using a User-defined Class Loader :
When theloadClass
method of the class loaderL
is invoked with the nameN
of a class or interface C to be loaded,L
must perform one of the following two operations in order to load C:
-
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 methoddefineClass
of class ClassLoader. Invoking defineClass causes the Java Virtual Machine to derive a class or interface denoted byN
usingL
from the array of bytes using the algorithm found in §5.3.5. -
The class loader
L
can delegate the loading of C to some other class loader L'. This is accomplished by passing the argumentN
directly or indirectly to an invocation of a method onL'
(typically theloadClass
method). The result of the invocation is C.
In either (1) or (2), if the class loaderL
is unable to load a class or interface denoted byN
for any reason, it must throw an instance of ClassNotFoundException.
所以,ClassNotFoundException
發生在【載入階段】:
ClassNotFoundException ClassNotFoundException
NoClassDefFoundError
Java Virtual Machine Specification - 5.3. Creation and Loading
ClassNotFoundException
, then the Java Virtual Machine must throw an instance ofNoClassDefFoundError
whose cause is the instance ofClassNotFoundException
.
(A subtlety here is that recursive class loading to load superclasses is performed as part of resolution (§5.3.5, step 3). Therefore, aClassNotFoundException
that results from a class loader failing to load a superclass must be wrapped in aNoClassDefFoundError
.)
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 namedN
, loading throws an instance ofNoClassDefFoundError
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. ReleaseLC
and throw aNoClassDefFoundError
.
所以,NoClassDefFoundError
發生在:
-
【載入階段】,因其他類的【驗證】or【解析】觸發對C類的【載入】,此時發生了
ClassNotFoundException
,那麼就要丟擲NoClassDefFoundError
,cause 是ClassNotFoundException
。 -
【載入階段】,在【解析】superclass的過程中發生的
ClassNotFoundException
也必須包在NoClassDefFoundError
裡。 -
【載入階段】,發現找到的二進位制裡的類名和要找的類名不一致時,丟擲
NoClassDefFoundError
-
【初始化階段】,如果C類的Class物件處於錯誤狀態,那麼丟擲
NoClassDefFoundError
追蹤類的載入
可以在JVM啟動時新增-verbose:class
來列印類載入過程。