1. 程式人生 > 程式設計 >JVM效能優化--Java的垃圾回收機制

JVM效能優化--Java的垃圾回收機制

一、Java記憶體結構

file

1、Java堆(Java Heap)

  java堆是java虛擬機器器所管理的記憶體中最大的一塊,是被所有執行緒共享的一塊記憶體區域,在虛擬機器器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,這一點在Java虛擬機器器規範中的描述是:所有的物件例項以及陣列都要在堆上分配。

  java堆是垃圾收集器管理的主要區域,因此也被成為“GC堆”(Garbage Collected Heap)。從記憶體回收角度來看java堆可分為:新生代和老生代。從記憶體分配的角度看,執行緒共享的Java堆中可能劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。無論怎麼劃分,都與存放內容無關,無論哪個區域,儲存的都是物件例項,進一步的劃分都是為了更好的回收記憶體,或者更快的分配記憶體。

  根據Java虛擬機器器規範的規定,java堆可以處於物理上不連續的記憶體空間中。當前主流的虛擬機器器都是可擴充套件的(通過 -Xmx 和 -Xms 控制)。如果堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError異常。

2、Java虛擬機器器棧(Java Virtual Machine Stacks)

  java虛擬機器器也是執行緒私有的,它的生命週期和執行緒相同。虛擬機器器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。

  咱們常說的堆記憶體、棧記憶體中,棧記憶體指的就是虛擬機器器棧。區域性變量表存放了編譯期可知的各種基本資料型別(8個基本資料型別)、物件引用(地址指標)、returnAddress型別。

  區域性變量表所需的記憶體空間在編譯期間完成分配。在執行期間不會改變區域性變量表的大小。

  這個區域規定了兩種異常狀態:如果執行緒請求的棧深度大於虛擬機器器所允許的深度,則丟擲StackOverflowError異常;如果虛擬機器器棧可以動態擴充套件,在擴充套件是無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。

3、本地方法棧(Native Method Stack)

  本地方法棧與虛擬機器器棧所發揮作用非常相似,它們之間的區別不過是虛擬機器器棧為虛擬機器器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器器使用到的native方法服務。本地方法棧也是丟擲兩個異常。

4、方法區(Method Area)

  方法區與java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。它有個別命叫Non-Heap(非堆)。當方法區無法滿足記憶體分配需求時,丟擲OutOfMemoryError異常。

5、直接記憶體(Direct Memory)

 直接記憶體不是虛擬機器器執行時資料區的一部分,也不是java虛擬機器器規範中定義的記憶體區域。但這部分割槽域也唄頻繁使用,而且也可能導致OutOfMemoryError異常在JDK1.4中新加入的NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。

6、執行時常量池(Runtime Constant Pool)

 執行時常量池是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在載入後進入方法區的執行時常量池中存放。

7、程式計數器(Program Counter Register)

程式計數器是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。由於Java虛擬機器器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,一個處理器都只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都有一個獨立的程式計數器,各個執行緒之間計數器互不影響,獨立儲存。稱之為“執行緒私有”的記憶體。程式計數器記憶體區域是虛擬機器器中唯一沒有規定OutOfMemoryError情況的區域。

8、執行引擎

虛擬機器器核心的元件就是執行引擎,它負責執行虛擬機器器的位元組碼,一般戶先進行編譯成機器碼後執行。

9、垃圾收集系統

垃圾收集系統是Java的核心,也是不可少的,Java有一套自己進行垃圾清理的機制,開發人員無需手工清理

二、垃圾回收機制演演算法分析

1、什麼是垃圾回收機制

不定時去堆記憶體中清理不可達物件。不可達的物件並不會馬上就會直接回收, 垃圾收集器在一個Java程式中的執行是自動的,不能強制執行,即使程式設計師能明確地判斷出有一塊記憶體已經無用了,是應該回收的,程式設計師也不能強制垃圾收集器回收該記憶體塊。程式設計師唯一能做的就是通過呼叫System.gc 方法來"建議"執行垃圾收集器,但其是否可以執行,什麼時候執行卻都是不可知的。這也是垃圾收集器的最主要的缺點。

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        test = null;
        System.gc(); // 手動回收垃圾
    }

    @Override
    protected void finalize() throws Throwable {
        // gc回收垃圾之前呼叫
        System.out.println("垃圾回收機制...");
    }
}複製程式碼

2、finalize方法作用

Java技術使用finalize()方法在垃圾收集器將物件從記憶體中清除出去前,做必要的清理工作。這個方法是由垃圾收集器在確定這個物件沒有被引用時對這個物件呼叫的。它是在Object類中定義的,因此所有的類都繼承了它。子類覆蓋finalize()方法以整理系統資源或者執行其他清理工作。finalize()方法是在垃圾收集器刪除物件之前對這個物件呼叫的。

3、新生代與老年代

Java 中的堆是 JVM 所管理的最大的一塊記憶體空間,主要用於存放各種類的例項物件。在 Java 中,堆被劃分成兩個不同的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分為三個區域:Eden、From Survivor、To Survivor。

這樣劃分的目的是為了使 JVM 能夠更好的管理堆記憶體中的物件,包括記憶體的分配以及回收。

堆的記憶體模型大致為:

file

預設的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過引數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。其中,新生代 ( Young ) 被細分為 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名為 from 和 to,以示區分。

預設的,Eden : from : to = 8 : 1 : 1 ( 可以通過引數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

根據垃圾回收機制的不同,Java堆有可能擁有不同的結構,最為常見的就是將整個Java堆分為

新生代和老年代。其中新生帶存放新生的物件或者年齡不大的物件,老年代則存放老年物件。

新生代分為den區、s0區、s1區,s0和s1也被稱為from和to區域,他們是兩塊大小相等並且可以互相角色的空間。

絕大多數情況下,物件首先分配在eden區,在新生代回收後,如果物件還存活,則進入s0或s1區,之後每經過一次

新生代回收,如果物件存活則它的年齡就加1,物件達到一定的年齡後,則進入老年代。

三、如何判斷物件是否存活

1、引用計數法

概念引用計數法就是如果一個物件沒有被任何引用指向,則可視之為垃圾。這種方法的缺點就是不能檢測到環的存在。首先需要宣告,至少主流的Java虛擬機器器裡面都沒有選用引用計數演演算法來管理記憶體。 什麼是引用計數演演算法:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器值減1.任何時刻計數器值為0的物件就是不可能再被使用的。那為什麼主流的Java虛擬機器器裡面都沒有選用這種演演算法呢?其中最主要的原因是它很難解決物件之間相互迴圈引用的問題。

2、根搜尋演演算法

概念根搜尋演演算法的基本思路就是通過一系列名為”GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

這個演演算法的基本思想是通過一系列稱為“GC Roots”的物件作為起始點,從這些節點向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈(即GC Roots到物件不可達)時,則證明此物件是不可用的。

那麼問題又來了,如何選取GCRoots物件呢?在Java語言中,可以作為GCRoots的物件包括下面幾種:

  • (1). 虛擬機器器棧(棧幀中的區域性變數區,也叫做區域性變量表)中引用的物件。
  • (2). 方法區中的類靜態屬性引用的物件。
  • (3). 方法區中常量引用的物件。
  • (4). 本地方法棧中JNI(Native方法)引用的物件。

下面給出一個GCRoots的例子,如下圖,為GCRoots的引用鏈。

file

file

user3和user5沒有引用鏈

根搜尋演演算法的基本思路就是通過一系列名為”GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

四、垃圾回收機制策略

1、標記清除演演算法

概念

該演演算法有兩個階段。

  • 1. 標記階段:找到所有可訪問的物件,做個標記
  • 2. 清除階段:遍歷堆,把未被標記的物件回收

應用場景

該演演算法一般應用於老年代,因為老年代的物件生命週期比較長。

優缺點

標記清除演演算法的優點和缺點

  • 1.優點
    • 是可以解決迴圈引用的問題
    • 必要時才回收(記憶體不足時)
  • 2.缺點:
    • 回收時,應用需要掛起,也就是stop the world。
    • 標記和清除的效率不高,尤其是要掃描的物件比較多的時候
    • 會造成記憶體碎片(會導致明明有記憶體空間,但是由於不連續,申請稍微大一些的物件無法做到),

2、複製演演算法

概念

如果jvm使用了coping演演算法,一開始就會將可用記憶體分為兩塊,from域和to域, 每次只是使用from域,to域則空閒著。當from域記憶體不夠了,開始執行GC操作,這個時候,會把from域存活的物件拷貝到to域,然後直接把from域進行記憶體清理。

應用場景

coping演演算法一般是使用在新生代中,因為新生代中的物件一般都是朝生夕死的,存活物件的數量並不多,這樣使用coping演演算法進行拷貝時效率比較高。jvm將Heap 記憶體劃分為新生代與老年代,又將新生代劃分為Eden(伊甸園) 與2塊Survivor Space(倖存者區),然後在Eden –>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實行Copying 演演算法。 不過jvm在應用coping演演算法時,並不是把記憶體按照1:1來劃分的,這樣太浪費記憶體空間了。一般的jvm都是8:1。也即是說,Eden區:From區:To區域的比例是

始終有90%的空間是可以用來建立物件的,而剩下的10%用來存放回收後存活的物件。

file

  • 1、當Eden區滿的時候,會觸發第一次young gc,把還活著的物件拷貝到Survivor From區;當Eden區再次觸發young gc的時候,會掃描Eden區和From區域,對兩個區域進行垃圾回收,經過這次回收後還存活的物件,則直接複製到To區域,並將Eden和From區域清空。
  • 2、當後續Eden又發生young gc的時候,會對Eden和To區域進行垃圾回收,存活的物件複製到From區域,並將Eden和To區域清空。
  • 3、可見部分物件會在From和To區域中複製來複制去,如此交換15次(由JVM引數MaxTenuringThreshold決定,這個引數預設是15),最終如果還是存活,就存入到老年代

注意: 萬一存活物件數量比較多,那麼To域的記憶體可能不夠存放,這個時候會藉助老年代的空間。

優缺點

  • 優點:在存活物件不多的情況下,效能高,能解決記憶體碎片和java垃圾回收演演算法之-標記清除 中導致的引用更新問題。
  • 缺點: 會造成一部分的記憶體浪費。不過可以根據實際情況,將記憶體塊大小比例適當調整;如果存活物件的數量比較大,coping的效能會變得很差。

3、標記壓縮演演算法

標記清除演演算法和標記壓縮演演算法非常相同,但是標記壓縮演演算法在標記清除演演算法之上解決記憶體碎片化

概念

file

壓縮演演算法簡單介紹

  • 任意順序 : 即不考慮原先物件的排列順序,也不考慮物件之間的引用關係,隨意移動物件;
  • 線性順序 : 考慮物件的引用關係,例如a物件引用了b物件,則儘可能將a和b移動到一塊;
  • 滑動順序 : 按照物件原來在堆中的順序滑動到堆的一端。

優缺點

  • 優點:解決記憶體碎片問題,
  • 缺點:壓縮階段,由於移動了可用物件,需要去更新引用。

4、Minor GC和Full GC區別

概念:

 新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因為 Java 物件大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。

 老年代 GC(Major GC  / Full GC):指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裡就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10倍以上。

Minor GC觸發機制:

當年輕代滿時就會觸發Minor GC,這裡的年輕代滿指的是Eden代滿,Survivor滿不會引發GCFull GC觸發機制:

當年老代滿時會引發Full GC,Full GC將會同時回收年輕代、年老代,當永久代滿時也會引發Full GC,會導致Class、Method元資訊的解除安裝其中

Minor GC如下圖所示

虛擬機器器給每個物件定義了一個物件年齡(Age)計數器。如果物件在 Eden 出生並經過第一次 Minor GC 後仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將物件年齡設為 1。物件在 Survivor 區中每熬過一次 Minor GC,年齡就增加 1 歲,當它的年齡增加到一定程度(預設為 15 歲)時,就會被晉升到老年代中。物件晉升老年代的年齡閾值,可以通過引數 -XX:MaxTenuringThreshold (閾值)來設定。file

JVM的永久代中會發生垃圾回收麼?

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細檢視垃圾收集器的輸出資訊,就會發現永久代也是被回收的。這就是為什麼正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到元資料區(注:Java8中已經移除了永久代,新加了一個叫做元資料區的native記憶體區)

5、分代演演算法

概述

這種演演算法,根據物件的存活週期的不同將記憶體劃分成幾塊,新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演演算法。可以用抓重點的思路來理解這個演演算法。新生代物件朝生夕死,物件數量多,只要重點掃描這個區域,那麼就可以大大提高垃圾收集的效率。另外老年代物件儲存久,無需經常掃描老年代,避免掃描導致的開銷。

新生代

在新生代,每次垃圾收集器都發現有大批物件死去,只有少量存活,採用複製演演算法,只需要付出少量存活物件的複製成本就可以完成收集;

老年代

而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須“標記-清除-壓縮”演演算法進行回收。參看java垃圾回收演演算法之-標記_清除壓縮

新建立的物件被分配在新生代,如果物件經過幾次回收後仍然存活,那麼就把這個物件劃分到老年代。

老年代區存放Young區Survivor滿後觸發minor GC後仍然存活的物件,當Eden區滿後會將存活的物件放入Survivor區域,如果Survivor區存不下這些物件,GC收集器就會將這些物件直接存放到Old區中,如果Survivor區中的物件足夠老,也直接存放到Old區中。如果Old區滿了,將會觸發Full GC回收整個堆記憶體。

個人部落格 蝸牛