Class與ClassLoader深入淺析

weixin_34146805發表於2018-10-30

先來一張java中JVM內的類載入器的層次體系結構,總整體上觀看JVM中所包含的classLoader有哪些,以及它們之間的關係:

9105777-0fdd2c0c24192d3e.jpg
class_loader.jpg

說到classLoader,從名字也可以知道它的作用。不就是載入類(class)的麼,那麼如何載入?何時載入?誰來載入呢等等問題,下面我就要來說說,實踐實踐。當然了,我還是先從class這個在OOP中屬於基石核心的概念來說說,並看看它是在java平臺是如何來的,具體長什麼樣哈?

Class檔案結構?

java平臺的強大跨平臺特性就是靠著這個東東啊。只要在java虛擬機器上執行開發的語言,像java,jruby,groovy等語言,都是執行在JVM基礎之上的。那麼在原始碼形式下,通過各自語言的編譯器,按照java虛擬機器規範(JVM要求在class檔案中使用許多強制性的語法和結構化約束)處理,就能得到一個通用的,機器無關的執行平臺的位元組碼檔案(*.class)。就如下圖所示:

9105777-8f91a2ce9e3fdcac.png
javac_class.png

那麼,這個萬能的class結構是什麼樣的呢?在java語言的jdk中是如何表示的?
由於JVM底層是c語言支援的,class檔案當然要通過符合C語言的資料結構來構造了。class的資料結構如下:
無符號數資料基本資料型別,以u1,u2,u4,u8來分別代表一個位元組(八位bit,也就是八個坑,每個坑只能放0和1),2個位元組,4個位元組和8個位元組的無符號數。
其中還包含表的概念,表是由多個無符號數或者其他表作為資料項構成的符合資料型別,所有的表習慣性的以”_info”結尾。整個class檔案本質上就是一張表。

Class {
    u4              magic;                  // 1 個  魔數                      
    u2              minor_version;          // 1    副版本號
    u2              major_version;          // 1    主版本號
    u2              constant_pool_count;    // 1    常量池計數器
    cp_info         constant_pool;          // constant_pool_count - 1 常量池資料區
    u2              access_flags;           // 1    訪問標識
    u2              this_class;             // 1    類索引
    u2              super_class;            // 1    父類索引
    u2              interfaces_count;       // 1    介面計數器
    u2              interfaces;             // interfaces_count  介面資訊資料區
    u2              fields_count;           // 1    欄位計數器
    field_info      fields;                 //fields_count  欄位資訊資料區
    u2              methods_count;          // 1    方法計數器
    method_info     methods;                // methods_count 方法資訊資料區
    u2              attributes_count;       // 1    屬性計數器
    attribute_info  attributes;             // attributes_count 屬性資訊資料區
}

可以看到,在OOP中class物件所涉及到的屬性,方法都有自己對應的表(field_info,method_info),其他的一些是其他資訊的記錄。

class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料項嚴格按照順序緊湊的排列在Class檔案中,中間沒有任何的分隔符。
這使得整個Class檔案中儲存的內容集合全部都是程式執行的必要資料,沒有空隙存在。

那當java檔案經過編譯得到的class的檔案是什麼樣的?
我們若是想要看懂class檔案中常量池內容,首先需要先知道常量池中14種常量池的結構,如下圖:

9105777-00f087095fbf427f.png
constant_pool_structrue_1.png

9105777-546493c8159cc877.png
constant_pool_structrue_2.png

理論上知道了class內部結構,那麼就可以自己編寫一個簡單java程式,並用javac命令將該java原始碼檔案編譯成位元組碼,在用編輯器開啟該位元組碼16進位制可以看到:
(現在我們看看class的二進位制檔案:(用了簡陋畫圖工具製作的,我使用JDK1.8來操作。看懂就就行(▔^▔)/ ))

9105777-01b888c45c867e60.png
class_file.png

臥槽,畫個圖,洪荒之力都出來了。好累….好了,關於java中class檔案內容就說到這。class檔案中內容包含挺多資料的,這些資料就決定了當類載入器載入該類,形成例項物件後具備那些功能啦。那麼反編譯工具如何得到java原始檔內容的問題也就明白了,還有其他像一些動態載入和代理增強功能實現,都是可以直接通過位元組碼進行操作實現的。
想看更詳細內容可以看這篇:JVM詳解部落格

class檔案如何被載入到JVM?

在class檔案通過原始碼編譯生成後,我們也知道class檔案內具體的二進位制資料內容代表什麼了,下面應該瞭解瞭解class檔案是如何被載入到JVM虛擬機器中的。類的載入當然與classLoader密不可分了。在java虛擬機器中,類載入全過程包括:載入,驗證,準備,解析和初始化五個階段。在載入階段,虛擬機器需要完成以下3件事情:

1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流。(可以從本地檔案,網路中,執行時動態生成,資料庫檔案等等地方獲取類位元組流)
2. 將這個位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構。
3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料訪問入口。

我在此說的就是這個載入階段過程。

JVM類載入器種類:

  • Bootstrap classLoader(引導類載入器):
    該類負責將存在\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(eg: rt.jar,名字不符合規範的即使放在lib中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被java程式直接引用。負責載入JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等。

  • Extension ClassLoader(擴充類載入器):
    這個載入器由sun.misc.Launcher$ExtClassLoader實現,負責載入\lib\ext目錄中,或者被java.ext.dirs系統環境變數指定路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。

  • .Application ClassLoader(應用系統程式類載入器):
    這個類載入器由sun.misc.Launcher$AppClassLoader實現,由於這個類載入器是ClassLoader中的getSystemClassLoader方法的返回值,所以一般稱為系統類載入器。它負責載入使用者類路徑(CLASSPATH)上所指定的類庫,開發者可以直接使用這個類載入器。通常我們自己寫的java類不就是通過該載入器獲取*.class中內容,並載入對應的Class物件的麼

  • Custom ClassLoader(自定義類載入器):
    想要自己實現一個類載入器,只需要繼承ClassLoader類即可。關於自定義類載入器有什麼作用,如何具體的實現自定義類載入器,需要在另外的文章中說了。

在這個載入階段,對於一個非陣列類載入階段(或者說載入階段中獲取二進位制位元組流動作)是我們開發人員可控性最強的,因為該階段既可以使用系統提供的引導類載入器完成,也可以有我們開發人員自定義的類載入器去完成,也就是說開發人員可以自定義類載入去控制位元組流的獲取方式。

對於陣列類而言,情況有所不同,陣列類本身不通過類載入建立,它是由Java虛擬機器直接建立的。但是陣列類裡面的資料型別的載入就與類載入器有關了:

  1. 若是陣列的元件型別(ComponentType)是引用型別,那麼就採用常規類載入器載入這個類,該陣列將在該元件型別的類載入器的類名稱空間上被標識。
  2. 若是元件型別不是引用型別(eg: int[]),java虛擬機器將會把該陣列標記為與引導類載入器關聯(Bootstarp classLoader)用來確定一個類的唯一性。

ClassLoader載入class過程

類載入器層次關係是雙親委派模型,該模型要求除了頂層的類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡的類載入器之間的父子關係一般不會以繼承關係來實現,而都是使用組合關係來複用父載入器的程式碼。本來想寫寫雙親委派的具體好處,和劣勢的,因為篇幅太大,就另起文章來說到說到。

      Bootstrap classLoader
             /\
            /||\
       Extenssion ClassLoader
             /\
            /||\
      Application ClassLoader
        /|         |\
User ClassLoader    User ClassLoader(自定義類載入器)

看看JDK中關於系統類載入器程式碼:java.lang.ClassLoader.java:

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();    //初始化獲取sun.misc.Launcher中的AppClassLoader類載入器
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
// The class loader for the system
// @GuardedBy("ClassLoader.class")
private static ClassLoader scl;
// Set to true once the system class loader has been set
// @GuardedBy("ClassLoader.class")
private static boolean sclSet;
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();      //載入sun.misc,Launcher類
        if (l != null) {
            Throwable oops = null;
            scl = l.getClassLoader();                               //載入sun.misc,Launcher類中AppClassLoader
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;      //若為真,表示系統類載入器載入成功完成
    }
}

上面可以看到,在ClassLoader類中,initSystemClassLoader方法會載入sun.misc.Launcher中的AppClassLoader屬性值,
就是獲取應用類載入器。
那麼該類的作用就是將CLASSPATH中java庫所有二進位制類位元組流載入到方法區(執行常量池,型別資訊,欄位資訊,方法資訊,類載入器引用等)中

而引導類載入器Bootstrap ClassLoader是JVM啟動的時候,就會自動建立該例項。

那看看sun.misc.Launcher原始碼,就可以知道擴充類載入器和應用類載入器的具體載入過程:(該Launcher類在rt.jar包中,該包放置所有J2SE的必要類)

當JVM啟動建立Bootstrap ClassLoader時候,就會載入rt.jar包中所有二進位制位元組流類資訊,到JVM中的方法區中。
然後,通過這些全限定的類位元組檔案,就可以建立對應的類物件例項,並將例項儲存到Heap中。

public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    private static Launcher launcher = new Launcher();
    public static Launcher getLauncher() {
        return launcher;
    }
    private ClassLoader loader;
    
    //ClassLoader.getSystemClassLoader會呼叫此方法
    public ClassLoader getClassLoader() {
        return loader;
    }
    public Launcher() {
        // 1. 建立ExtClassLoader 
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader");
        }
        // 2. 用ExtClassLoader作為parent去建立AppClassLoader 
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader");
        }
        // 3. 設定AppClassLoader為ContextClassLoader
        Thread.currentThread().setContextClassLoader(loader);
        //...
    }
    static class ExtClassLoader extends URLClassLoader {
        private File[] dirs;
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();
            return new ExtClassLoader(dirs);
        }
        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);
            this.dirs = dirs;
        }
        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            //...
            return dirs;
        }
    }
    /**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {
        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);
            URL[] urls = (s == null) ? new URL[0] : pathToURLs(path);
            return new AppClassLoader(urls, extcl);
        }
        AppClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent, factory);
        }
        
        /**
         * Override loadClass so we can checkPackageAccess.
         */
        public synchronized Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            int i = name.lastIndexOf('.');
            if (i != -1) {
                SecurityManager sm = System.getSecurityManager();
                if (sm != null) {
                    //
                    sm.checkPackageAccess(name.substring(0, i));
                }
            }
            return (super.loadClass(name, resolve));
        }
    }
}

可以看到,當獲取AppClassLoader的時候,就loader欄位賦值時候,是通過傳遞extcl變數,通過父類載入器去完成的。
引導類是載入java執行是必要的類庫檔案。

系統類載入器則是我們開發中經常寫的原始碼編譯成位元組碼,載入位元組碼類資訊到JVM方法區域中的工具。

因為不同的載入器的名稱空間會對相互載入的類的訪問性,可見性等都會有影響。每個執行緒其實也繫結著一個上下文的類載入器:

  1. 同一個名稱空間內的類是相互可見的。
  2. 子載入器的名稱空間包含所有的父載入器的命令空間,因此子載入器載入的類能看見父載入器載入的類。(eg:AppClassLoader可以看見所有BootstrapClassLoader載入的類,就像java.lang.*下所有的包,我們自定義的類都能使用該包空間下的所有的類。)
  3. 由父載入器載入的類不能看見子載入器載入的類。(這也算是雙親委託載入的一個弊端了,若是父載入器想要載入子載入器載入的類如何實現?)
  4. 如果兩個載入器之間沒有直接或者間接的父子關係。那麼它們各自載入的類是互不可見的。(像很多三方框架和伺服器,eg:tomcat中,自定義由類的類載入器來載入不同的名稱空間的類,那麼這些類互不可見,完全解耦,互不干擾和影響。注意這與具備父子關係的父載入器想訪問子載入器載入的類是不同的)
  5. 當兩個不同名稱空間內的類互不可見時候,其實還是可以採用java的反射機制來訪問例項的屬性和方法。

class檔案中的資訊在JVM如何儲存的?

Class物件比較特殊,它雖然是物件,但是存放在方法區裡面。根據我目前有限知識,參考了其他書籍可知。當JVM啟動例項化引導類載入器和擴充類,應用程式載入器時候,就會把包括JDK中重要庫包(eg:rt.jar,resources.jar、charsets.jar等)和我們自己寫的java程式碼編譯過後,儲存這class具體資訊的位元組碼檔案中的類資訊,都載入到JVM中的方法區中了。

包括將類中所有資訊(執行時常量,型別資訊,欄位資訊,方法資訊,屬性資訊,類載入器引用類資訊,對應class例項引用)都放置到方法區。

就像一個大的加工類工廠一樣,將包裝了許多原料的麻包,麻袋通過機器,人工(就相當與類載入器)精密處理,將不同的物料分配到倉房中的不同指定位置,以備後面需要材料的時候,進行出庫等等。

存放在JVM方法區中的並不包含類位元組流對應的物件例項。只是存放類的資訊和一些class例項引用。而物件例項大多通常都是建立在heap堆區域中了。

class物件如何獲取和建立?

類初始化節點是類載入過程最後一步,除了上面說的可以自定義類載入器對二進位制位元組流自行載入外,其餘過程都是JVM來主導和控制執行的。到了初始化階段,才是真正執行類中定義的java程式碼。(或者是class位元組碼檔案中已儲存到JVM方法區中的內容)

當一系列的類準備,載入,驗證,解析,初始化後呢,在java記憶體中也就有了對應類的Class代表的物件
在jdk的原始碼包:java.lang.Class中也說到java系統程式在執行時,一直對所有的物件進行所謂的執行時型別標識(靜態的編譯型別,動態的執行時型別),這項資訊儲存了每個物件所屬於的類,JVM通常就可以使用執行時型別標識資訊來選擇正確的方法呼叫執行,用來儲存每個物件的型別標識資訊的類就是Class類。(在JVM類載入器載入類位元組流的時候,也就會自動建立所謂的型別標識,將方法區域中類的資訊都對映儲存到Class的類物件例項中)

Class類沒有公共的構造器,Class物件是當JVM中類載入器載入很多類位元組流和顯示呼叫defineClass方法時自動建立好的。
java應用程式中的每個例項物件通過obj.getClass()都能得到其對應的Class類。每個類都有一個Class物件,當程式執行時候,JVM首先檢查要載入的類對應的Class物件是否已經載入。如果該Class物件沒有初始化載入,那麼JVM會根據全限定類名對.class檔案在類路徑上進行查詢,並將.class檔案內容裝載到Class物件中

每個陣列也會被對映到一個對應的Class物件例項,並且所有具有相同元素型別和維度的陣列都共享該Class物件。一般某個類的Class物件被載入記憶體,那麼就可以用來建立這個類的所有物件。(MyObject o = new MyObject())

  • 如何可以得到Class物件?
  1. 呼叫Object類的getClass()方法可以得到對應的Class物件。
Myobject o;
Class c1 = o.getClass();
  1. .使用Class類中的靜態forName()方法得到與全限定名字串對應的Class物件。也就是通過反射來獲取。(JVM類載入器載入,並封裝class資訊到Class物件中)
Class c2 = Class.forName("xx.xx.MyObject");
  1. 如果T時一個java型別(基本型別,引用型別),那麼可以通過T.class就代表了匹配的類物件。
Class cl1 = Student.class;
Class cl2 = int.class;
Class cl3 = String[].class;

那麼java.lang.Class有哪些常用的方法?

  1. getName()
    一個Class物件描述了一個特定類的屬性,Class類中最常用的方法getName已String形式返回Class 物件所表示的實體(類、介面、陣列類、基本型別或 void)名稱。
  2. newInstance()
    Class還有一個有用的方法可以為類建立一個例項,這個方法叫做newInstance()。例如:
    x.getClass.newInstance(),建立了一個同x一樣型別的新例項。newInstance()方法呼叫預設構造器(無引數構造器)初始化新建物件。
  3. getClassLoader()
    返回載入該類位元組碼的類載入器。

總的大體來說,java程式執行與OS互動圖如下:(參考自網路)


9105777-a95999e366221398.jpg
all.jpg

好了,對於class說得差不多了。從一開始的java原始碼,經過編譯成*.class位元組碼檔案,也詳細分析了位元組碼裡二進位制對應的含義。然後到JVM類載入器載入這些class位元組碼檔案,生成java.lang.Class物件。這個流程也流暢的完結了,若是有不對的地方,再修改。(僅供拋磚引玉.......)

參考:
Java虛擬機器規範(Java SE 7)中文版(Java_Virtual_Machine_Specification_Java_SE_7)
[深入理解Java虛擬機器:JVM高階特性與最佳實踐].周志明
亦山部落格

相關文章