一道面試題搞懂JVM類加載機制(類被初始化的幾種情況,類文件加載的過程)


有這樣一道面試題:

class Singleton{
    private static Singleton singleton = new Singleton();
    public static int value1;
    public static int value2 = 0;

    private Singleton(){
        value1++;
        value2++;
    }

    public static Singleton getInstance(){
        return singleton;
    }

}

class Singleton2{
    public static int value1;
    public static int value2 = 0;
    private static Singleton2 singleton2 = new Singleton2();

    private Singleton2(){
        value1++;
        value2++;
    }

    public static Singleton2 getInstance2(){
        return singleton2;
    }

}

public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("Singleton1 value1:" + singleton.value1);
        System.out.println("Singleton1 value2:" + singleton.value2);

        Singleton2 singleton2 = Singleton2.getInstance2();
        System.out.println("Singleton2 value1:" + singleton2.value1);
        System.out.println("Singleton2 value2:" + singleton2.value2);
    }

輸出結果如下:

稍后會帶來分析。

一 類加載機制

JVM類加載分為5個過程:加載,驗證,准備,解析,初始化,使用,卸載,如下圖所示: 
這里寫圖片描述

下面來看看加載,驗證,准備,解析,初始化這5個過程的具體動作。

1.1 加載

加載主要是將.class文件(並不一定是.class。可以是ZIP包,網絡中獲取)中的二進制字節流讀入到JVM中。 在加載階段,JVM需要完成3件事: 1)通過類的全限定名獲取該類的二進制字節流;
2)將字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;
3)在內存中生成一個該類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。

1.2 連接

1.2.1 驗證

驗證是連接階段的第一步,主要確保加載進來的字節流符合JVM規范。
驗證階段會完成以下4個階段的檢驗動作:
1)文件格式驗證
2)元數據驗證(是否符合Java語言規范)
3)字節碼驗證(確定程序語義合法,符合邏輯)
4)符號引用驗證(確保下一步的解析能正常執行)

1.2.2 准備

准備是連接階段的第二步,主要為靜態變量在方法區分配內存,並設置默認初始值。

1.2.3 解析

解析是連接階段的第三步,是虛擬機將常量池內的符號引用替換為直接引用的過程。

1.3 初始化

初始化階段是類加載過程的最后一步,主要是根據程序中的賦值語句主動為類變量賦值。
注:
1)當有父類且父類為初始化的時候,先去初始化父類;
2)再進行子類初始化語句。

(假如構造方法是public的,被new之后,也是在靜態變量初始化完成之后才會執行構造方法)

什么時候需要對類進行初始化?

1)使用new該類實例化對象的時候;
2)讀取或設置類靜態字段的時候(但被final修飾的字段,在編譯器時就被放入常量池的靜態字段除外static final);
3)調用類靜態方法的時候;
4)使用反射Class.forName(“xxxx”)對類進行反射調用的時候,該類需要初始化;
5) 初始化一個類的時候,有父類,先初始化父類(注:1. 接口除外,父接口在調用的時候才會被初始化;2.子類引用父類靜態字段,只會引發父類初始化);
6) 被標明為啟動類的類(即包含main()方法的類)要初始化;
7)當使用JDK1.7的動態語言支持時,如果一個java.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

以上情況稱為對一個類進行主動引用,且有且只要以上幾種情況需要對類進行初始化。

再回過頭來分析一開始的面試題:

Singleton輸出結果:1 0

原因:

1 首先執行main中的Singleton singleton = Singleton.getInstance(); 
2 類的加載:加載類Singleton 
3 類的驗證 
4 類的准備:為靜態變量分配內存,設置默認值。這里為singleton(引用類型)設置為null,value1,value2(基本數據類型)設置默認值0 
5 類的初始化(按照賦值語句進行修改): 
  執行private static Singleton singleton = new Singleton(); 
  執行Singleton的構造器:value1++;value2++; 此時value1,value2均等於1 
  執行 
    public static int value1; 
    public static int value2 = 0; 
  此時value1=1,value2=0

Singleton2輸出結果:1 1 

原因:

1 首先執行main中的Singleton2 singleton2 = Singleton2.getInstance2(); 
2 類的加載:加載類Singleton2 
3 類的驗證 
4 類的准備:為靜態變量分配內存,設置默認值。這里為value1,value2(基本數據類型)設置默認值0,singleton2(引用類型)設置為null, 
5 類的初始化(按照賦值語句進行修改): 
  執行 
  public static int value2 = 0; 
  此時value2=0(value1不變,依然是0); 
  執行 
  private static Singleton singleton = new Singleton(); 
  執行Singleton2的構造器:value1++;value2++; 
  此時value1,value2均等於1,即為最后結果

二 類加載器

類加載器實現的功能是即為加載階段獲取二進制字節流的時候。

JVM提供了以下3種系統的類加載器:

  •  啟動類加載器(Bootstrap ClassLoader):最頂層的類加載器,負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
  •  擴展類加載器(Extension ClassLoader):負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。
  •  應用程序類加載器(Application ClassLoader):也叫做系統類加載器,可以通過getSystemClassLoader()獲取,負責加載用戶路徑(classpath)上的類庫。如果沒有自定義類加載器,一般這個就是默認的類加載器。

類加載器之間的層次關系如下:

照片來源:http://www.importnew.com/25295.html

類加載器之間的這種層次關系叫做雙親委派模型。
雙親委派模型要求除了頂層的啟動類加載器(Bootstrap ClassLoader)外,其余的類加載器都應當有自己的父類加載器。這里的類加載器之間的父子關系一般不是以繼承關系實現的,而是用組合實現的。

雙親委派模型的工作過程

如果一個類接受到類加載請求,他自己不會去加載這個請求,而是將這個類加載請求委派給父類加載器,這樣一層一層傳送,直到到達啟動類加載器(Bootstrap ClassLoader)。 
只有當父類加載器無法加載這個請求時,子加載器才會嘗試自己去加載。

破壞雙親委派模型

雙親委派模型很好的解決了各個類加載器加載基礎類的統一性問題。即越基礎的類由越上層的加載器進行加載。
若加載的基礎類中需要回調用戶代碼,而這時頂層的類加載器無法識別這些用戶代碼,怎么辦呢?這時就需要破壞雙親委派模型了。
下面介紹兩個例子來講解破壞雙親委派模型的過程。

JNDI破壞雙親委派模型

JNDI是Java標准服務,它的代碼由啟動類加載器去加載。但是JNDI需要回調獨立廠商實現的代碼,而類加載器無法識別這些回調代碼(SPI)。
為了解決這個問題,引入了一個線程上下文類加載器。 可通過Thread.setContextClassLoader()設置。
利用線程上下文類加載器去加載所需要的SPI代碼,即父類加載器請求子類加載器去完成類加載的過程,而破壞了雙親委派模型。

Spring破壞雙親委派模型

Spring要對用戶程序進行組織和管理,而用戶程序一般放在WEB-INF目錄下,由WebAppClassLoader類加載器加載,而Spring由Common類加載器或Shared類加載器加載。
那么Spring是如何訪問WEB-INF下的用戶程序呢?
使用線程上下文類加載器。 Spring加載類所用的classLoader都是通過Thread.currentThread().getContextClassLoader()獲取的。當線程創建時會默認創建一個AppClassLoader類加載器(對應Tomcat中的WebAppclassLoader類加載器): setContextClassLoader(AppClassLoader)。
利用這個來加載用戶程序。即任何一個線程都可通過getContextClassLoader()獲取到WebAppclassLoader。

三 附上Tomcat類加載架構:


這里寫圖片描述

Tomcat目錄下有4組目錄:

/common目錄下:類庫可以被Tomcat和Web應用程序共同使用;由 Common ClassLoader類加載器加載目錄下的類庫;
/server目錄:類庫只能被Tomcat可見;由 Catalina ClassLoader類加載器加載目錄下的類庫;
/shared目錄:類庫對所有Web應用程序可見,但對Tomcat不可見;由 Shared ClassLoader類加載器加載目錄下的類庫;
/WebApp/WEB-INF目錄:僅僅對當前web應用程序可見。由 WebApp ClassLoader類加載器加載目錄下的類庫;
每一個JSP文件對應一個JSP類加載器。


---------------------
原文:https://blog.csdn.net/noaman_wgs/article/details/74489549


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM