Java的性能優化,整理出一篇文章,供以后溫故知新。
JVM GC(垃圾回收機制)
在學習Java GC 之前,我們需要記住一個單詞:stop-the-world 。它會在任何一種GC算法中發生。stop-the-world 意味着JVM因為需要執行GC而停止了應用程序的執行。當stop-the-world 發生時,除GC所需的線程外,所有的線程都進入等待狀態,直到GC任務完成。GC優化很多時候就是減少stop-the-world 的發生。
JVM GC回收哪個區域內的垃圾?
需要注意的是,JVM GC只回收堆區和方法區內的對象。而棧區的數據,在超出作用域后會被JVM自動釋放掉,所以其不在JVM GC的管理范圍內。
JVM GC怎么判斷對象可以被回收了?
· 對象沒有引用
· 作用域發生未捕獲異常
· 程序在作用域正常執行完畢
· 程序執行了System.exit()
· 程序發生意外終止(被殺線程等)
在Java程序中不能顯式的分配和注銷緩存,因為這些事情JVM都幫我們做了,那就是GC。
有些時候我們可以將相關的對象設置成null 來試圖顯示的清除緩存,但是並不是設置為null 就會一定被標記為可回收,有可能會發生逃逸。
將對象設置成null 至少沒有什么壞處,但是使用System.gc() 便不可取了,使用System.gc() 時候並不是馬上執行GC操作,而是會等待一段時間,甚至不執行,而且System.gc() 如果被執行,會觸發Full GC ,這非常影響性能。
JVM GC什么時候執行?
eden區空間不夠存放新對象的時候,執行Minro GC。升到老年代的對象大於老年代剩余空間的時候執行Full GC,或者小於的時候被HandlePromotionFailure 參數強制Full GC 。調優主要是減少 Full GC 的觸發次數,可以通過 NewRatio 控制新生代轉老年代的比例,通過MaxTenuringThreshold 設置對象進入老年代的年齡閥值(后面會介紹到)。
按代的垃圾回收機制
新生代(Young generation):絕大多數最新被創建的對象都會被分配到這里,由於大部分在創建后很快變得不可達,很多對象被創建在新生代,然后“消失”。對象從這個區域“消失”的過程我們稱之為:Minor GC 。
老年代(Old generation):對象沒有變得不可達,並且從新生代周期中存活了下來,會被拷貝到這里。其區域分配的空間要比新生代多。也正由於其相對大的空間,發生在老年代的GC次數要比新生代少得多。對象從老年代中消失的過程,稱之為:Major GC 或者 Full GC。
持久代(Permanent generation)也稱之為 方法區(Method area):用於保存類常量以及字符串常量。注意,這個區域不是用於存儲那些從老年代存活下來的對象,這個區域也可能發生GC。發生在這個區域的GC事件也被算為 Major GC 。只不過在這個區域發生GC的條件非常嚴苛,必須符合以下三種條件才會被回收:
1、所有實例被回收
2、加載該類的ClassLoader 被回收
3、Class 對象無法通過任何途徑訪問(包括反射)
可能我們會有疑問:
如果老年代的對象需要引用新生代的對象,會發生什么呢?
為了解決這個問題,老年代中存在一個 card table ,它是一個512byte大小的塊。所有老年代的對象指向新生代對象的引用都會被記錄在這個表中。當針對新生代執行GC的時候,只需要查詢 card table 來決定是否可以被回收,而不用查詢整個老年代。這個 card table 由一個write barrier 來管理。write barrier給GC帶來了很大的性能提升,雖然由此可能帶來一些開銷,但完全是值得的。
默認的新生代(Young generation)、老年代(Old generation)所占空間比例為 1 : 2 。
新生代空間的構成與邏輯
為了更好的理解GC,我們來學習新生代的構成,它用來保存那些第一次被創建的對象,它被分成三個空間:
· 一個伊甸園空間(Eden)
· 兩個幸存者空間(Fron Survivor、To Survivor)
默認新生代空間的分配:Eden : Fron : To = 8 : 1 : 1
每個空間的執行順序如下:
1、絕大多數剛剛被創建的對象會存放在伊甸園空間(Eden)。
2、在伊甸園空間執行第一次GC(Minor GC)之后,存活的對象被移動到其中一個幸存者空間(Survivor)。
3、此后,每次伊甸園空間執行GC后,存活的對象會被堆積在同一個幸存者空間。
4、當一個幸存者空間飽和,還在存活的對象會被移動到另一個幸存者空間。然后會清空已經飽和的哪個幸存者空間。
5、在以上步驟中重復N次(N = MaxTenuringThreshold(年齡閥值設定,默認15))依然存活的對象,就會被移動到老年代。
從上面的步驟可以發現,兩個幸存者空間,必須有一個是保持空的。如果兩個兩個幸存者空間都有數據,或兩個空間都是空的,那一定是你的系統出現了某種錯誤。
我們需要重點記住的是,對象在剛剛被創建之后,是保存在伊甸園空間的(Eden)。那些長期存活的對象會經由幸存者空間(Survivor)轉存到老年代空間(Old generation)。
也有例外出現,對於一些比較大的對象(需要分配一塊比較大的連續內存空間)則直接進入到老年代。一般在Survivor 空間不足的情況下發生。
老年代空間的構成與邏輯
老年代空間的構成其實很簡單,它不像新生代空間那樣划分為幾個區域,它只有一個區域,里面存儲的對象並不像新生代空間絕大部分都是朝聞道,夕死矣。這里的對象幾乎都是從Survivor 空間中熬過來的,它們絕不會輕易的狗帶。因此,Full GC(Major GC)發生的次數不會有Minor GC 那么頻繁,並且做一次Major GC 的時間比Minor GC 要更長(約10倍)。
JVM GC 算法講解
1、根搜索算法
根搜索算法是從離散數學中的圖論引入的,程序把所有引用關系看作一張圖,從一個節點GC ROOT 開始,尋找對應的引用節點,找到這個節點后,繼續尋找這個節點的引用節點。當所有的引用節點尋找完畢后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點。

上圖紅色為無用的節點,可以被回收。
目前Java中可以作為GC ROOT的對象有:
1、虛擬機棧中引用的對象(本地變量表)
2、方法區中靜態屬性引用的對象
3、方法區中常亮引用的對象
4、本地方法棧中引用的對象(Native對象)
基本所有GC算法都引用根搜索算法這種概念。
2、標記 - 清除算法

標記-清除算法采用從根集合進行掃描,對存活的對象進行標記,標記完畢后,再掃描整個空間中未被標記的對象進行直接回收,如上圖。
標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活的對象比較多的情況下極為高效,但由於標記-清除算法直接回收不存活的對象,並沒有對還存活的對象進行整理,因此會導致內存碎片。
3、復制算法
復制算法將內存划分為兩個區間,使用此算法時,所有動態分配的對象都只能分配在其中一個區間(活動區間),而另外一個區間(空間區間)則是空閑的。
復制算法采用從根集合掃描,將存活的對象復制到空閑區間,當掃描完畢活動區間后,會的將活動區間一次性全部回收。此時原本的空閑區間變成了活動區間。下次GC時候又會重復剛才的操作,以此循環。
復制算法在存活對象比較少的時候,極為高效,但是帶來的成本是犧牲一半的內存空間用於進行對象的移動。所以復制算法的使用場景,必須是對象的存活率非常低才行,而且最重要的是,我們需要克服50%內存的浪費。
4、標記 - 整理算法
標記-整理算法采用 標記-清除 算法一樣的方式進行對象的標記、清除,但在回收不存活的對象占用的空間后,會將所有存活的對象往左端空閑空間移動,並更新對應的指針。標記-整理 算法是在標記-清除 算法之上,又進行了對象的移動排序整理,因此成本更高,但卻解決了內存碎片的問題。
JVM為了優化內存的回收,使用了分代回收的方式,對於新生代內存的回收(Minor GC)主要采用復制算法。而對於老年代的回收(Major GC),大多采用標記-整理算法。
垃圾回收器簡介
需要注意的是,每一個回收器都存在Stop The World 的問題,只不過各個回收器在Stop The World 時間優化程度、算法的不同,可根據自身需求選擇適合的回收器。
1、Serial(-XX:+UseSerialGC)
從名字我們可以看出,這是一個串行收集器。
Serial收集器是Java虛擬機中最基本、歷史最悠久的收集器。在JDK1.3之前是Java虛擬機新生代收集器的唯一選擇。目前也是ClientVM下ServerVM 4核4GB以下機器默認垃圾回收器。Serial收集器並不是只能使用一個CPU進行收集,而是當JVM需要進行垃圾回收的時候,需暫停所有的用戶線程,直到回收結束。
使用算法:復制算法
JVM中文名稱為Java虛擬機,因此它像一台虛擬的電腦在工作,而其中的每一個線程都被認為是JVM的一個處理器,因此圖中的CPU0、CPU1實際上為用戶的線程,而不是真正的機器CPU,不要誤解哦。
Serial收集器雖然是最老的,但是它對於限定單個CPU的環境來說,由於沒有線程交互的開銷,專心做垃圾收集,所以它在這種情況下是相對於其他收集器中最高效的。
2、SerialOld(-XX:+UseSerialGC)
SerialOld是Serial收集器的老年代收集器版本,它同樣是一個單線程收集器,這個收集器目前主要用於Client模式下使用。如果在Server模式下,它主要還有兩大用途:一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個就是作為CMS收集器的后備預案,如果CMS出現Concurrent Mode Failure,則SerialOld將作為后備收集器。
使用算法:標記 - 整理算法
運行示意圖與上圖一致。
3、ParNew(-XX:+UseParNewGC)
ParNew其實就是Serial收集器的多線程版本。除了Serial收集器外,只有它能與CMS收集器配合工作。
使用算法:復制算法

ParNew是許多運行在Server模式下的JVM首選的新生代收集器。但是在單CPU的情況下,它的效率遠遠低於Serial收集器,所以一定要注意使用場景。
4、ParallelScavenge(-XX:+UseParallelGC)
ParallelScavenge又被稱為吞吐量優先收集器,和ParNew 收集器類似,是一個新生代收集器。
使用算法:復制算法
ParallelScavenge收集器的目標是達到一個可控件的吞吐量,所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。如果虛擬機總共運行了100分鍾,其中垃圾收集花了1分鍾,那么吞吐量就是99% 。
5、ParallelOld(-XX:+UseParallelOldGC)
ParallelOld是並行收集器,和SerialOld一樣,ParallelOld是一個老年代收集器,是老年代吞吐量優先的一個收集器。這個收集器在JDK1.6之后才開始提供的,在此之前,ParallelScavenge只能選擇SerialOld來作為其老年代的收集器,這嚴重拖累了ParallelScavenge整體的速度。而ParallelOld的出現后,“吞吐量優先”收集器才名副其實!
使用算法:標記 - 整理算法

在注重吞吐量與CPU數量大於1的情況下,都可以優先考慮ParallelScavenge + ParalleloOld收集器。
6、CMS (-XX:+UseConcMarkSweepGC)
CMS是一個老年代收集器,全稱 Concurrent Low Pause Collector,是JDK1.4后期開始引用的新GC收集器,在JDK1.5、1.6中得到了進一步的改進。它是對於響應時間的重要性需求大於吞吐量要求的收集器。對於要求服務器響應速度高的情況下,使用CMS非常合適。
CMS的一大特點,就是用兩次短暫的暫停來代替串行或並行標記整理算法時候的長暫停。
使用算法:標記 - 清理
CMS的執行過程如下:
· 初始標記(STW initial mark)
在這個階段,需要虛擬機停頓正在執行的應用線程,官方的叫法STW(Stop Tow World)。這個過程從根對象掃描直接關聯的對象,並作標記。這個過程會很快的完成。
· 並發標記(Concurrent marking)
這個階段緊隨初始標記階段,在“初始標記”的基礎上繼續向下追溯標記。注意這里是並發標記,表示用戶線程可以和GC線程一起並發執行,這個階段不會暫停用戶的線程哦。
· 並發預清理(Concurrent precleaning)
這個階段任然是並發的,JVM查找正在執行“並發標記”階段時候進入老年代的對象(可能這時會有對象從新生代晉升到老年代,或被分配到老年代)。通過重新掃描,減少在一個階段“重新標記”的工作,因為下一階段會STW。
· 重新標記(STW remark)
這個階段會再次暫停正在執行的應用線程,重新重根對象開始查找並標記並發階段遺漏的對象(在並發標記階段結束后對象狀態的更新導致),並處理對象關聯。這一次耗時會比“初始標記”更長,並且這個階段可以並行標記。
· 並發清理(Concurrent sweeping)
這個階段是並發的,應用線程和GC清除線程可以一起並發執行。
· 並發重置(Concurrent reset)
這個階段任然是並發的,重置CMS收集器的數據結構,等待下一次垃圾回收。
CMS的缺點:
1、內存碎片。由於使用了 標記-清理 算法,導致內存空間中會產生內存碎片。不過CMS收集器做了一些小的優化,就是把未分配的空間匯總成一個列表,當有JVM需要分配內存空間的時候,會搜索這個列表找到符合條件的空間來存儲這個對象。但是內存碎片的問題依然存在,如果一個對象需要3塊連續的空間來存儲,因為內存碎片的原因,尋找不到這樣的空間,就會導致Full GC。
2、需要更多的CPU資源。由於使用了並發處理,很多情況下都是GC線程和應用線程並發執行的,這樣就需要占用更多的CPU資源,也是犧牲了一定吞吐量的原因。
3、需要更大的堆空間。因為CMS標記階段應用程序的線程還是執行的,那么就會有堆空間繼續分配的問題,為了保障CMS在回收堆空間之前還有空間分配給新加入的對象,必須預留一部分空間。CMS默認在老年代空間使用68%時候啟動垃圾回收。可以通過-XX:CMSinitiatingOccupancyFraction=n來設置這個閥值。
7、GarbageFirst(G1)
這是一個新的垃圾回收器,既可以回收新生代也可以回收老年代,SunHotSpot1.6u14以上EarlyAccess版本加入了這個回收器,Sun公司預期SunHotSpot1.7發布正式版本。通過重新划分內存區域,整合優化CMS,同時注重吞吐量和響應時間。杯具的是Oracle收購這個收集器之后將其用於商用收費版收集器。因此目前暫時沒有發現哪個公司使用它,這個放在之后再去研究吧。
整理一下新生代和老年代的收集器。
新生代收集器:
Serial (-XX:+UseSerialGC)
ParNew(-XX:+UseParNewGC)
ParallelScavenge(-XX:+UseParallelGC)
G1 收集器
老年代收集器:
SerialOld(-XX:+UseSerialOldGC)
ParallelOld(-XX:+UseParallelOldGC)
CMS(-XX:+UseConcMarkSweepGC)
G1 收集器
目前了解的GC收集器就是這么多了,如果有哪位有緣的朋友看到了這篇文章,恰好有更好的GC收集器推薦,歡迎留言交流。
原帖地址:https://zhuanlan.zhihu.com/p/25539690
