深入理解java虛擬機-JVM高級特性和最佳實現(xiàn)(四)——類加載機制

每篇一葉

前言

上回說到垃圾收集機制和內(nèi)存分配,這回咱們來了解下虛擬機類加載機制。
“代碼編譯的結(jié)果從本地機器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲格式發(fā)展的一小步,卻是編程語言發(fā)展的一大步”

基本概念

類加載周期

加載、驗證、準備、解析、初始化、使用、卸載


類的生命周期

虛擬機規(guī)范中規(guī)定有且只有5種情況必須立即對類進行初始化。
1). 遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。
2). 使用java.lang.reflect包的方法對類進行反射調(diào)用
3). 當初始化一個類式,發(fā)現(xiàn)其父類還沒有初始化,先初始化其父類
4). 虛擬機啟動時,用戶需要制定一個主類(main),虛擬機會先初始化這個主類
5). 當使用JDK1.7動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,則這個方法句柄對應的類需要初始化。

我們通過代碼來驗證下相關(guān)信息
a. 被動使用類字段演示
子類調(diào)用父類靜態(tài)變量時

public class SuperClass {
    static{
        System.out.println("SuperClass init ...");
    }
    public  static int value=123;
}
public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init ...");
    }
    
}
public class NoInitialization {
    /**
     * -XX:+TraceClassLoading 查看類加載過程
     * @param args
     */
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

運行結(jié)果

SuperClass init ...
123

發(fā)現(xiàn),父類初始化了,至于要不要初始化子類,就要看虛擬機了。

b.通過定義數(shù)組引用類

public class NoInitialization {
    /**
     * -XX:+TraceClassLoading 查看類加載過程
     * @param args
     */
    public static void main(String[] args) {
        SuperClass[] sc = new SuperClass[10];
    }
}

發(fā)現(xiàn)父類,子類均不初始化

c.訪問常量不會導致類初始化

public class SuperClass {
    static{
        System.out.println("SuperClass init ...");
    }
    public  static int value=123;
    public final static String hello="hello,world";
}
public class NoInitialization {
    /**
     * -XX:+TraceClassLoading 查看類加載過程
     * @param args
     */
    public static void main(String[] args) {
        System.out.println(SubClass.hello);
    }
}

運行結(jié)果

hello,world

原因是常量會在編譯階段存入調(diào)用類的常量池中,本質(zhì)上沒有直接引用到定義常量的類

類的加載過程
  • 加載
    類加載干了什么呢?通過類的全名來獲取定義該類的二進制字節(jié)流,將字節(jié)流代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中生成代表這個類的java.lang.Class對象作為這個類各種數(shù)據(jù)的訪問入口,這三步。

  • 驗證
    驗證是連接階段的第一步。
    java語言之所以是相對安全的語言,是因為使用純粹的java代碼無法實現(xiàn)如訪問數(shù)組邊界以外的數(shù)據(jù)、將一個對象轉(zhuǎn)型為它并未實現(xiàn)的類型、跳轉(zhuǎn)到不存在的代碼行之類的,編譯器會幫我們拒絕編譯。然而,Class文件并不僅僅是java編譯而來的,可以通過別的途徑也能實現(xiàn),如果虛擬機運行時不驗證的話,所謂的安全就要打折扣了,因此,虛擬機有了驗證作為連接的第一步。

    1. 文件格式驗證
      驗證字節(jié)流是否符合Class文件格式的規(guī)范,并能被當前版本虛擬機處理。
    2. 元數(shù)據(jù)驗證
      對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合java語言規(guī)范的要求,如,這個類是否有父類,這個類是否繼承了不允許被繼承的類,如果不是抽象類,是否實現(xiàn)其父類或接口中要求實現(xiàn)的所有方法等
    3. 字節(jié)碼驗證
      最復雜的一個階段,通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的符合邏輯的。
    4. 符號引用驗證
  • 準備
    準備階段就是正式為類變量分配內(nèi)存并設置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配。這個時候進行的內(nèi)存分配僅包括類變量即靜態(tài)變量,而我們的實例變量時分配在堆中。初始值除了final修飾的外,一般是數(shù)據(jù)類型的零值。如果是final修飾的將直接賦結(jié)果值。

  • 解析
    解析階段就是虛擬機將常量池內(nèi)的符號引用替換為直接引用的一個過程。先理解下符號引用和直接引用的概率。
    符號引用:使用一組符號來描述所引用的目標,與虛擬機內(nèi)存布局無關(guān),引用的目標不一定已經(jīng)加載到內(nèi)存,明確定義在java虛擬機規(guī)范的Class文件中。
    直接引用:直接指向目標的指針、相對偏移量或者一個能間接定位到目標的句柄。與內(nèi)存布局相關(guān),一個引用在不同的虛擬機實例翻譯過來一般也不同,引用的目標已經(jīng)在內(nèi)存中存在。

  • 初始化
    加載類的最后一步。前面的類加載過程除了加載階段用戶通過自定義類加載器參與外,完全是虛擬機主導的,到了初始化階段才是真正執(zhí)行類中定義的java代碼。
    初始過程是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>()的特點如下

    1. <clinit>()方法是由編譯器自動收集類中的所有類變量的復制動作和靜態(tài)代碼塊中的語句合并產(chǎn)生。語句是有先后順序的,如定義在靜態(tài)代碼塊之后的變量,靜態(tài)代碼塊可以進行賦值,但是不能訪問。代碼如下
public class FieldResolution {
    static{
         i=8;
        System.out.println(i);
    }
    static int i;
}

這段代碼編譯時會報Cannot reference a field before it is defined,非法向前引用。而去掉打印語句,發(fā)現(xiàn)程序不會報錯,編譯源碼顯示static int i=8; 我們可以認為編輯器會第一時間尋找到int j 的變量,在靜態(tài)代碼塊中賦值,如果靜態(tài)代碼塊后面初始化過的話,會第二次賦值,這樣以程序在后面的為準。

  1. 由于父類的<clinit>()方法會先執(zhí)行,這就意味著父類中定義的靜態(tài)代碼塊要優(yōu)先于子類的變量賦值操作。
  2. <clinit>()對于類或者接口來說不是必需的,如果一個類沒有靜態(tài)代碼塊也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。
  3. <clinit>()會被多線程環(huán)境下加鎖,同步。
public class DeadLoopClass {
    static{
        //如果沒有if會報Initializer does not complete normally
        if(true){
            System.out.println(Thread.currentThread().getName()+"init...");
            while(true){
            }
        }
    }
}
class TestDemo{
    public static void main(String[] args) {
        Runnable run = new Runnable() {
            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println(Thread.currentThread()+"--start");
                DeadLoopClass deap = new DeadLoopClass();
                System.out.println(Thread.currentThread()+"--over");
            }
        };
        
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t1.start();
        t2.start();
    }
}

運行結(jié)果

Thread[Thread-0,5,main]--start
Thread[Thread-1,5,main]--start
Thread-0init...

類加載器

通過一個類的全限定名來獲取描述此類的二進制字節(jié)流,這個動作放到java虛擬機外部去實現(xiàn),以便讓應用程序自己決定如何去獲取所需要的類。

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name)
                    throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null){
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b,0,b.length);
                } catch (Exception e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        
        Object obj = myLoader.loadClass("com.classloading.ClassLoaderTest");
        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.classloading.ClassLoaderTest);
    }
}

運行結(jié)果

class java.lang.Class
false

我們構(gòu)造了一個簡單的類加載器,加載同一個路徑下的Class文件,然后和系統(tǒng)應用程序類加載器的去比較,發(fā)現(xiàn)是兩個獨立的類,因此,類加載器不同,類也不同。

  1. 雙親委托模型

從java虛擬機角度來講,只存在兩種類加載器:啟動類加載器(Bootstrap ClassLoader),其他類加載器,獨立于虛擬機外部,且全部繼承自java.lang.ClassLoader。


雙親委托模型

雙親委托模型除了頂部的啟動類加載器,其他的都有自己的父加載器。這里類加載器之間的父子關(guān)系一般不會以繼承的關(guān)系來實現(xiàn),而都是使用組合關(guān)系來復用父加載器的代碼。
雙親委托模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委托給父類加載器去完成,每個層次的類加載器都是如此,最終傳遞到了啟動類加載器,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載。
使用雙親委托機制的好處在于,Java類隨著它的類加載器一起具備優(yōu)先級的層次關(guān)系,如Object類,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都會委托給最頂級的類加載器去加載,這樣就不會造成系統(tǒng)中存在多個Object類。

  1. 破壞雙親委托模型
    到目前為止出現(xiàn)過三次大規(guī)模的被破壞情況
    a. 雙親委托模型是在JDK1.2之后引入的,ClassLoader在JDK1.0就存在了,為了向前兼容,在JDK1.2后加入了一個findClass()方法,之前用戶繼承ClassLoader類唯一目的是重寫loadClass方法,后來推薦把自己的類加載邏輯放到findClass方法中。
    b. 模型自身缺陷,雙親委托很好的解決了各個類加載器的基礎(chǔ)類的統(tǒng)一問題,如果基礎(chǔ)類又要調(diào)用回用戶的代碼,這個時候就有問題了。如JNDI服務,JNDI的目的是對資源進行集中管理和查找,它需要調(diào)用由獨立廠商實現(xiàn)并部署在應用程序的ClassPath的JNDI接口提供者的代碼,但啟動類加載器不可能認識這些,為了處理這個問題,引入了線程上下文加載器
    c. 用戶對程序動態(tài)性的追求而導致。比如熱部署啊,代碼熱替換。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容