更多 Java 虛擬機方面的文章,請參見文集《Java 虛擬機》
一個類 Person 從代碼到使用:
- 編譯器負責將 Person.java 源文件編譯為 Person.class 字節碼文件
- 類加載器 Class Loader 負責將 Person.class 字節碼 (表現形式為字節數組 byte[])轉換為 JVM 中的 Class<Person> 對象
- 隨后 JVM 再利用 Class<Person> 對象 實例化為 Person 對象
1. 類的加載
1.1 類加載器 Class Loader
作用:
- 將 .class 文件中的字節碼轉換為 JVM 中的 Class 對象(不是 Class 的實例)
-
為 JVM 中相同名稱的類創建隔離空間。使得同一名稱不同版本的兩個 Java 類可以在 JVM 中同時存在,例如 OSGI。
在 JVM 中判斷兩個類是否相同:類的二進制名稱相同 并且 類加載器相同
類加載器 Class Loader 具有層次組織結構,即每個類加載器都有一個父類加載器,通過 getParent()
可以獲得父類加載器。
類加載器 Class Loader 使用代理模式,每個類加載器即可以自己完成 Java 類的定義工作,也可以代理給其他的類加載器來完成。
- 初始類加載器:啟動一個類的加載過程
-
定義類加載器:負責最終定義這個類。
例如在下面的代碼中,A 的定義類加載器負責啟動 B 的加載過程:
class A {
private B b;
}
1.2 類加載器 Class Loader 的加載策略
- 類加載器在嘗試自己去加載某個類之前,會首先代理給父類加載器。當父類加載器在 class path 中找不到對應的 .class 字節碼文件時,才會嘗試自己加載。
一般的 Java 應用使用該策略。從ClassLoader
的loadClass()
方法中可以看出c = parent.loadClass(name, false);
:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- 相反策略,類加載器首先嘗試自己去加載某個類,當其在 class path 中找不到對應的 .class 字節碼文件時,再代理給父類加載器。
該策略在 Java Web 容器中比較常見。Apache Tomcat 為每個 Application 提供一個獨立的類加載器WebappClassLoader
,使得 Application 自己的類的優先級高于 Web 容器提供的類,因此不同的 Application 可以使用不同版本的庫。
1.3 JVM 自帶的 Class Loader
- SystemClassLoader:C++編寫,加載核心庫
java.*
- ExtClassLoader:Java編寫,加載擴展庫
javax.*
- AppClassLoader:Java編寫,加載程序所在目錄
通過 Thread.currentThread().getContextClassLoader()
獲得當前類加載器
public static void main(String[] args) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
System.out.println(cl.toString());
try {
Class c = cl.loadClass("jvm.Person");
System.out.println(c.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
輸出:
sun.misc.Launcher
AppClassLoader@75b84c92
1.4 自定義類加載器 Class Loader
繼承父類 ClassLoader
, ClassLoader
中包含如下方法:
-
final Class<?> defineClass(String name, byte[] b, int off, int len)
- 將字節碼數組轉換為 Class<?> 對象
- 該方法不能被 override
-
final Class<?> findLoadedClass(String name)
- 查找已經加載過的 Class<?> 對象,即 Java 類
- 一個類加載器不會重復加載同一個類
- 該方法不能被 override
-
Class<?> findClass(String name)
- 根據名稱查找并加載 Java 類
- 該方法需要被 override
-
Class<?> loadClass(String name)
- 根據名稱加載 Java 類
- 該方法不能被 override
例如我們可以自定義一個類加載器負責從網絡中獲取字節碼,并轉化為 Class 對象。
class MyClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = new byte[1024];
// 從 網絡中根據 name 讀取 字節數組
// 將字節碼數組轉換為 Class<?> 對象
return defineClass(name, bytes, 0, bytes.length);
}
}
1.5 顯示加載 VS 隱式加載
-
顯示加載:
- 通過
Class c = Class.forName("Student");
- 通過 ClassLoader 的
loadClass()
方法,例如:
- 通過
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class c = cl.loadClass("Student");
- Class.forName() 與 ClassLoader.loadClass() 的區別
-
隱式加載:通過
new
,例如Student s = new Student("")
2. 類的鏈接
類的鏈接:將 Java 類的二進制代碼合并到 JVM 的運行狀態之中的過程。
包括三個步驟:
- 驗證:確保 Java 類的二進制表示在結構上是合理的
- 準備:創建靜態域并賦值
- 解析:確保當前類引用的其他類被正確地找到,該過程可能會觸發其他類被加載。
關于解析,不同的 JVM 有不同的解析策略,例如:
public class A {
public void main(String args[]) {
B b = null;
}
}
- 策略1:鏈接 A 的時候發現引用了 B,因此加載 B
- 策略2:鏈接 A 的時候發現引用了 B,但是 B 沒有被使用,因此不加載 B。在真正使用 B 時才加載 B,例如
b = new B();
3. 類的初始化
類的初始化:當 Java 類第一次被真正使用的時候,JVM 會負責初始化該類。包括:
- 執行靜態代碼塊
- 初始化靜態域
注意:是類的初始化,不是對象的初始化。
例如:下面的代碼不會初始化類 A,因為 A 沒有真正被使用。
public static void main(String[] args) {
A a;
}
static class A {
static int i = 10;
static {
System.out.println("Init class A");
}
}
例如:下面的代碼會初始化類 A,因為 A 真正被使用,輸出 Init class A
public static void main(String[] args) {
int i = A.i;
}
static class A {
static int i = 10;
static {
System.out.println("Init class A");
}
}