07.Java類載入問題

瀟湘劍雨發表於2019-01-19

目錄介紹

  • 7.0.0.1 Java記憶體模型裡包含什麼?程式計數器的作用是什麼?常量池的作用是什麼?
  • 7.0.0.2 什麼是類載入器?類載入器工作機制是什麼?類載入器種類?什麼是雙親委派機制?
  • 7.0.0.3 什麼時候發生類初始化?類初始化後對類的做了什麼,載入變數,常量,方法都記憶體那個位置?
  • 7.0.0.4 通過下面一個程式碼案例理解類載入順序?當遇到 類名.變數 載入時,只載入變數所在類嗎?
  • 7.0.0.5 看下面這段程式碼,說一下準備階段和初始化階段常量變化的原理?變數初始化過程?
  • 7.0.0.7 說收垃圾回收機制?為什麼引用計數器判定物件是否回收不可行?有哪些引用型別?
  • 7.0.0.8 談談Java的類載入過程?載入做了什麼?驗證做了什麼?準備做了什麼?解析做了什麼?初始化做了什麼?

好訊息

  • 部落格筆記大彙總【15年10月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計500篇[近100萬字],將會陸續發表到網上,轉載請註明出處,謝謝!
  • 連結地址:https://github.com/yangchong2…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

7.0.0.1 Java記憶體模型裡包含什麼?程式計數器的作用是什麼?常量池的作用是什麼?

  • Java記憶體模型裡包含什麼?

    • JVM會用一段空間來儲存執行程式期間需要用到的資料和相關資訊,這段空間就是執行時資料區(Runtime Data Area),也就是常說的JVM記憶體。JVM會將它所管理的記憶體劃分為執行緒私有資料區和執行緒共享資料區兩大類。
    • 執行緒私有資料區包含:

      • 1.程式計數器:是一個資料結構,用於儲存當前正常執行的程式的記憶體地址。Java虛擬機器的多執行緒就是通過執行緒輪流切換並分配處理器時間來實現的,為了執行緒切換後能恢復到正確的位置,每條執行緒都需要一個獨立的程式計數器,互不影響,該區域為“執行緒私有”。
      • 2.Java虛擬機器棧:執行緒私有的,與執行緒生命週期相同,用於儲存區域性變數表,操作棧,方法返回值。區域性變數表放著基本資料型別,還有物件的引用。
      • 3.本地方法棧:跟虛擬機器棧很像,不過它是為虛擬機器使用到的Native方法服務。
    • 執行緒共享資料區包含:
    • 技術部落格大總結

      • 4.Java堆:所有執行緒共享的一塊記憶體區域,用於存放幾乎所有的物件例項和陣列;是垃圾收集器管理的主要區域,也被稱做“GC堆”;是Java虛擬機器所管理的記憶體中最大的一塊。
      • 5.方法區:各個執行緒共享的區域,儲存虛擬機器載入的類資訊,常量,靜態變數,編譯後的程式碼。
      • 6.執行時常量池:代表執行時每個class檔案中的常量表。包括幾種常量:編譯時的數字常量、方法或者域的引用。
  • 程式計數器的作用是什麼?
  • 常量池的作用是什麼?

7.0.0.2 什麼是類載入器?類載入器工作機制是什麼?類載入器種類?什麼是雙親委派機制?

  • 什麼是類載入器?

    • 負責讀取 Java 位元組程式碼,並轉換成java.lang.Class類的一個例項;
  • 類載入器工作機制是什麼

    • 是虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可被虛擬機器直接使用的Java型別的過程。另外,型別的載入、連線和初始化過程都是在程式執行期完成的,從而通過犧牲一些效能開銷來換取Java程式的高度靈活性。下面介紹類載入每個階段的任務:

      • 載入(Loading):通過類的全限定名來獲取定義此類的二進位制位元組流;將該二進位制位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構,該資料儲存資料結構由虛擬機器實現自行定義;在記憶體中生成一個代表這個類的java.lang.Class物件,它將作為程式訪問方法區中的這些型別資料的外部介面
      • 驗證(Verification):確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,包括檔案格式驗證、後設資料驗證、位元組碼驗證和符號引用驗證
      • 準備(Preparation):為類變數分配記憶體,因為這裡的變數是由方法區分配記憶體的,所以僅包括類變數而不包括例項變數,後者將會在物件例項化時隨著物件一起分配在Java堆中;設定類變數初始值,通常情況下零值
      • 解析(Resolution):虛擬機器將常量池內的符號引用替換為直接引用的過程
      • 初始化(Initialization):是類載入過程的最後一步,會開始真正執行類中定義的Java位元組碼。而之前的類載入過程中,除了在『載入』階段使用者應用程式可通過自定義類載入器參與之外,其餘階段均由虛擬機器主導和控制
  • 類載入器種類?

    • 啟動類載入器,Bootstrap ClassLoader,載入JACA_HOMElib,或者被-Xbootclasspath引數限定的類
    • 擴充套件類載入器,Extension ClassLoader,載入libext,或者被java.ext.dirs系統變數指定的類
    • 應用程式類載入器,Application ClassLoader,載入ClassPath中的類庫
    • 自定義類載入器,通過繼承ClassLoader實現,一般是載入我們的自定義類
    • 技術部落格大總結
  • 什麼是雙親委派機制?

    • 主要是表示類載入器之間的層次關係

      • 前提:除了頂層啟動類載入器外,其餘類載入器都應當有自己的父類載入器,且它們之間關係一般不會以繼承(Inheritance)關係來實現,而是通過組合(Composition)關係來複用父載入器的程式碼。
      • 工作過程:若一個類載入器收到了類載入的請求,它先會把這個請求委派給父類載入器,並向上傳遞,最終請求都傳送到頂層的啟動類載入器中。只有當父載入器反饋自己無法完成這個載入請求時,子載入器才會嘗試自己去載入。

7.0.0.3 什麼時候發生類初始化?類初始化後對類的做了什麼,載入變數,常量,方法都記憶體那個位置?

  • 什麼時候發生類初始化

    • 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java程式碼場景是:使用new關鍵字例項化物件的時候,讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。

      • 呼叫一個型別的靜態方法時(即在位元組碼中執行invokestatic指令)
      • 呼叫一個型別或介面的靜態欄位,或者對這些靜態欄位執行賦值操作時(即在位元組碼中,執行getstatic或者putstatic指令),不過用final修飾的靜態欄位除外,它被初始化為一個編譯時常量表示式
    • 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
    • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
    • 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。
    • 當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項左後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼鎖對應的類沒有進行過初始化時。
  • 類初始化後對類的做了什麼技術部落格大總結

    • 這個階段主要是對類變數初始化,是執行類構造器的過程。
    • 換句話說,只對static修飾的變數或語句進行初始化。
    • 如果初始化一個類的時候,其父類尚未初始化,則優先初始化其父類。
    • 如果同時包含多個靜態變數和靜態程式碼塊,則按照自上而下的順序依次執行。

7.0.0.4 通過下面一個程式碼案例理解類載入順序?當遇到 類名.變數 載入時,只載入變數所在類嗎?

  • 程式碼案例如下所示

    class A{
        public static int value = 134;
        static{
            System.out.println("A");
        }
    }
    
    class B extends  A{
        static{
            System.out.println("B");
        }
    }
    
    
    public class Demo {
       public static void main(String args[]){
           int s = B.value;
           System.out.println(s);
       }
    }
  • a.列印錯誤結果

    A 
    B
    134 
  • b.列印正確結果

    A
    134 
    • 觀察程式碼,發現B.value中的value變數是A類的。所以,幫主在這裡大膽的猜測一下,當遇到 類名.變數 載入時,只載入變數所在類。
  • 如何做才能列印a這種結果呢?

    class A{
        public static int valueA = 134;
        static{
            System.out.println("A");
        }
    }
    
    class B extends  A{
        public static int valueB = 245;
        static{
            System.out.println("B");
        }
    }
    
    public class Demo {
       public static void main(String args[]){
           int s = B.valueB;
           System.out.println(s);
       }
    }
    A
    B
    245 

7.0.0.5 看下面這段程式碼,說一下準備階段和初始化階段常量變化的原理?

  • 看下面這段程式碼

    public static int value1  = 5;
    public static int value2  = 6;
    static{
        value2 = 66;
    }
  • 準備階段和初始化階段常量變化?

    • 結果

      • 在準備階段value1和value2都等於0;
      • 在初始化階段value1和value2分別等於5和66;
  • 變數初始化過程?

    • 所有類變數初始化語句和靜態程式碼塊都會在編譯時被前端編譯器放在收集器裡頭,存放到一個特殊的方法中,這個方法就是<clinit>方法,即類/介面初始化方法,該方法只能在類載入的過程中由JVM呼叫;
    • 編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數;
    • 如果超類還沒有被初始化,那麼優先對超類初始化,但在<clinit>方法內部不會顯示呼叫超類的<clinit>方法,由JVM負責保證一個類的<clinit>方法執行之前,它的超類<clinit>方法已經被執行。
    • JVM必須確保一個類在初始化的過程中,如果是多執行緒需要同時初始化它,僅僅只能允許其中一個執行緒對其執行初始化操作,其餘執行緒必須等待,只有在活動執行緒執行完對類的初始化操作之後,才會通知正在等待的其他執行緒。(所以可以利用靜態內部類實現執行緒安全的單例模式)
    • 如果一個類沒有宣告任何的類變數,也沒有靜態程式碼塊,那麼可以沒有類<clinit>方法;

7.0.0.7 說收垃圾回收機制?為什麼引用計數器判定物件是否回收不可行?

  • 判定物件可回收有兩種方法:

    • 引用計數演算法:

      • 給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的。然而在主流的Java虛擬機器裡未選用引用計數演算法來管理記憶體,主要原因是它難以解決物件之間相互迴圈引用的問題,所以出現了另一種物件存活判定演算法。
    • 可達性分析法:

      • 通過一系列被稱為『GC Roots』的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。其中可作為GC Roots的物件:虛擬機器棧中引用的物件,主要是指棧幀中的本地變數、本地方法棧中Native方法引用的物件、方法區中類靜態屬性引用的物件、方法區中常量引用的物件
  • 回收演算法有以下四種:

    • 分代收集演算法:是當前商業虛擬機器都採用的一種演算法,根據物件存活週期的不同,將Java堆劃分為新生代和老年代,並根據各個年代的特點採用最適當的收集演算法。技術部落格大總結

      • 新生代:大批物件死去,只有少量存活。使用『複製演算法』,只需複製少量存活物件即可。
      • 老年代:物件存活率高。使用『標記—清理演算法』或者『標記—整理演算法』,只需標記較少的回收物件即可。
    • 複製演算法:把可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用盡後,把還存活著的物件『複製』到另外一塊上面,再將這一塊記憶體空間一次清理掉。
    • 標記-清除演算法:首先『標記』出所有需要回收的物件,然後統一『清除』所有被標記的物件。
    • 標記-整理演算法:首先『標記』出所有需要回收的物件,然後進行『整理』,使得存活的物件都向一端移動,最後直接清理掉端邊界以外的記憶體。
  • 垃圾收集演算法分類

    • 標記-清楚演算法(Mark-Sweep)

      • 在標記階段,確定所有要回收的物件,並做標記。清除階段緊隨標記階段,將標記階段確定不可用的物件清除。標記—清除演算法是基礎的收集演算法,有兩個不足:1)標記和清除階段的效率不高;2)清除後回產生大量的不連續空間,這樣當程式需要分配大記憶體物件時,可能無法找到足夠的連續空間。
    • 複製演算法(Copying)

      • 複製演算法是把記憶體分成大小相等的兩塊,每次使用其中一塊,當垃圾回收的時候,把存活的物件複製到另一塊上,然後把這塊記憶體整個清理掉。複製演算法實現簡單,執行效率高,但是由於每次只能使用其中的一半,造成記憶體的利用率不高。現在的JVM 用複製方法收集新生代,由於新生代中大部分物件(98%)都是朝生夕死的,所以會分成1塊大記憶體Eden和兩塊小記憶體Survivor(大概是8:1:1),每次使用1塊大記憶體和1塊小記憶體,當回收時將2塊記憶體中存活的物件賦值到另一塊小記憶體中,然後清理剩下的。
    • 標記—整理演算法(Mark-Compact)

      • 標記—整理演算法和複製演算法一樣,但是標記—整理演算法不是把存活物件複製到另一塊記憶體,而是把存活物件往記憶體的一端移動,然後直接回收邊界以外的記憶體。標記—整理演算法提高了記憶體的利用率,並且它適合在收集物件存活時間較長的老年代。
    • 分代收集(Generational Collection)

      • 分代收集是根據物件的存活時間把記憶體分為新生代和老年代,根據各代物件的存活特點,每個代採用不同的垃圾回收演算法。新生代採用複製演算法,老年代採用標記—整理演算法。
  • 為什麼引用計數器判定物件是否回收不可行?

    • 實現簡單,判定效率高,但不能解決迴圈引用問題,同時計數器的增加和減少帶來額外開銷。
  • 引用型別有哪些種

    • 強引用:預設的引用方式,不會被垃圾回收,JVM寧願丟擲OutOfMemory錯誤也不會回收這種物件。
    • 軟引用(SoftReference):如果一個物件只被軟引用指向,只有記憶體空間不足夠時,垃圾回收器才會回收它;
    • 弱引用(WeakReference):如果一個物件只被弱引用指向,當JVM進行垃圾回收時,無論記憶體是否充足,都會回收該物件。
    • 虛引用(PhantomReference):虛引用和前面的軟引用、弱引用不同,它並不影響物件的生命週期。如果一個物件與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。虛引用通常和ReferenceQueue配合使用。

7.0.0.8 談談Java的類載入過程?載入做了什麼?驗證做了什麼?準備做了什麼?解析做了什麼?初始化做了什麼?

  • Java檔案從編碼完成到最終執行過程

    • 編譯:編譯,即把我們寫好的java檔案,通過javac命令編譯成位元組碼,也就是我們常說的.class檔案。
    • 執行:執行,則是把編譯聲稱的.class檔案交給Java虛擬機器(JVM)執行。
    • 舉個通俗點的例子來說,JVM在執行某段程式碼時,遇到了classA,然而此時記憶體中並沒有classA的相關資訊,於是JVM就會到相應的class檔案中去尋找classA的類資訊,並載入進記憶體中,這就是我們所說的類載入過程。
  • 談談Java的類載入過程?

    • 類載入的過程主要分為三個部分:
    • 載入
    • 連結

      • 而連結又可以細分為三個小部分:
      • 驗證
      • 準備
      • 解析
    • 初始化
  • 載入做了什麼?

    • 載入指的是把class位元組碼檔案從各個來源通過類載入器裝載入記憶體中。

      • 這裡有兩個重點:
      • 位元組碼來源。一般的載入來源包括從本地路徑下編譯生成的.class檔案,從jar包中的.class檔案,從遠端網路,以及動態代理實時編譯
      • 類載入器。一般包括啟動類載入器,擴充套件類載入器,應用類載入器,以及使用者的自定義類載入器。
    • 在載入階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機器需要完成以下3件事情:

      • 通過一個類的全限定名來獲取定義此類的二進位制位元組流(並沒有指明要從一個Class檔案中獲取,可以從其他渠道,譬如:網路、動態生成、資料庫等);
      • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
      • 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口;
    • 載入階段和連線階段(Linking)的部分內容(如一部分位元組碼檔案格式驗證動作)是交叉進行的,載入階段尚未完成,連線階段可能已經開始,但這些夾在載入階段之中進行的動作,仍然屬於連線階段的內容,這兩個階段的開始時間仍然保持著固定的先後順序。
  • 驗證做了什麼?技術部落格大總結

    • 主要是為了保證載入進來的位元組流符合虛擬機器規範,不會造成安全錯誤。
    • 包括對於檔案格式的驗證,比如常量中是否有不被支援的常量?檔案中是否有不規範的或者附加的其他資訊?
    • 對於後設資料的驗證,比如該類是否繼承了被final修飾的類?類中的欄位,方法是否與父類衝突?是否出現了不合理的過載?
    • 對於位元組碼的驗證,保證程式語義的合理性,比如要保證型別轉換的合理性。
    • 對於符號引用的驗證,比如校驗符號引用中通過全限定名是否能夠找到對應的類?校驗符號引用中的訪問性(private,public等)是否可被當前類訪問?
  • 準備做了什麼?

    • 主要是為類變數(注意,不是例項變數)分配記憶體,並且賦予初值。
    • 特別需要注意,初值,不是程式碼中具體寫的初始化的值,而是Java虛擬機器根據不同變數型別的預設初始值。
    • 比如8種基本型別的初值,預設為0;引用型別的初值則為null;常量的初值即為程式碼中設定的值,final static a = 123, 那麼該階段a的初值就是123
  • 解析做了什麼?

    • 將常量池內的符號引用替換為直接引用的過程。
    • 兩個重點:

      • 符號引用。即一個字串,但是這個字串給出了一些能夠唯一性識別一個方法,一個變數,一個類的相關資訊。
      • 直接引用。可以理解為一個記憶體地址,或者一個偏移量。比如類方法,類變數的直接引用是指向方法區的指標;而例項方法,例項變數的直接引用則是從例項的頭指標開始算起到這個例項變數位置的偏移量
    • 舉個例子來說,現在呼叫方法hello(),這個方法的地址是1234567,那麼hello就是符號引用,1234567就是直接引用。
    • 在解析階段,虛擬機器會把所有的類名,方法名,欄位名這些符號引用替換為具體的記憶體地址或偏移量,也就是直接引用。
  • 初始化做了什麼?

    • 這個階段主要是對類變數初始化,是執行類構造器的過程。
    • 換句話說,只對static修飾的變數或語句進行初始化。
    • 如果初始化一個類的時候,其父類尚未初始化,則優先初始化其父類。
    • 如果同時包含多個靜態變數和靜態程式碼塊,則按照自上而下的順序依次執行。

其他介紹

01.關於部落格彙總連結

02.關於我的部落格

相關文章