天天看點

類的加載機制

目錄介紹

  • 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 有什麼不同呢?

1.1 看下建立類加載過程

  • Person p = new Person()請寫一下類的加載過程?
    1).因為new用到了Person.class,是以會先找到Person.class檔案,并加載到記憶體中;
    2).執行該類中的static代碼塊,如果有的話,給Person.class類進行初始化;
    3).在堆記憶體中開辟空間配置設定記憶體位址;
    4).在堆記憶體中建立對象的特有屬性,并進行預設初始化;
    5).對屬性進行顯示初始化;
    6).對對象進行構造代碼塊初始化;
    7).對對象進行與之對應的構造函數進行初始化;
    8).将記憶體位址付給棧記憶體中的p變量           

  • 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>

      方法,把對象按照程式員的意願進行初始化,這樣一個真正可用的對象才算完全産生出來。

  • 在 Hotspot 虛拟機中,對象在記憶體中的布局可以分為3快區域:對象頭、執行個體資料和對齊填充。
  • Hotspot虛拟機的對象頭包括兩部分資訊,第一部分用于存儲對象自身的自身運作時資料(哈希嗎、GC分代年齡、鎖狀态标志等等),另一部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是那個類的執行個體。
  • 執行個體資料部分是對象真正存儲的有效資訊,也是在程式中所定義的各種類型的字段内容。
  • 對齊填充部分不是必然存在的,也沒有什麼特别的含義,僅僅起占位作用。
  • 因為Hotspot虛拟機的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,換句話說就是對象的大小必須是8位元組的整數倍。而對象頭部分正好是8位元組的倍數(1倍或2倍),是以,當對象執行個體資料部分沒有對齊時,就需要通過對齊填充來補全。

  • Java 虛拟機在執行 Java 程式的過程中會把它管理的記憶體劃分成若幹個不同的資料區域。
  • 這些組成部分一些事線程私有的,其他的則是線程共享的。
    • 線程私有的:
      • 程式計數器
      • 虛拟機棧
      • 本地方法棧
    • 線程共享的:
      • Java堆
      • 方法區
      • 運作時常量池
      • 直接記憶體

  • 程式計數器:是一個資料結構,用于儲存目前正常執行的程式的記憶體位址。Java虛拟機的多線程就是通過線程輪流切換并配置設定處理器時間來實作的,為了線程切換後能恢複到正确的位置,每條線程都需要一個獨立的程式計數器,互不影響,該區域為“線程私有”。
  • 程式計數器是一塊較小的記憶體空間,可以看作是目前線程所執行的位元組碼的行号訓示器。位元組碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等功能都需要依賴這個計數器來完。
  • 程式計數器主要有兩個作用:
    • 1.位元組碼解釋器通過改變程式計數器來依次讀取指令,進而實作代碼的流程控制,如:順序執行、選擇、循環、異常處理。
    • 2.在多線程的情況下,程式計數器用于記錄目前線程執行的位置,進而當線程被切換回來的時候能夠知道該線程上次運作到哪兒。
  • 注意:程式計數器是唯一一個不會出現OutOfMemoryError的記憶體區域,它的生命周期随着線程的建立而建立,随着線程的結束而死亡。

  • 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虛拟機棧,而且随着線程的建立而建立,随着線程的死亡而死亡。

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

  • Java堆:所有線程共享的一塊記憶體區域,此記憶體區域的唯一目的就是存放對象執行個體,對象執行個體幾乎都在這配置設定記憶體。在虛拟機啟動時建立。
  • Java 堆是垃圾收集器管理的主要區域,是以也被稱作GC堆(Garbage Collected Heap).從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,是以Java堆還可以細分為:新生代和老年代:在細緻一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收記憶體,或者更快地配置設定記憶體。
  • 在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆記憶體空間,而元空間使用的是實體記憶體,直接受到本機的實體記憶體限制)。

  • 方法區:各個線程共享的區域,儲存虛拟機加載的類資訊,常量,靜态變量,編譯後的代碼。
    • 雖然Java虛拟機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個别名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
  • 相對而言,垃圾收集行為在這個區域是比較少出現的,但并非資料進入方法區後就“永久存在”了。如何了解這句話?

  • 運作時常量池:代表運作時每個class檔案中的常量表。包括幾種常量:編譯時的數字常量、方法或者域的引用。
    • 。Class 檔案中包括類的版本、字段、方法、接口等描述資訊
  • 既然運作時常量池時方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會抛出 OutOfMemoryError 異常。JDK1.7及之後版本的 JVM已經将運作時常量池從方法區中移了出來,在Java堆(Heap)中開辟了一塊區域存放運作時常量池。

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

  • 建立對象就是為了使用對象,我們的Java程式通過棧上的 reference 資料來操作堆上的具體對象。對象的通路方式有虛拟機實作而定
  • 目前主流的通路方式有
    • ①使用句柄
    • ②直接指針
  • 這兩種對象通路方式各有優勢。
    • 使用句柄來通路的最大好處是 reference 中存儲的是穩定的句柄位址,在對象被移動時隻會改變句柄中的執行個體資料指針,而 reference 本身不需要修改。
    • 使用直接指針通路方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

  • 如果使用句柄的話,那麼Java堆中将會劃分出一塊記憶體來作為句柄池,reference 中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊;

  • 如果使用直接指針通路,那麼 Java 堆對像的布局中就必須考慮如何放置通路類型資料的相關資訊,而reference 中存儲的直接就是對象的位址。

  • 思考一下,更多可以參考我的部落格:

  • Java 的自動記憶體管理主要是針對對象記憶體的回收和對象記憶體的配置設定。同時,Java 自動記憶體管理最核心的功能是 堆 記憶體中對象的配置設定與回收。
  • JDK1.8之前的堆記憶體示意圖:
    • 從上圖可以看出堆記憶體的分為新生代、老年代和永久代。新生代又被進一步分為: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.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 沒有任何引用鍊相連的話,則證明此對象是不可用的。
4.2.3 再談引用
  • 無論是通過引用計數法判斷對象引用數量,還是通過可達性分析法判斷對象的引用鍊是否可達,判定對象的存活都與“引用”有關。
  • JDK1.2以後,Java對引用的概念進行了擴充,将引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)
  • 關于四種引用以及源代碼分析,可以看我的這篇文章: https://blog.csdn.net/m0_37700275/article/details/79820814

  • 即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經曆兩次标記過程;可達性分析法中不可達的對象被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆寫 finalize 方法,或 finalize 方法已經被虛拟機調用過時,虛拟機将這兩種情況視為沒有必要執行。
  • 被判定為需要執行的對象将會被放在一個隊列中進行第二次标記,除非這個對象與引用鍊上的任何一個對象建立關聯,否則就會被真的回收。

  • 方法區主要回收的是無用的類,那麼如何判斷一個類是無用的類的呢?要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是 “無用的類” :
    • 該類所有的執行個體都已經被回收,也就是 Java 堆中不存在該類的任何執行個體。
    • 加載該類的 ClassLoader 已經被回收。
    • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。
  • 虛拟機可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是“可以”,而并不是和對象一樣不使用了就會必然被回收。

  • 1 String 對象的兩種建立方式:
    String str1 = "abcd";
    String str2 = new String("abcd");
    System.out.println(str1==str2);//false           
  • 這兩種不同的建立方法是有差别的【記住:隻要使用new方法,便需要建立新的對象】
    • 第一種方式是在常量池中拿對象
    • 第二種方式是直接在堆記憶體空間建立一個新的對象。

  • 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.關于我的部落格