JVM GC 機制與性能優化

1 背景介紹

與C/C++相比,JAVA並不要求咱們去人爲編寫代碼進行內存回收和垃圾清理。JAVA提供了垃圾回收器(garbage collector)來自動檢測對象的做用域),可自動把再也不被使用的存儲空間釋放掉,也就是說,GC機制能夠有效地防止內存泄露以及內存溢出。前端

JAVA 垃圾回收器的主要任務是java

  • 分配內存
  • 確保被引用對象的內存不被錯誤地回收
  • 回收再也不被引用的對象的內存空間

凡事都有兩面性。垃圾回收器在把程序員從釋放內存的複雜工做中解放出來的同時,爲了實現垃圾回收,garbage collector必須跟蹤內存的使用狀況,釋放沒用的對象,在完成內存的釋放以後還須要處理堆中的碎片, 這樣作一定會增長JVM的負擔。程序員

爲何要了解JAVA的GC機制? 綜上所述,除了做爲一個程序員,精益求精是基本要求以外,深刻了解GC機制讓咱們的代碼更有效率,尤爲是在構建大型程序時,GC直接影響着內存優化和運行速度。算法

2 JAVA 內存區域

瞭解GC機制以前,須要首先搞清楚JAVA程序在執行的時候,內存到底是如何劃分的。編程

私有內存區的區域名稱和相應的特性以下表所示:後端

區域名稱 特性
程序計數器 指示當前程序執行到了哪一行,執行JAVA方法時紀錄正在執行的虛擬機字節碼指令地址;執行本地方法時,計數器值爲undefined
虛擬機棧 用於執行JAVA方法。棧幀存儲局部變量表、操做數棧、動態連接、方法返回地址和一些額外的附加信息。程序執行時棧幀入棧;執行完成後棧幀出棧
本地方法棧 用於執行本地方法,其它和虛擬機棧相似

着重說一下虛擬機棧中的局部變量表,裏面存放了三個信息:數組

  • 各類基本數據類型(boolean、byte、char、short、int、float、long、double)
  • 對象引用(reference)
  • returnAddress地址

這個returnAddress和程序計數器有什麼區別?前者是指示JVM的指令執行到哪一行,後者則是你的代碼執行到哪一行。緩存

私有內存區伴隨着線程的產生而產生,一旦線程停止,私有內存區也會自動消除,所以咱們在本文中討論的內存回收主要是針對共享內存區。下面介紹一下共享內存區。安全

區域名稱 特性
JAVA堆 JAVA虛擬機管理的內存中最大的一塊,全部線程共享,幾乎全部的對象實例和數組都在這類分配內存。GC主要就是在JAVA堆中進行的
方法區 用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。可是已經被最新的 JVM 取消了。如今,被加載的類做爲元數據加載到底層操做系統的本地內存區。

3 JAVA堆

既然GC主要發生在堆內存中,這部分咱們會對堆內存進行比較詳細的描述。性能優化

堆內存是由存活和死亡的對象組成的。存活的對象是應用能夠訪問的,不會被垃圾回收。死亡的對象是應用不可訪問尚且尚未被垃圾收集器回收掉的對象。一直到垃圾收集器把這些對象回收掉以前,他們會一直佔據堆內存空間。堆是應用程序在運行期請求操做系統分配給本身的向高地址擴展的數據結構,是不連續的內存區域。用一句話總結堆的做用:程序運行時動態申請某個大小的內存空間。 

新生代:剛剛新建的對象在Eden中,經歷一次Minor GC,Eden中的存活對象就會被移動到第一塊survivor space S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和S0中的存活對象又會被複制送入第二塊survivor space S1。S0和Eden被清空,而後下一輪S0與S1交換角色,如此循環往復。若是對象的複製次數達到16次,該對象就會被送到老年代中。

至於爲何新生代要分出兩個survivor區,在個人另外一篇博客中有詳細介紹爲何新生代內存須要有兩個Survivor區

老年代:若是某個對象經歷了幾回垃圾回收以後還存活,就會被存放到老年代中。老年代的空間通常比新生代大。

GC名稱 介紹
Minor GC 發生在新生代,頻率高,速度快(大部分對象活不過一次Minor GC)
Major GC 發生在老年代,速度慢
Full GC 清理整個堆空間

不過實際運行中,Major GC會伴隨至少一次 Minor GC,所以也沒必要過多糾結於究竟是哪一種GC(在有些資料中看到把full GC和Minor GC等價的說法)。

那麼,當咱們建立一個對象後,它會被放在堆內存的哪一個部分呢? 

若是Major GC以後仍是老年代不足,那蒼天也救不了了。。。。JVM會拋出內存不足的異常。

4 垃圾回收機制

JAVA 並無給咱們提供明確的代碼來標註一塊內存並將其回收。或許你會說,咱們能夠將相關對象設爲 null 或者用 System.gc()。然而,後者將會嚴重影響代碼的性能,由於通常每一次顯式的調用 system.gc() 都會中止全部響應,去檢查內存中是否有可回收的對象。這會對程序的正常運行形成極大的威脅。另外,調用該方法並不能保證 JVM 當即進行垃圾回收,僅僅是通知 JVM 要進行垃圾回收了,具體回收與否徹底由 JVM 決定。這樣作是費力不討好。

垃圾回收器是利用有向圖來記錄和管理內存中的全部對象,經過該有向圖,就能夠識別哪些對象「可達」,哪些對象「不可達」,「不可達」的對象就是能夠被回收的。這裏舉一個很簡單的例子來講明這個原理:

public class Test{
  public static void main(String[] a){
     Integer n1=new Integer(9);
     Integer n2=new Integer(3);
     n2=n1;
     // other codes
  }
}

 

如上圖所示,垃圾回收器在遍歷有向圖時,資源2所佔的內存不可達,垃圾回收器就會回收該塊內存空間。

4.1 垃圾回收算法概述

追蹤回收算法(tracing collector) 
從根結點開始遍歷對象的應用圖。同時標記遍歷到的對象。遍歷完成後,沒有被標記的對象就是目前未被引用,能夠被回收。

壓縮回收算法(Compacting Collector) 
把堆中活動的對象集中移動到堆的一端,就會在堆的另外一端流出很大的空閒區域。這種處理簡化了消除碎片的工做,但可能帶來性能的損失。

複製回收算法(Coping Collector) 
把堆均分紅兩個大小相同的區域,只使用其中的一個區域,直到該區域消耗完。此時垃圾回收器終端程序的執行,經過遍歷把全部活動的對象複製到另外一個區域,複製過程當中它們是緊挨着佈置的,這樣也能夠達到消除內存碎片的目的。複製結束後程序會繼續運行,直到該區域被用完。 
可是,這種方法有兩個缺陷:

  1. 對於指定大小的堆,須要兩倍大小的內存空間,
  2. 須要中斷正在執行的程序,下降了執行效率

按代回收算法(Generational Collector) 
爲何要按代進行回收?這是由於不一樣對象生命週期不一樣,每次回收都要遍歷全部存活對象,對於整個堆內存進行回收無疑浪費了大量時間,對症下藥能夠提升垃圾回收的效率。主要思路是:把堆分紅若搞個子堆,每一個子堆視爲一代,算法在運行的過程當中優先收集「年幼」的對象,若是某個對象通過屢次回收仍然「存活」,就移動到高一級的堆,減小對其掃描次數。

4.2 垃圾回收器

回收器 概述 年輕代 老年代
串行回收器(serial collector) 客戶端模式的默認回收器,所謂的串行,指的就是單線程回收,回收時將會暫停全部應用線程的執行 參見本文第三部分 serial old回收器標記-清除-合併。標記全部存活對象,從頭遍歷堆,清除全部死亡對象,最後把存活對象移動到堆的前端,堆的後端就空了
並行回收器 服務器模式的默認回收器,利用多個線程進行垃圾回收,充分利用CPU,回收期間暫停全部應用線程 Parallel Scavenge回收器,關注可控制的吞吐量(吞吐量=代碼運行時間/(代碼運行時間加垃圾回收時間)。吞吐量越大,垃圾回收時間越短,能夠充分利用CPU。可是 parrellel old回收器,多線程,一樣採起「標記-清除-合併」。特色是「吞吐量優先」
CMS回收器 停頓時間最短,分爲如下步驟:1初始標記;2併發標記;3從新標記;4併發清除。優勢是停頓時間短,併發回收,缺點是沒法處理浮動垃圾,並且會致使空間碎片產生 X 適用
G1回收器 新技術,將堆內存劃分爲多個等大的區域,按照每一個區域進行回收。工做過程是1初始標記;2併發標記;3最終標記;4篩選回收。特色是並行併發,分代收集,不會致使空間碎片,也能夠由編程者自主肯定停頓時間上限 適用 適用

附轉載的GC參數彙總以及一個使用實例,轉載來源是 
JVM垃圾回收器工做原理及使用實例介紹 
1. 與串行回收器相關的參數 
-XX:+UseSerialGC:在新生代和老年代使用串行回收器。 
-XX:+SuivivorRatio:設置 eden 區大小和 survivor 區大小的比例。 
-XX:+PretenureSizeThreshold:設置大對象直接進入老年代的閾值。當對象的大小超過這個值時,將直接在老年代分配。 
-XX:MaxTenuringThreshold:設置對象進入老年代的年齡的最大值。每一次 Minor GC 後,對象年齡就加 1。任何大於這個年齡的對象,必定會進入老年代。 
2. 與並行 GC 相關的參數 
-XX:+UseParNewGC: 在新生代使用並行收集器。 
-XX:+UseParallelOldGC: 老年代使用並行回收收集器。 
-XX:ParallelGCThreads:設置用於垃圾回收的線程數。一般狀況下能夠和 CPU 數量相等。但在 CPU 數量比較多的狀況下,設置相對較小的數值也是合理的。 
-XX:MaxGCPauseMills:設置最大垃圾收集停頓時間。它的值是一個大於 0 的整數。收集器在工做時,會調整 Java 堆大小或者其餘一些參數,儘量地把停頓時間控制在 MaxGCPauseMills 之內。 
-XX:GCTimeRatio:設置吞吐量大小,它的值是一個 0-100 之間的整數。假設 GCTimeRatio 的值爲 n,那麼系統將花費不超過 1/(1+n) 的時間用於垃圾收集。 
-XX:+UseAdaptiveSizePolicy:打開自適應 GC 策略。在這種模式下,新生代的大小,eden 和 survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。 
3. 與 CMS 回收器相關的參數 
-XX:+UseConcMarkSweepGC: 新生代使用並行收集器,老年代使用 CMS+串行收集器。 
-XX:+ParallelCMSThreads: 設定 CMS 的線程數量。 
-XX:+CMSInitiatingOccupancyFraction:設置 CMS 收集器在老年代空間被使用多少後觸發,默認爲 68%。 
-XX:+UseFullGCsBeforeCompaction:設定進行多少次 CMS 垃圾回收後,進行一次內存壓縮。 
-XX:+CMSClassUnloadingEnabled:容許對類元數據進行回收。 
-XX:+CMSParallelRemarkEndable:啓用並行重標記。 
-XX:CMSInitatingPermOccupancyFraction:當永久區佔用率達到這一百分比後,啓動 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。 
-XX:UseCMSInitatingOccupancyOnly:表示只在到達閾值的時候,才進行 CMS 回收。 
-XX:+CMSIncrementalMode:使用增量模式,比較適合單 CPU。 
4. 與 G1 回收器相關的參數 
-XX:+UseG1GC:使用 G1 回收器。 
-XX:+UnlockExperimentalVMOptions:容許使用實驗性參數。 
-XX:+MaxGCPauseMills:設置最大垃圾收集停頓時間。 
-XX:+GCPauseIntervalMills:設置停頓間隔時間。 
5. 其餘參數 
-XX:+DisableExplicitGC: 禁用顯示 GC。

經常使用參數以下

調優實例

import java.util.HashMap;


public class GCTimeTest {
 static HashMap map = new HashMap();

 public static void main(String[] args){
 long begintime = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 if(map.size()*512/1024/1024>=400){
 map.clear();//保護內存不溢出
 System.out.println("clean map");
 }
 byte[] b1;
 for(int j=0;j<100;j++){
 b1 = new byte[512];
 map.put(System.nanoTime(), b1);//不斷消耗內存
 }
 }
 long endtime = System.currentTimeMillis();
 System.out.println(endtime-begintime);
 }
}

經過上面的代碼運行 1 萬次循環,每次分配 512*100B 空間,採用不一樣的垃圾回收器,輸出程序運行所消耗的時間。 
使用參數-Xmx512M -Xms512M -XX:+UseParNewGC 運行代碼,輸出以下: 
clean map 8565 
cost time=1655 
使用參數-Xmx512M -Xms512M -XX:+UseParallelOldGC –XX:ParallelGCThreads=8 運行代碼,輸出以下: 
clean map 8798 
cost time=1998

5 JAVA性能優化

大多說針對內存的調優,都是針對於特定狀況的。可是實際中,調優很難與JAVA運行動態特性的實際狀況和工做負載保持一致。也就是說,幾乎不可能經過單純的調優來達到消除GC的目的。

真正影響JAVA程序性能的,就是碎片化。碎片是JAVA堆內存中的空閒空間,多是TLAB剩餘空間,也多是被釋放掉的具備較長生命週期的小對象佔用的空間。

下面是一些在實際寫程序的過程當中應該注意的點,養成這些習慣能夠在必定程度上減小內存的無謂消耗,進一步就能夠減小由於內存不足致使GC不斷。相似的這種經驗能夠多積累交流:

  1. 減小new對象。每次new對象以後,都要開闢新的內存空間。這些對象不被引用以後,還要回收掉。所以,若是最大限度地合理重用對象,或者使用基本數據類型替代對象,都有助於節省內存;
  2. 多使用局部變量,減小使用靜態變量。局部變量被建立在棧中,存取速度快。靜態變量則是在堆內存;
  3. 避免使用finalize,該方法會給GC增添很大的負擔;
  4. 若是是單線程,儘可能使用非多線程安全的,由於線程安全來自於同步機制,同步機制會下降性能。例如,單線程程序,能使用HashMap,就不要用HashTable。同理,儘可能減小使用synchronized
  5. 用移位符號替代乘除號。eg:a*8應該寫做a<<3
  6. 對於常常反覆使用的對象使用緩存;
  7. 儘可能使用基本類型而不是包裝類型,儘可能使用一維數組而不是二維數組;
  8. 儘可能使用final修飾符,final表示不可修改,訪問效率高
  9. 單線程狀況下(或者是針對於局部變量),字符串儘可能使用StringBuilder,比StringBuffer要快;
  10. String爲何慢?由於String 是不可變的對象, 所以在每次對 String 類型進行改變的時候其實都等同於生成了一個新的 String 對象,而後將指針指向新的 String 對象。若是不能保證線程安全,儘可能使用StringBuffer來鏈接字符串。這裏須要注意的是,StringBuffer的默認緩存容量是16個字符,若是超過16,apend方法調用私有的expandCapacity()方法,來保證足夠的緩存容量。所以,若是能夠預設StringBuffer的容量,避免append再去擴展容量。若是能夠保證線程安全,就是用StringBuilder。示例下面兩個示例·:

示例一:

StringBuffer st = new StringBuffer(50);
st.append("let us cook");
st.append(" ");
st.append("a matcha cake for our dinner");
String s = st.toString();

示例二:

public String toString() {
    return new StringBuilder().append("[").append(name).append("]")
                .append("[").append(Message).append("]")
                .append("[").append(salary).append("]").toString();
    }
相關文章
相關標籤/搜索