一篇帶給你JVM 類加載過程解析
類加載過程
類加載的時機
一個類型被加載到虛擬機內存中開始,到卸載出內存為止、它的整個生命周期將會經歷加載、驗證、準備、解析、初始化、使用、卸載七個階段。其中驗證、準備、解析為連接
類被主動加載的 7 種情況
- 創建類的實例, 比如:new Object();
- 訪問某個類或接口的靜態變量,或者對該靜態變量賦值;
- 調用類的靜態方法;
- 反射(如 Class.forName("com.test.Test");
- 初始化一個類的子類;
- Java虛擬機啟動時被標記為啟動類的類, 就是包含 main 方法的類(Java Test);
- JDK1.7開始提供的動態語言支持,java.lang.invoke.MethodHandle實例的解析結果REF_getStatic, REF_putStatic,;
REF_invokeStatic句柄對應的類沒有被初始化則初始化。
其它加載情況
當 Java 虛擬機初始化一個類時,要求它所有的父類都被初始化,單這一條規則并不適用于接口。
- 在初始化一個類時,并不會先初始化它所實現的接口
- 在初始化一個接口時,并不會先初始化它的父類接口
- 因此,一個父接口并不會因為他的子接口或者實現了類的初始化而初始化,只有當程序首次被使用特定接口的靜態變量時,才會導致該接口的初始化。
只有當前程序訪問的靜態變量或靜態方法確實在當前類或當前接口定義時,才可認為是對接口或類的主動使用。
調用 ClassLoader 類的 loadClass 方法加載一類,并不是對類的主動使用,不會導致類的初始化。
測試例子 1:
- public class Test_2 extends Test_2_A {
- static {
- System.out.println("子類靜態代碼塊");
- }
- {
- System.out.println("子類代碼塊");
- }
- public Test_2() {
- System.out.println("子類構造方法");
- }
- public static void main(String[] args) {
- new Test_2();
- }
- }
- class Test_2_A {
- static {
- System.out.println("父類靜態代碼塊");
- }
- {
- System.out.println("父類代碼塊");
- }
- public Test_2_A() {
- System.out.println("父類構造方法");
- }
- public static void find() {
- System.out.println("靜態方法");
- }
- }
- //代碼塊和構造方法執行順序
- //1).父類靜態代碼塊
- //2).子類靜態代碼塊
- //3).父類代碼塊
- //4).父類構造方法
- //5).子類代碼塊
- //6).子類構造方法
測試例子 2:
- public class Test_1 {
- public static void main(String[] args) {
- System.out.println(Test_1_B.str);
- }
- }
- class Test_1_A {
- public static String str = "A str";
- static {
- System.out.println("A Static Block");
- }
- }
- class Test_1_B extends Test_1_A {
- static {
- System.out.println("B Static Block");
- }
- }
- //輸出結果
- //A Static Block
- //A str
類加載流程
加載
在硬盤上查找并且通過 IO 讀入字節碼文件,使用到該類的時候才會被加載,例如調用 main 方法, new 關鍵字調用對象等,在加載階段會在內存中生成這個類的 java.lang.Class 對象, 作為方法區這個類的各種數據的訪問入口。
驗證
校驗字節碼文件的正確性
準備
給類的靜態變量分配內存,并且賦予默認值
解析
將符號引用替換為直接引用,該節點會把一些靜態方法(符號引用,比如 main() 方法)替換為指向數據所存內存的指針或句柄等(直接引用),這就是所謂的靜態鏈接過程(類加載期間完成),動態鏈接是在程序運行期間完成的將符號引用替換為直接引用。
初始化
對類的靜態變量初始化為指定的值,執行靜態代碼塊。
類加載器
- **_引導類加載器(Bootstrap Class Loader) _**負責加載
\lib\ 目錄或者被 -Dbootclaspath 參數指定的類, 比如: rt.jar, tool.jar 等 。 - 拓展類加載器(Extension Class Loader) 負責加載
\lib\ext\ 或 -Djava.ext.dirs 選項所指定目錄下的類和 jar包。 - 應用程序類加載器(System Class Loader) 負責加載 CLASSPATH 或 -Djava.class.path所指定的目錄下的類和 jar 包。
- 自定義類加載器:負責加載用戶自定義包路徑下的類包,通過 ClassLoader 的子類實現 Class 的加載。
測試文件:
- public class TestJVMClassLoader {
- public static void main(String[] args) {
- System.out.println(String.class.getClassLoader());
- System.out.println(DESKeyFactory.class.getClassLoader());
- System.out.println(TestJVMClassLoader.class.getClassLoader());
- System.out.println();
- ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
- ClassLoader extClassLoader = appClassLoader.getParent();
- ClassLoader bootstrapClassLoader = extClassLoader.getParent();
- System.out.println("bootstrapClassLoader: " + bootstrapClassLoader);
- System.out.println("extClassLoader: " + extClassLoader);
- System.out.println("appClassLoader: " + appClassLoader);
- System.out.println();
- System.out.println("bootstrapLoader 加載以下文件:");
- URL[] urls = Launcher.getBootstrapClassPath().getURLs();
- for (URL url : urls) {
- System.out.println(url);
- }
- System.out.println();
- System.out.println("extClassLoader 加載以下文件:");
- System.out.println(System.getProperty("java.ext.dirs"));
- System.out.println();
- System.out.println("appClassLoader 加載以下文件:");
- System.out.println(System.getProperty("java.class.path"));
- }
- }
雙親委派機制
什么是雙親委派機制?
一個類加載器收到了類加載的請求, 它首先不會自己去嘗試自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(即搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己完成加載。
類加載和雙親委派模型如下圖所示
我們再來看看 ClassLoader 類的 loadClass 方法
- // loadClass
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- // 首先檢查當前類是否被加載
- 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;
- }
- }
- // 類加載器的包含關系
- public abstract class ClassLoader {
- private static native void registerNatives();
- static {
- registerNatives();
- }
- // 當前 ClassLoader 和 parent ClassLoader 的包含關系
- private final ClassLoader parent;
- }
總結:
- 不是樹形結構(只是邏輯樹形結構),而是包含/包裝關系。
- 加載順序,應用類加載器,拓展加載器,系統加載器。
- 如果有一個類加載器能夠成功加載 Test 類,那么這個類加載器被稱為定義類加載器,所有可能返回 Class 對象引用的類加載器(包括定義類加載器)都被稱為初始類加載器。
設計雙親委派機制的目的?
- 保證 Java 核心庫的類型安全:所有的java 應用都會至少引用 java.lang.Object 類, 也就是說在運行期, java.lang.Object 的這個類會被加載到 Java 虛擬機中,如果這個加載過程是由 Java 應用自己的類加載器所完成的,那么很有可能會在 JVM 中存在多個版本的 java.lang.Object 類,而且這些類之間還是不兼容的。互不可見的(正是命名空間發揮著作用)借助于雙親委托機制,Java 核心庫中的類加載工作都是由啟動類加載器統一來完成的。從而確保了Java 應用所使用的都是同一個版本的 Java 核心類庫,他們之間是相互兼容的。
- 可以確保 Java 核心庫所提供的類不會被自定義的類所替代。
- 不同的類加載器可以為相同類(binary name)的類創建額外的命名空間。相同名稱的類可以并存在Java虛擬機中,只需要不同的類加載器來加載他們即可,不同的類加載器的類之間是不兼容的,這相當于在JAVA虛擬機內部創建了一個又一個相互隔離的Java類空間,這類技術在很多框架中得到了實際運用。
自定義類加載器
自定義類加載器加載類,下面是一個簡單的 Demo
- import java.io.ByteArrayOutputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.InputStream;
- public class ClassLoaderTest extends ClassLoader {
- private static String rxRootPath;
- static {
- rxRootPath = "/temp/class/";
- }
- @Override
- public Class findClass(String name) {
- byte[] b = loadClassData(name);
- return defineClass(name, b, 0, b.length);
- }
- /**
- * 讀取 .class 文件為字節數組
- *
- * @param name 全路徑類名
- * @return
- */
- private byte[] loadClassData(String name) {
- try {
- String filePath = fullClassName2FilePath(name);
- InputStream is = new FileInputStream(new File(filePath));
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- byte[] buf = new byte[2048];
- int r;
- while ((r = is.read(buf)) != -1) {
- bos.write(buf, 0, r);
- }
- return bos.toByteArray();
- } catch (Throwable e) {
- e.printStackTrace();
- }
- return null;
- }
- /**
- * 全限定名轉換為文件路徑
- *
- * @param name
- * @return
- */
- private String fullClassName2FilePath(String name) {
- return rxRootPath + name.replace(".", "//") + ".class";
- }
- public static void main(String[] args) throws ClassNotFoundException {
- ClassLoaderTest classLoader = new ClassLoaderTest();
- String className = "com.test.TestAA";
- Class clazz = classLoader.loadClass(className);
- System.out.println(clazz.getClassLoader());
- // 輸出結果
- //cn.xxx.xxx.loader.ClassLoaderTest@3764951d
- }
- }
Tomcat 類加載器
Tomcat 中的類加載器模型
Tomcat 類加載器說明
tomcat 的幾個主要類加載器:
- commonLoader:Tomcat 最基本的類加載器, 加載路徑中的 class 可以被 Tomcat 容器本身以及各個 WebApp 訪問。
- catalinaLoader:Tomcat 容器私有的類加載器 加載路徑中的 class 對于 Webapp 不可見;
- sharaLoader: 各個Webapp 共享的類加載器, 加載路徑中的 class 對于所有 webapp 可見, 但是對于 Tomcat 容器不可見。
- webappLoader: 各個 Webapp 私有的類加載, 加載路徑中的 class 只對當前 webapp 可見, 比如加載 war 包里面相關的類,每個 war 包應用都有自己的 webappClassLoader 對象,對應不同的命名空間,實現相互隔離,比如 war 包中可以引入不同的 spring 版本,實現多個 spring 版本 應用的同時運行。
總結:
從圖中的委派關系中可以看出:
Commonclassloader 能加載的類都可以被 Catalinaclassloader和 Sharedclassloadert 使用, 從而實現了公有類庫的共用,而Catalinaclassloader 和 Sharedclassloader自己能加載的類則與對方相互隔離 Webappclassloader 可以使用 Shared Loader 加載到的類,但各個 Webappclassloader 實例之間相互隔離而 Jasper Loader 的加載范圍僅僅是這個 JSP 文件所編譯出來的那一個 . class 文件,它出現的目的就是為了被丟棄: 當 Web 容器檢測到 JSP 文件被修改時,會替換掉目前的 Jasperloader 的實例,并通過再建立一個新的 Jsp 類加載器來實現 JSP 文件的熱加載功能。
Tomcat這種類加載機制違背了java推薦的雙親委派模型了嗎? 答案是: 違背了
tomcat不是這樣實現, tomcat為了實現隔離性, 沒有遵守這個約定, 每個 webapp Loader加載自己的目錄下的 class'文件,不會傳遞給父類加載器,打破了雙親委派機制
參考資料
《深入理解 Java 虛擬機》 第三版 周志明
Apache Tomcat Documentation