讀書筆記之《實戰Java虛擬機》(10):Class 裝載系統

Class 文件裝載流程

類裝載條件:java

  • 建立一個類的實例時;
  • 調用類的靜態方法時;
  • 使用類或接口的靜態字段時(final 常量除外);
  • 使用 java.lang.reflect 包中的方法反射類的方法是;
  • 初始化子類時,要求先初始化父類;
  • 做爲啓動虛擬機,含有 main() 方法的那個類;

Parent:apache

public class Parent {

    static {
        System.out.println("Parent init");
    }

    public static int v = 100;
}
複製代碼

Child:數組

public class Child extends Parent {

    static {
        System.out.println("Child init");
    }
}
複製代碼

測試:bash

public class Main {

    public static void main(String[] args) {
        Child child = new Child();
    }
}
複製代碼

輸出:數據結構

Parent init
Child init
複製代碼

修改測試類:jvm

public class Main {

    public static void main(String[] args) {
        System.out.println(Child.v);
    }
}
複製代碼

輸出:ide

Parent init
100
複製代碼

引用一個字段時,只有直接定義該字段的類,纔會被初始化。測試

雖然 Child 類沒有被初始化,可是此時 Child 類以及被系統加載,只是沒有進入到初始化階段。ui

使用 -XX:+TraceClassLoading 打印:this

[Loaded Parent from file:/D:/workspace/tutorial-jvm/out/production/tutorial-jvm/]
[Loaded Child from file:/D:/workspace/tutorial-jvm/out/production/tutorial-jvm/]
Parent init
100
複製代碼

若是修改 Parent 靜態變量,用 final 修飾,再次執行:

100
複製代碼

javac 在編譯時,將常量直接植入目標類,再也不使用被引用類。

加載類

  • 經過類的全名,獲取類的二進制數據;
  • 解析類的二進制數據流爲方法區內的數據結構;
  • 建立 java.lang.Class 類的實例,表示該類型;

驗證類

  • 格式檢查
  • 語義檢查
  • 字節碼驗證
  • 符號引用驗證

準備

虛擬機會爲這個類分配相應的內存空間,並設置初始值。

解析類

將類、接口、字段和方法的符號引用專爲直接引用。

初始化

類裝載的最後階段,開始執行 Java 字節碼。

ClassLoader

ClassLoader,類裝載器。CLassLoader 在 Java 中有着很是重要的左右,主要工做在 Class 裝在的加載階段,其主要做用是從系統外部得到 Class 二進制數據流。

認識 ClassLoader

全部的 Class 都是由 ClassLoader 進行加載的,ClassLoader 負責經過各類法師將 Class 信息的二進制數據流讀入系統,而後交給 Java 虛擬機進行鏈接、初始化等操做。

ClassLoader 是一個抽象類,主要方法以下:

  • public Class<?> loadClass(String name) throws ClassNotFoundException
    給定一個類名,加載一個類;
  • protected final Class<?> defineClass(byte[] b, int off, int len)
    根據給定的字節流 b 定義一個類,off 和 len 參數表示實際 Class 信息在 byte 數組中的位置和長度。這是受保護的方法,只有在自定義 ClassLoader 子類中可使用;
  • protected Class<?> findClass(String name) throws ClassNotFoundException
    查找一個類,這是一個受保護的方法,也是重載 ClassLoader 時,重要的系統擴展點;
  • protected final Class<?> findLoadedClass(String name)
    尋找已經加載的類。final 修飾,沒法被修改;

ClassLoader 中,還有一個重要的字段 parent,也是一個 ClassLoader 的實例,表示這個 ClassLoader 的雙親。類加載過程當中, ClassLoader 可能會將某些請求交予本身的雙親處理。

ClassLoader 分類

標準 Java 程序中,Java 虛擬機會建立三種 ClassLoader 爲整個應用程序服務:

  • BootStrap ClassLoader 啓動類加載器
  • Extension ClassLoader 拓展類加載器
  • App ClassLoader 應用類加載器,也成爲系統類加載器

自下往上爲本身的雙親。當系統須要使用一個類時,在判斷類是否已經被加載時,會先從當前底層類加載器進行判斷。當系統須要加載一個類時,會從頂層類開始加載,一次向下嘗試,直到成功。

public class Main {

    public static void main(String[] args) {
        ClassLoader classLoader = Main.class.getClassLoader();
        while (classLoader != null) {
            System.out.println(classLoader);
            classLoader = classLoader.getParent();
        }
    }
}
複製代碼

輸出:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
複製代碼

Main 類家在與 AppClassLoader,它的雙親爲 ExtClassLoader。可是 ExtClassLoader 沒法再取得啓動類加載器,是由於這是一個系統級的純 C 實現。所以任何加載在啓動類加載器中的類是沒法獲取其 ClassLoader 實例的:

String.class.getClassLoader() // null
複製代碼

雙親委託

在類加載的時候,系統會判斷當前類是否已經被加載,若是已經被加載,就會直接返回可用的類,不然就會嘗試加載。在嘗試加載時,會先請求雙親處理,若是雙親請求失敗,則會本身加載。

參考 ClassLoader 中 loadClass() 方法:

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;
        }
    }
複製代碼

雙親爲 null 有兩種狀況,第一,其雙親就是啓動類加載器;第二,當前加載器就是啓動類加載器。

判斷類是否加載時,應用類加載器會順着雙親路徑網上判斷,直到啓動類加載器。可是啓動類加載器不會往下詢問,這個委託路線是單向的,理解這點很重要。

雙親委託的弊端

頂層的 ClassLoader 沒法訪問底層的 ClassLoader 所加載的類。

當在系統類中,提供一個接口,接口須要在應用中得以實現。該接口綁定一個工廠方法,用於建立該接口的實例。因爲啓動類加載器沒法向下詢問,就會出現該工廠發發沒法建立由應用類加載器加載的應用實例。擁有這種問題組件有不少,好比 JDBC、Xml Parser 等。

雙親委託補充

以 javax.xml.parses 中實現 XML 文件解析功能模塊爲例,構造一個 DocumentBuilderFactory 的實例(加載在啓動類加載器中):

public static DocumentBuilderFactory newInstance() {
    return FactoryFinder.find(
            /* The default property name according to the JAXP spec */
            DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory"
            /* The fallback implementation class name */
            "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
}
複製代碼

跟進這個方法,最後閱讀到:

ClassLoader getContextClassLoader() throws SecurityException{
    return (ClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
        public Object run() {
            ClassLoader cl = null;
            //try {
            cl = Thread.currentThread().getContextClassLoader();
            //} catch (SecurityException ex) { }

            if (cl == null)
                cl = ClassLoader.getSystemClassLoader();

            return cl;
        }
    });
}
複製代碼

Thread 有兩個方法:

  • public ClassLoader getContextClassLoader()
  • public void setContextClassLoader(ClassLoader cl)

經過這兩個方法,能夠把一個 ClassLoader 置於一個線程實例之中,使其相對共享,默認狀況下上下文加載器就是應用類加載器。

突破雙親模式

public class MyClassLoader extends ClassLoader {

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 重寫類加載流程
        return super.loadClass(name);
    }
}
複製代碼

熱替換

熱替換是指在程序的運行過程當中,不中止服務,只經過替換程序文件來修改程序的行爲。基本上大部分腳本語言天生支持熱替換,例如 PHP。

兩個不一樣 ClassLoader 加載同一個類,在虛擬機內部,會認爲這 2 個類是徹底不一樣的。

自定義 ClassLoader:

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

/** * @author caojiantao */
public class MyClassLoader extends ClassLoader {

    private String fileName;

    public MyClassLoader(String fileName) {
        this.fileName = fileName;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(name);
        if (clazz == null) {
            try (FileInputStream is = new FileInputStream(fileName + name + ".class");
                 ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                byte[] buffer = new byte[1024];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    baos.write(buffer, 0, len);
                }
                byte[] bytes = baos.toByteArray();
                return defineClass(name, bytes, 0, bytes.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.findClass(name);
    }
}
複製代碼

熱替換的 Java 類:

/** * @author caojiantao */
public class Hello {

    public void sayHello() {
        System.out.println("hello");
    }
}
複製代碼

測試類:

import java.lang.reflect.Method;

/** * @author caojiantao */
public class Main {

    public static void main(String[] args) {
        while (true) {
            MyClassLoader classLoader = new MyClassLoader("C:\\Users\\caojiantao\\Desktop\\");
            try {
                Class clazz = classLoader.loadClass("Hello");
                Object instance = clazz.newInstance();
                Method say = instance.getClass().getMethod("sayHello");
                say.invoke(instance);
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }
}
複製代碼

將 Hello.java 編譯後的 class 文件放在桌面,執行程序:

hello
hello
複製代碼

修改 Hello 類,從新編譯成 class 文件替換桌面文件:

/** * @author caojiantao */
public class Hello {

    public void sayHello() {
        System.out.println("hello world");
    }
}
複製代碼

輸出:

hello
hello
hello world
複製代碼

因爲雙親沒法加載 Hello 類,每次都由自定義的 ClassLoader 加載,從而達到熱替換的效果。

相關文章
相關標籤/搜索