1. 程式人生 > >java虛擬機器、GC學習筆記

java虛擬機器、GC學習筆記

近期學習java的垃圾回收機制(GC),遂翻閱了《深入理解java虛擬機器》這本書籍,本文相對書中內容進行記錄,也算是學習筆記了,由於水平有限,對於本文出現的問題和錯誤,還望大家即時指出和交流!

一、執行時資料區分類如下圖:(圖片出自《深入理解java虛擬機器》)

1、方法區是各個執行緒共享的記憶體區域,主要儲存的是java虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼和資料

java虛擬機器規範把它描述成堆的一個邏輯部分,但是他有一個別名叫做Non-Heap(非堆),主要是為了與java堆區分開來。根據java虛擬機器規範的規定,當方法區無法滿足記憶體分配需求時將丟擲OutOfMemoryError異常。

2、堆是各個執行緒共享的記憶體區域,此區域存放的是物件的例項,java堆是垃圾回收機制的主要區域,堆中可分為新生代和老年代,在細緻一點的可分為Eden空間、From Survivor空間、To Survivor空間等,java堆可以處於物理上不連續的空間,只要邏輯上連續就可以了。單堆中沒有記憶體滿足例項分配事務,將丟擲OutOfMemoryError異常。

3、虛擬機器棧是執行緒私有的,它的生命週期與執行緒相同,它描述的是java方法執行的記憶體模型,對這個區域規定了兩種異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常;如果虛擬機器棧可以動態擴充套件(當前大部分的Java虛擬機器都可動態擴充套件,只不過Java虛擬機器規範中也允許固定長度的虛擬機器棧),如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。

4、本地方法棧(Native Method Stack)與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。 在虛擬機器規範中對本地方法棧中方法使用的語言、 使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。 甚至有的虛擬機器(譬如Sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。 與虛擬機器棧一樣,本地方法棧區域也會拋StackOverflowErrorOutOfMemoryError異常。

5、程式計數器是較小的一塊記憶體空間,它是執行緒私有的,每個執行緒都會分配一個執行緒計時器,用來表示當前執行緒執行的位元組碼的行號指示器。在多執行緒中,一個執行緒執行的時候釋放鎖,另一個執行緒執行完,再回來執行前面執行緒的時候,就是通過程式計時器來獲取繼續執行的位置

6、執行時常量池是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放

二、什麼時候需要進行GC?

當jvm無法為新的物件分配空間是會觸發Minor GC,比如Eden佔滿時,所以新物件分配的頻率越高,那麼Minor GC的頻率就會越高,從年輕代指向老年帶的引用都是被忽略的而從老年代指向年輕代的引用都被認為是GC Root,Minor GC都會引起全線停頓(Stop The World),Eden區的物件基本都是垃圾。

當從年輕代升入老年代而老年代的空間不足時會觸發Full GC(Major GC),所以很多時候Minor GC都會引起Full GC,PG空間滿時也會引起Full GC

三、對什麼物件進行GC?

可達性分析演算法:判斷物件是否存活,基本的思路:通過一系列GC Roots物件作為起點,從這些起點開始向下搜尋,搜尋過的路徑成為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何的引用鏈相連時,則說明此物件為不可達,可以被回收

圖中Object5、6、7雖然相互有關聯,但是它們到GC Roots是不可達的,所以可以被回收。

在java虛擬機器中可以被作為GC Roots的物件包括以下幾種:

虛擬機器棧(棧幀中的本地變量表)中引用的物件、方法區中類靜態屬性引用的物件、方法區中常量引用的物件、本地方法棧中JNI引用的物件

四、GC的具體內容?

垃圾回收演算法:

標記清除演算法:

最基礎的收集演算法是“標記-清除”(Mark-Sweep)演算法,如同它的名字一樣,演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件,它的標記過程其實在前一節講述物件標記判定時已經介紹過了。 之所以說它是最基礎的收集演算法,是因為後續的收集演算法都是基於這種思路並對其不足進行改進而得到的。 它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。  

複製演算法,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。 當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉,優點:沒有記憶體碎片情況,實現簡單,執行效率高,缺點:記憶體將縮小為原來的一辦,成本太大。這種演算法主要回收新生代,將新生代的記憶體分成一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[1]。當回收時,將Eden和Survivor中還存活著的物件一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間,HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),只有10%的記憶體會被“浪費”。

標記整理演算法,標記過程與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

分代收集演算法,根據物件存活週期的不同將記憶體劃分為幾塊。 一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。 在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。 而老年代中因為物件存活率高、 沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”演算法來進行回收。