類的載入機制

帥氣的碼農發表於2020-07-05

目錄介紹

  • 01.Java物件的建立過程

    • 1.0 看下建立類載入過程
    • 1.1 物件的建立
    • 1.2 物件的記憶體佈局
  • 02.Java記憶體區域

    • 2.0 執行時資料區域
    • 2.1 程式計數器
    • 2.2 虛擬機器棧
    • 2.3 本地方法棧
    • 2.4 Java堆
    • 2.5 方法區
    • 2.6 執行時常量池
    • 2.7 直接記憶體
  • 03.Java物件的訪問定位方式

    • 3.1 控制程式碼
    • 3.2 直接指標
  • 04.Java物件銷燬分析

    • 4.1 JVM記憶體分配與回收
    • 4.2 判斷物件是否死亡
    • 4.3 不可達的物件並非“非死不可”
    • 4.4 如何判斷一個常量是廢棄常量
    • 4.5 如何判斷一個類是無用的類
    • 4.6 GC回收演算法詳解
  • 05.String類和常量池

    • 5.1 String物件的兩種建立方式
    • 5.2 String型別的常量池

好訊息

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

問題思考答疑

  • 說一下建立一個物件,類的載入過程。類資訊,常量,變數,方法分別放到記憶體中哪裡?
  • 對於執行時資料區域,哪些是私有的,哪些是共享的,為什麼要這樣設計?
  • 程式計數器會出現OOM嗎?它的生命週期是怎麼樣的?
  • 本地方法棧和Java虛擬機器棧有什麼區別?本地方法棧在什麼情況下會造成OOM?
  • java堆主要是做什麼作用的?
  • 什麼是類的載入檢查,主要檢查什麼,如何檢查呢?
  • Java物件訪問定位方式有哪些?主要有什麼區別?為什麼說使用指標效率更高?
  • String類可以new嗎?直接new和賦值的內容有什麼區別,分別放在記憶體中什麼地方?
  • 如何判斷物件是否死亡(兩種方法)。如果有不同方法,那麼之間有什麼區別?
  • 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。
  • 如何判斷一個常量是廢棄常量,如何判斷一個類是無用的類?
  • 垃圾收集有哪些演算法,各自的特點?常見的垃圾回收器有那些?
  • HotSpot為什麼要分為新生代和老年代?
  • 介紹一下CMS,G1收集器。Minor Gc和Full GC 有什麼不同呢?

01.Java物件的建立過程

1.1 看下建立類載入過程

  • Person p = new Person()請寫一下類的載入過程?

    1).因為new用到了Person.class,所以會先找到Person.class檔案,並載入到記憶體中;
    2).執行該類中的static程式碼塊,如果有的話,給Person.class類進行初始化;
    3).在堆記憶體中開闢空間分配記憶體地址;
    4).在堆記憶體中建立物件的特有屬性,並進行預設初始化;
    5).對屬性進行顯示初始化;
    6).對物件進行構造程式碼塊初始化;
    7).對物件進行與之對應的建構函式進行初始化;
    8).將記憶體地址付給棧記憶體中的p變數

1.1 物件的建立

  • Java物件的建立過程,我建議最好是能默寫出來,並且要掌握每一步在做什麼。

    • 1.類載入檢查
    • 2.分配記憶體
    • 3.初始化零值
    • 4.設定物件頭
    • 5.執行init方法
  • ①類載入檢查:

    • 虛擬機器遇到一條 new 指令時,首先將去檢查這個指令的引數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被載入過、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。
  • ②分配記憶體:

    • 類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。物件所需的記憶體大小在類載入完成後便可確定,為物件分配空間的任務等同於把一塊確定大小的記憶體從 Java 堆中劃分出來。分配方式“指標碰撞”“空閒列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定
    • 記憶體分配的兩種方式:

      • 選擇以上兩種方式中的哪一種,取決於 Java 堆記憶體是否規整。而 Java 堆記憶體是否規整,取決於 GC 收集器的演算法是”標記-清除”,還是”標記-整理”(”標記-壓縮”),值得注意的是,複製演算法記憶體也是規整的
    • 記憶體分配併發問題

      • 在建立物件的時候有一個很重要的問題,就是執行緒安全,因為在實際開發過程中,建立物件是很頻繁的事情,作為虛擬機器來說,必須要保證執行緒是安全的,通常來講,虛擬機器採用兩種方式來保證執行緒安全:
    • CAS+失敗重試:

      • CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。虛擬機器採用 CAS 配上失敗重試的方式保證更新操作的原子性。
    • TLAB:

      • 為每一個執行緒預先在Eden區分配一塊兒記憶體,JVM在給執行緒中的物件分配記憶體時,首先在TLAB分配,當物件大於TLAB中的剩餘記憶體或TLAB的記憶體已用盡時,再採用上述的CAS進行記憶體分配
  • ③初始化零值:

    • 記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這一步操作保證了物件的例項欄位在 Java 程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
  • ④設定物件頭:

    • 初始化零值完成之後,虛擬機器要對物件進行必要的設定,例如這個物件是那個類的例項、如何才能找到類的後設資料資訊、物件的雜湊嗎、物件的 GC 分代年齡等資訊。
    • 這些資訊存放在物件頭中。 另外,根據虛擬機器當前執行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式。
  • ⑤執行 init 方法:

    • 在上面工作都完成之後,從虛擬機器的視角來看,一個新的物件已經產生了,但從 Java 程式的視角來看,物件建立才剛開始,<init> 方法還沒有執行,所有的欄位都還為零。所以一般來說,執行 new 指令之後會接著執行 <init> 方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全產生出來。

1.2 物件的記憶體佈局

  • 在 Hotspot 虛擬機器中,物件在記憶體中的佈局可以分為3快區域:物件頭例項資料對齊填充
  • Hotspot虛擬機器的物件頭包括兩部分資訊第一部分用於儲存物件自身的自身執行時資料(雜湊嗎、GC分代年齡、鎖狀態標誌等等),另一部分是型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是那個類的例項。
  • 例項資料部分是物件真正儲存的有效資訊,也是在程式中所定義的各種型別的欄位內容。
  • 對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位作用。
  • 因為Hotspot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是物件的大小必須是8位元組的整數倍。而物件頭部分正好是8位元組的倍數(1倍或2倍),因此,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。

02.Java記憶體區域

2.0 執行時資料區域

  • Java 虛擬機器在執行 Java 程式的過程中會把它管理的記憶體劃分成若干個不同的資料區域。
  • 這些組成部分一些事執行緒私有的,其他的則是執行緒共享的。

    • 執行緒私有的:

      • 程式計數器
      • 虛擬機器棧
      • 本地方法棧
    • 執行緒共享的:

      • Java堆
      • 方法區
      • 執行時常量池
      • 直接記憶體
  • image

2.1 程式計數器

  • 程式計數器:是一個資料結構,用於儲存當前正常執行的程式的記憶體地址。Java虛擬機器的多執行緒就是通過執行緒輪流切換並分配處理器時間來實現的,為了執行緒切換後能恢復到正確的位置,每條執行緒都需要一個獨立的程式計數器,互不影響,該區域為“執行緒私有”。
  • 程式計數器是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等功能都需要依賴這個計數器來完。
  • 程式計數器主要有兩個作用:

    • 1.位元組碼直譯器通過改變程式計數器來依次讀取指令,從而實現程式碼的流程控制,如:順序執行、選擇、迴圈、異常處理。
    • 2.在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次執行到哪兒。
  • 注意:程式計數器是唯一一個不會出現OutOfMemoryError的記憶體區域,它的生命週期隨著執行緒的建立而建立,隨著執行緒的結束而死亡。

2.2 虛擬機器棧

  • Java虛擬機器棧:執行緒私有的,與執行緒生命週期相同,用於儲存區域性變數表,操作棧,方法返回值。區域性變數表放著基本資料型別,還有物件的引用。
  • Java 記憶體可以粗糙的區分為堆記憶體(Heap)和棧記憶體(Stack),其中棧就是現在說的虛擬機器棧,或者說是虛擬機器棧中區域性變數表部分。(實際上,Java虛擬機器棧是由一個個棧幀組成,而每個棧幀中都擁有:區域性變數表、運算元棧、動態連結、方法出口資訊。)
  • 區域性變數表主要存放了編譯器可知的各種資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference型別,它不同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制程式碼或其他與此物件相關的位置)。
  • Java 虛擬機器棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。

    • StackOverFlowError: 若Java虛擬機器棧的記憶體大小不允許動態擴充套件,那麼當執行緒請求棧的深度超過當前Java虛擬機器棧的最大深度的時候,就丟擲StackOverFlowError異常。
    • OutOfMemoryError: 若Java虛擬機器棧的記憶體大小允許動態擴充套件,且當執行緒請求棧時記憶體用完了,無法再動態擴充套件了,此時丟擲OutOfMemoryError異常。
  • Java 虛擬機器棧也是執行緒私有的,每個執行緒都有各自的Java虛擬機器棧,而且隨著執行緒的建立而建立,隨著執行緒的死亡而死亡。

2.3 本地方法棧

  • 本地方法棧:跟虛擬機器棧很像, 虛擬機器棧為虛擬機器執行 Java 方法 (也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的 Native 方法服務。 在 HotSpot 虛擬機器中和 Java 虛擬機器棧合二為一。
  • 本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的區域性變數表、運算元棧、動態連結、出口資訊。
  • 方法執行完畢後相應的棧幀也會出棧並釋放記憶體空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種異常。

2.4 Java堆

  • Java堆:所有執行緒共享的一塊記憶體區域,此記憶體區域的唯一目的就是存放物件例項,物件例項幾乎都在這分配記憶體。在虛擬機器啟動時建立。
  • Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC堆(Garbage Collected Heap).從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集演算法,所以Java堆還可以細分為:新生代和老年代:在細緻一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收記憶體,或者更快地分配記憶體。

    • image
  • 在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆記憶體空間,而元空間使用的是實體記憶體,直接受到本機的實體記憶體限制)。

2.5 方法區

  • 方法區:各個執行緒共享的區域,儲存虛擬機器載入的類資訊,常量,靜態變數,編譯後的程式碼。

    • 雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
  • 相對而言,垃圾收集行為在這個區域是比較少出現的,但並非資料進入方法區後就“永久存在”了。如何理解這句話?

2.6 執行時常量池

  • 執行時常量池:代表執行時每個class檔案中的常量表。包括幾種常量:編譯時的數字常量、方法或者域的引用。

    • 。Class 檔案中包括類的版本、欄位、方法、介面等描述資訊
  • 既然執行時常量池時方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會丟擲 OutOfMemoryError 異常。JDK1.7及之後版本的 JVM已經將執行時常量池從方法區中移了出來,在Java堆(Heap)中開闢了一塊區域存放執行時常量池。

2.7 直接記憶體

  • 直接記憶體並不是虛擬機器執行時資料區的一部分,也不是虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。而且也可能導致OutOfMemoryError異常出現。
  • JDK1.4中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel)快取區(Buffer) 的 I/O 方式,它可以直接使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在 Java 堆中的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。這樣就能在一些場景中顯著提高效能,因為避免了在 Java 堆和 Native 堆之間來回複製資料
  • 本機直接記憶體的分配不會收到 Java 堆的限制,但是,既然是記憶體就會受到本機總記憶體大小以及處理器定址空間的限制。

03.Java物件的訪問定位方式

  • 建立物件就是為了使用物件,我們的Java程式通過棧上的 reference 資料來操作堆上的具體物件。物件的訪問方式有虛擬機器實現而定
  • 目前主流的訪問方式有

    • ①使用控制程式碼
    • ②直接指標
  • 這兩種物件訪問方式各有優勢。

    • 使用控制程式碼來訪問的最大好處是 reference 中儲存的是穩定的控制程式碼地址,在物件被移動時只會改變控制程式碼中的例項資料指標,而 reference 本身不需要修改。
    • 使用直接指標訪問方式最大的好處就是速度快,它節省了一次指標定位的時間開銷。

3.1 控制程式碼

  • 如果使用控制程式碼的話,那麼Java堆中將會劃分出一塊記憶體來作為控制程式碼池,reference 中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自的具體地址資訊;
    image

3.2 直接指標

  • 如果使用直接指標訪問,那麼 Java 堆對像的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而reference 中儲存的直接就是物件的地址。
    image

04.Java物件銷燬分析

4.1 JVM記憶體分配與回收

  • Java 的自動記憶體管理主要是針對物件記憶體的回收和物件記憶體的分配。同時,Java 自動記憶體管理最核心的功能是 記憶體中物件的分配與回收。
  • JDK1.8之前的堆記憶體示意圖:

    • image
    • 從上圖可以看出堆記憶體的分為新生代、老年代和永久代。新生代又被進一步分為:Eden 區+Survior1 區+Survior2 區。值得注意的是,在JDK1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆記憶體空間,而元空間使用的是實體記憶體,直接受到本機的實體記憶體限制)。
  • 分代回收演算法

    • 目前主流的垃圾收集器都會採用分代回收演算法,因此需要將堆記憶體分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集演算法。
    • 大多數情況下,物件在新生代中 eden 區分配。當 eden 區沒有足夠空間進行分配時,虛擬機器將發起一次Minor GC。
  • Minor Gc和Full GC 有什麼不同呢?

    • 新生代GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。
    • 老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC經常會伴隨至少一次的Minor GC(並非絕對),Major GC的速度一般會比Minor GC的慢10倍以上。

4.2 判斷物件是否死亡

  • 堆中幾乎放著所有的物件例項,對堆垃圾回收前的第一步就是要判斷那些物件已經死亡(即不能再被任何途徑使用的物件)。

    • image
4.2.1 引用計數法
  • 給物件中新增一個引用計數器,每當有一個地方引用它,計數器就加1;當引用失效,計數器就減1;任何時候計數器為0的物件就是不可能再被使用的。

    • 這個方法實現簡單,效率高,但是目前主流的虛擬機器中並沒有選擇這個演算法來管理記憶體,其最主要的原因是它很難解決物件之間相互迴圈引用的問題。
    • 所謂物件之間的相互引用問題,如下面程式碼所示:除了物件objA和objB相互引用著對方之外,這兩個物件之間再無任何引用。但是他們因為互相引用對方,導致它們的引用計數器都不為0,於是引用計數演算法無法通知 GC 回收器回收他們。
    public class Test {
        Object instance = null;
        public static void main(String[] args) {
            Test objA = new Test();
            Test objB = new Test();
            objA.instance = objB;
            objB.instance = objA;
            objA = null;
            objB = null;
        }
    }
4.2.2 可達性分析演算法
  • 這個演算法的基本思想就是通過一系列的稱為 “GC Roots” 的物件作為起點,從這些節點開始向下搜尋,節點所走過的路徑稱為引用鏈,當一個物件到 GC Roots 沒有任何引用鏈相連的話,則證明此物件是不可用的。

    • image
4.2.3 再談引用
  • 無論是通過引用計數法判斷物件引用數量,還是通過可達性分析法判斷物件的引用鏈是否可達,判定物件的存活都與“引用”有關。
  • JDK1.2以後,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)
  • 關於四種引用以及原始碼分析,可以看我的這篇文章:https://blog.csdn.net/m0_37700275/article/details/79820814

4.3 不可達的物件並非“非死不可”

  • 即使在可達性分析法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑階段”,要真正宣告一個物件死亡,至少要經歷兩次標記過程;可達性分析法中不可達的物件被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行 finalize 方法。當物件沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機器呼叫過時,虛擬機器將這兩種情況視為沒有必要執行。
  • 被判定為需要執行的物件將會被放在一個佇列中進行第二次標記,除非這個物件與引用鏈上的任何一個物件建立關聯,否則就會被真的回收。

4.4 如何判斷一個常量是廢棄常量

4.5 如何判斷一個類是無用的類

  • 方法區主要回收的是無用的類,那麼如何判斷一個類是無用的類的呢?要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是 “無用的類”

    • 該類所有的例項都已經被回收,也就是 Java 堆中不存在該類的任何例項。
    • 載入該類的 ClassLoader 已經被回收。
    • 該類對應的 java.lang.Class 物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
  • 虛擬機器可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是“可以”,而並不是和物件一樣不使用了就會必然被回收。

4.6 GC回收演算法詳解

05.String類和常量池

5.1 String物件的兩種建立方式

  • 1 String 物件的兩種建立方式:

    String str1 = "abcd";
    String str2 = new String("abcd");
    System.out.println(str1==str2);//false
  • 這兩種不同的建立方法是有差別的【記住:只要使用new方法,便需要建立新的物件】

    • 第一種方式是在常量池中拿物件
    • 第二種方式是直接在堆記憶體空間建立一個新的物件。
  • image

5.2 String型別的常量池

  • String 型別的常量池比較特殊。它的主要使用方法有兩種:

    • 直接使用雙引號宣告出來的 String 物件會直接儲存在常量池中。
    • 如果不是用雙引號宣告的 String 物件,可以使用 String 提供的 intern 方String.intern() 是一個 Native 方法,它的作用是:如果執行時常量池中已經包含一個等於此 String 物件內容的字串,則返回常量池中該字串的引用;如果沒有,則在常量池中建立與此 String 內容相同的字串,並返回常量池中建立的字串的引用。
String s1 = new String("yc");
String s2 = s1.intern();
String s3 = "yc";
System.out.println(s2);//yc
System.out.println(s1 == s2);//false,因為一個是堆記憶體中的String物件一個是常量池中的String物件,
System.out.println(s3 == s2);//true,因為兩個都是常量池中的String對

關於其他內容介紹

01.關於部落格彙總連結

02.關於我的部落格


相關文章