Java虛擬機(一)內存管理子系統

(一)內存管理子系統

1.虛擬機內存區域介紹

虛擬機管理的內存區域

  • 程序計數器
    是一塊較小的內存區域,存放記錄字節碼指令的地址(如果執行的是native方法,則爲空),此內存區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。
  • 虛擬機棧
    存放是的棧元素是棧幀,棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。會拋出StackOverflowError異常和OutOfMemoryError異常。
  • 本地方法棧
    與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一

  • 此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。會拋出OutOfMemoryError異常。
  • 方法區
    用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。這區域的內存回收目標主要是針對常量池的回收和對類型的卸載。拋出OutOfMemoryError異常。
    • 運行時常量池
      是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。一般來說,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
  • 直接內存
    並不是虛擬機運行時數據區的一部分,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆中來回複製數據

2.堆上的內存管理過程

(以常用的虛擬機HotSpot和常用的內存區域Java堆爲例,深入探討HotSpot虛擬機在Java堆中對象分配、佈局和訪問的全過程。)

對象的創建

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。
分配方式有兩種:

  • 指針碰撞
    假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離。

  • 空閒列表
    如果Java堆中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。

選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
在爲對象分配內存的時候,涉及到線程安全問題,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。
有兩個解決方案:1.對分配過程進行同步處理2.內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定分配緩衝區。

內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。根據虛擬機當前的運行狀態的不同,如是否啓用偏向鎖等,對象頭會有不同的設置方式。

再接下來就是執行類的構造方法。

對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

  • 對象頭

    • 第一部分
      用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等
    • 第二部分
      類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
  • 實例數據
    對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。

  • 對齊填充
    並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
對象的訪問定位
  • 使用句柄
    Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息,如圖
    句柄訪問
  • 直接指針
    Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。
    直接指針訪問

這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不需要修改。
使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。就本書討論的主要虛擬機Sun HotSpot而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的範圍來看,各種語言和框架使用句柄來訪問的情況也十分常見。

3.垃圾收集器與內存分配策略

1.哪些內存需要回收?
2.什麼時候回收?
3.如何回收?

1.哪些內存需要回收?

程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的,因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就跟隨着回收了。Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的是這部分內存。

2.什麼時候回收

回收Java堆
當對象死亡時,我們可以回收其所佔用的內存
對象死亡判定:

  • 引用計數算法
    給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的。
    侷限性是:不能解決循環引用的問題。
  • 可達算法
    基本思路就是通過一系列的稱爲”GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
    GC Roots:
    虛擬機棧(棧幀中的本地變量表)中引用的對象。
    方法區中類靜態屬性引用的對象。
    方法區中常量引用的對象。
    本地方法棧中JNI(即一般說的Native方法)引用的對象。

回收方法區
廢棄常量和無用的類。

回收廢棄常量與回收Java堆中的對象非常類似,用引用來判定。

判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:
1.該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
2.加載該類的ClassLoader已經被回收。
3.該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

3.如何回收?

垃圾收集算法

  • 標記-清除
  • 複製算法
  • 標記-整理
  • 分代收集
    比較常用。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。

Hotspot的算法實現
如何去發起內存回收

  • 枚舉根節點
  • 安全點
  • 安全區域

內存回收動作的執行者是實現了回收算法的垃圾收集器,通常虛擬機中往往不止有一種GC收集器。

按線程處理方式可分爲單線程,多線程收集器
按收集的內存區域可分爲新生代、老年代的收集器

最普遍的內存分配規則
1.對象優先在Eden分配
2.大對象直接進入老年代
3.長期存活的對象將進入老年代
4.動態對象年齡判定
5.空間分配擔保

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章