歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 理解Java類加載機制(譯文)

理解Java類加載機制(譯文)

日期:2017/3/1 9:16:20   编辑:Linux編程

理解java類加載機制

你想寫類加載器?或者你遇到了ClassCastException異常,或者你遇到了奇怪的LinkageError狀態約束異常。應該仔細看看java類的加載處理了。

什麼是類加載器以及它是如何對類進行加載的?

一個Java類是由java.lang.ClassLoader類的一個實例加載的。由於java.lang.ClassLoader自己本身是一個抽象類所以一個類加載器只能夠是java.lang.ClassLoader類的具體子類的實例。如果是這種情況,那麼哪一個類加載器來加載java.lang.ClassLoader這個類?(經典的"誰將會加載加載者"引導的問題)。事實證明JVM有一個內置的引導類加載器。引導加載器加載java.lang.ClassLoader和許多其他java平台類。

要加載一個具體的java類,例如com.acme.Foo,JVM調用java.lang.ClassLoader類的loadClass方法(事實上,JVM查找loadClassInternal方法-如果發現loadClassInternal方法則用loadClassInternal方法,否則JVM使用loadClass方法,而loadClassInternal方法會調用loadClass方法)。loadClass方法接收類名來加載類返回表示加載的類的java.lang.Class實例。事實上loadClass方法找到.class文件(或者URL)的實際字節,並調用defineClass方法來構造出java.lang.Class類的字節數組。加載器上調用loadClass方法的加載器稱之為初始化加載器(即,JVM啟動加載使用這個加載器).但是,啟動加載器不是直接加載類的-而是可能委托給另外一個類加載器(例如,它的父加載器)-它自己也可能委派給另外一個加載器去加載等等。最終在委托鏈中的某些類加載器對象調用defineClass方法加載有關的類(com.acme.Foo)。
這個特殊的類加載器叫做com.acme.Foo的確切加載器。在運行時,一個java類是由類的完全限定類名和和類加載器確定其唯一性的。如果指定相同的類名(即,相同的完全限定類名)的類是由兩個不同的類加載器加載的,那麼這些類是不同的-即使這些.class的字節碼是相同的並且都是從相同的位置進行加載的(相同的URL)。

有多少種類加載器並且它們是從哪裡加載類的?

即便是一個簡單的"hell world"java程序,也有至少3種類加載器。

  1. 引導類加載器

    • 加載java平台基礎類(例如java.lang.Object,java.lang.Thread等)。
    • 加載rt.jar包種的類($JRE_HOME/lib/rt.jar)。
    • -Xbootclasspath參數可以更改啟動類路徑-Xbootclasspath/p: 和 -Xbootclasspath/a:參數可以前置/追加額外的引導目錄-一定要格外小心這樣做。在大多數情況下,要避免隨意變更啟動類路徑。
    • 在Sun的實現中,只讀系統屬性sun.boot.class.path設置為指向啟動類路徑。注意你不能在運行時修改這個屬性-如果你修改了修改也不會生效。
    • 這個加載器由java的null表示。例如,java.lang.Object.class.getClassLoader()方法將會返回null(還有其他的類例如java.lang.Integer,java.awt.Frame,java.sql.DriverManager等)
  2. 擴展類加載器
    • 從已安裝的可選包中加載類。
    • 從$JRE_HOME/lib/ext目錄下加載jar文件。
    • 可以使用-Djava.ext.dirs命令修改系統屬性java.ext.dirs來修改擴展目錄。
    • 在Sun的實現中,它是sun.misc.Launcher$ExtClassLoader的實例(事實上它是sun.misc.Launcher類的一個內部類)。
    • 在編寫代碼中。你可能讀取(只能夠讀取!)系統通屬性java.ext.dirs來尋找哪個目錄是擴展目錄。你不能在在運行期間修改這個屬性-即使你修改了修改也不會生效。
  3. 應用類加載器
    • 從應用的classpath中加載類。
    • 使用環境變量CLASSPATH(者-cp或者-classpath選項設置應用的classpath,如果CLASSPATH和-cp都找不到,則使用"."(當前目錄)。
    • 只讀系統屬性java.class.path是應用類路徑。你不能在運行期間修改這個屬性-即使你修改了修改也不會生效。
    • java.lang.ClassLoader.getSystemClassLoader()方法的返回值是這個加載器
    • 這個加載器也叫做"系統加載器"-不過不要將加載java"系統"類的啟動加載器混淆。
    • 這個加載器加載你應用的"main"(由main方法的類)。在Sun的實現中,它是sun.misc.Launcher$AppClassLoader的一個實例(事實上它是sun.misc.Launcher類的一個內部類)。
    • 默認的應用加載器用擴展加載器做為它的父加載器。
    • 你可以使用-Djava.system.class.loader命令更改應用的類加載器。這個值指定java.lang.ClassLoader的子類的名字.首先默認應用加載器加載已命名的類(這個類在CLASSPATH或者-cp)和創建一個它的實例.新創建的這個類的實例用於加載應用的main類。

一個類典型的類記載流程

讓我們假設你正在運行一個"hello world" java程序。我們來看一下類的加載流程。JVM用應用類加載器加載主方法(main)所在的類。如果你運行下面的程序

class Main {
    public static void main(String[] args) {
        System.out.println(Main.class.getClassLoader());
        javax.swing.JFrame f = new javax.swing.JFrame();
        f.setVisible(true);
        SomeAppClass s = new SomeAppClass();
}

它會打印如下內容
sun.misc.Launcher$AppClassLoader@17943a4

每當一些其它的類引用在Main類中被解析時,JVM用Main所在類的明確的加載器-應用類加載器-做為初始化加載器。在上面的列子中,為了加載javax.swing.JFrame類JVM將使用應用類加載器做為一個初始化加載器。即,JVM將用應用類應用做為初始化加載器。即。JVM將調用loadClass()方法(loadClassInternal方法)在應用類加載器中。應用類加載器委托給擴展類加載器。
擴展加載器檢查這是否是一個啟動類(用私有方法 - ClassLoader.findBootstrapClass),啟動類加載器是否從rt.jar加載過它。
當SomeAppClass的引用類被解析時,JVM有著相同的過程-用應用類加載器做為初始化加載器。
應用加載器委托給擴展加載器,擴展加載器檢查啟動加載器,啟動加載器找不到"SomeAppClass"類,
於是擴展加載器檢查"SomeAppClass"類是否在擴展jars裡,結果發現不在。
於是應用類加載器檢查在應用的CLASSPATH下的.class字節,如果找到了則進行加載,如果沒有找到,將會拋出NoClassDefFoundError異常。

總結:

  • Class是由具體的類加載器與類的完全限定類名唯一定義的。

  • 如果具體的類加載器不同,即使.class字符是從文件系統中的相同位置進行加載的Classes也是不同的。

  • 類加載器委托給父加載器進行加載。

  • 加載Bar類中引用的Foo類,JVM使用Bar類的確切的類加載器做為初始化加載器。JVM會在Bar類的確切加載器上會調用loadClass()方法加載Foo類。

  • JVM緩存->運行時的類每次初始化加載都將被記錄。JVM將會緩存用於以後的解析。即,loadClass()方法不會對於每一次引用都調用。這能確保時間的不變性-即,一個類加載器不允許加載相同類名但字節碼不同的類。
    他是由緩存來實現的。好的類加載器應該通過調用ClassLoader得call()方法來檢查緩存。

原文鏈接

Understanding Java class loading https://blogs.oracle.com/sundararajan/entry/understanding_java_class_loading

Copyright © Linux教程網 All Rights Reserved