【轉載】Java性能優化之JVM GC(垃圾回收機制)

文章來源:https://zhuanlan.zhihu.com/p/25539690java

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怎麼判斷對象能夠被回收了?數據結構

· 對象沒有引用多線程

· 做用域發生未捕獲異常併發

· 程序在做用域正常執行完畢jvm

· 程序執行了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的條件很是嚴苛,必須符合如下三種條件纔會被回收:

一、全部實例被回收

二、加載該類的ClassLoader 被回收

三、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

每一個空間的執行順序以下:

一、絕大多數剛剛被建立的對象會存放在伊甸園空間(Eden)。

二、在伊甸園空間執行第一次GC(Minor GC)以後,存活的對象被移動到其中一個倖存者空間(Survivor)。

三、此後,每次伊甸園空間執行GC後,存活的對象會被堆積在同一個倖存者空間。

四、當一個倖存者空間飽和,還在存活的對象會被移動到另外一個倖存者空間。而後會清空已經飽和的哪一個倖存者空間。

五、在以上步驟中重複N次(N = MaxTenuringThreshold(年齡閥值設定,默認15))依然存活的對象,就會被移動到老年代。

從上面的步驟能夠發現,兩個倖存者空間,必須有一個是保持空的。若是兩個兩個倖存者空間都有數據,或兩個空間都是空的,那必定是你的系統出現了某種錯誤。

咱們須要重點記住的是,對象在剛剛被建立以後,是保存在伊甸園空間的(Eden)。那些長期存活的對象會經由倖存者空間(Survivor)轉存到老年代空間(Old generation)。

也有例外出現,對於一些比較大的對象(須要分配一塊比較大的連續內存空間)則直接進入到老年代。通常在Survivor 空間不足的狀況下發生。

 

老年代空間的構成與邏輯

老年代空間的構成其實很簡單,它不像新生代空間那樣劃分爲幾個區域,它只有一個區域,裏面存儲的對象並不像新生代空間絕大部分都是朝聞道,夕死矣。這裏的對象幾乎都是從Survivor 空間中熬過來的,它們毫不會輕易的狗帶。所以,Full GC(Major GC)發生的次數不會有Minor GC 那麼頻繁,而且作一次Major GC 的時間比Minor GC 要更長(約10倍)。

 

JVM GC 算法講解

一、根搜索算法

根搜索算法是從離散數學中的圖論引入的,程序把全部引用關係看做一張圖,從一個節點GC ROOT 開始,尋找對應的引用節點,找到這個節點後,繼續尋找這個節點的引用節點。當全部的引用節點尋找完畢後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點。

上圖紅色爲無用的節點,能夠被回收。

目前Java中能夠做爲GC ROOT的對象有:

一、虛擬機棧中引用的對象(本地變量表)

二、方法區中靜態屬性引用的對象

三、方法區中常亮引用的對象

四、本地方法棧中引用的對象(Native對象)

基本全部GC算法都引用根搜索算法這種概念。

 

二、標記 - 清除算法

 

標記-清除算法採用從根集合進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象進行直接回收,如上圖。

標記-清除算法不須要進行對象的移動,而且僅對不存活的對象進行處理,在存活的對象比較多的狀況下極爲高效,但因爲標記-清除算法直接回收不存活的對象,並無對還存活的對象進行整理,所以會致使內存碎片。

三、複製算法

 

 

複製算法將內存分爲兩個區間,使用此算法時,全部動態分配的對象都只能分配在其中一個區間(活動區間),而另一個區間(空間區間)則是空閒的。

 

複製算法採用從根集合掃描,將存活的對象複製到空閒區間,當掃描完畢活動區間後,會的將活動區間一次性所有回收。此時本來的空閒區間變成了活動區間。下次GC時候又會重複剛纔的操做,以此循環。

複製算法在存活對象比較少的時候,極爲高效,可是帶來的成本是犧牲一半的內存空間用於進行對象的移動。因此複製算法的使用場景,必須是對象的存活率很是低才行,並且最重要的是,咱們須要克服50%內存的浪費。

四、標記 - 整理算法

 

 

標記-整理算法採用 標記-清除 算法同樣的方式進行對象的標記、清除,但在回收不存活的對象佔用的空間後,會將全部存活的對象往左端空閒空間移動,並更新對應的指針。標記-整理 算法是在標記-清除 算法之上,又進行了對象的移動排序整理,所以成本更高,但卻解決了內存碎片的問題。

 

JVM爲了優化內存的回收,使用了分代回收的方式,對於新生代內存的回收(Minor GC)主要採用複製算法。而對於老年代的回收(Major GC),大多采用標記-整理算法。

 

垃圾回收器簡介

須要注意的是,每個回收器都存在Stop The World 的問題,只不過各個回收器在Stop The World 時間優化程度、算法的不一樣,可根據自身需求選擇適合的回收器。

一、Serial(-XX:+UseSerialGC)

從名字咱們能夠看出,這是一個串行收集器。

Serial收集器是Java虛擬機中最基本、歷史最悠久的收集器。在JDK1.3以前是Java虛擬機新生代收集器的惟一選擇。目前也是ClientVM下ServerVM 4核4GB如下機器默認垃圾回收器。Serial收集器並非只能使用一個CPU進行收集,而是當JVM須要進行垃圾回收的時候,需暫停全部的用戶線程,直到回收結束。

使用算法:複製算法

 

 

JVM中文名稱爲Java虛擬機,所以它像一臺虛擬的電腦在工做,而其中的每個線程都被認爲是JVM的一個處理器,所以圖中的CPU0、CPU1實際上爲用戶的線程,而不是真正的機器CPU,不要誤解哦。

 

Serial收集器雖然是最老的,可是它對於限定單個CPU的環境來講,因爲沒有線程交互的開銷,專心作垃圾收集,因此它在這種狀況下是相對於其餘收集器中最高效的。

 

二、SerialOld(-XX:+UseSerialGC)

SerialOld是Serial收集器的老年代收集器版本,它一樣是一個單線程收集器,這個收集器目前主要用於Client模式下使用。若是在Server模式下,它主要還有兩大用途:一個是在JDK1.5及以前的版本中與Parallel Scavenge收集器搭配使用,另一個就是做爲CMS收集器的後備預案,若是CMS出現Concurrent Mode Failure,則SerialOld將做爲後備收集器。

使用算法:標記 - 整理算法

運行示意圖與上圖一致。

 

三、ParNew(-XX:+UseParNewGC)

ParNew其實就是Serial收集器的多線程版本。除了Serial收集器外,只有它能與CMS收集器配合工做。

使用算法:複製算法

 

ParNew是許多運行在Server模式下的JVM首選的新生代收集器。可是在單CPU的狀況下,它的效率遠遠低於Serial收集器,因此必定要注意使用場景。

 

四、ParallelScavenge(-XX:+UseParallelGC)

ParallelScavenge又被稱爲吞吐量優先收集器,和ParNew 收集器相似,是一個新生代收集器。

使用算法:複製算法

ParallelScavenge收集器的目標是達到一個可控件的吞吐量,所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。若是虛擬機總共運行了100分鐘,其中垃圾收集花了1分鐘,那麼吞吐量就是99% 。

 

五、ParallelOld(-XX:+UseParallelOldGC)

ParallelOld是並行收集器,和SerialOld同樣,ParallelOld是一個老年代收集器,是老年代吞吐量優先的一個收集器。這個收集器在JDK1.6以後纔開始提供的,在此以前,ParallelScavenge只能選擇SerialOld來做爲其老年代的收集器,這嚴重拖累了ParallelScavenge總體的速度。而ParallelOld的出現後,「吞吐量優先」收集器才名副其實!

使用算法:標記 - 整理算法

 

在注重吞吐量與CPU數量大於1的狀況下,均可以優先考慮ParallelScavenge + ParalleloOld收集器。

 

六、CMS (-XX:+UseConcMarkSweepGC)

CMS是一個老年代收集器,全稱 Concurrent Low Pause Collector,是JDK1.4後期開始引用的新GC收集器,在JDK1.五、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的缺點:

一、內存碎片。因爲使用了 標記-清理 算法,致使內存空間中會產生內存碎片。不過CMS收集器作了一些小的優化,就是把未分配的空間彙總成一個列表,當有JVM須要分配內存空間的時候,會搜索這個列表找到符合條件的空間來存儲這個對象。可是內存碎片的問題依然存在,若是一個對象須要3塊連續的空間來存儲,由於內存碎片的緣由,尋找不到這樣的空間,就會致使Full GC。

二、須要更多的CPU資源。因爲使用了併發處理,不少狀況下都是GC線程和應用線程併發執行的,這樣就須要佔用更多的CPU資源,也是犧牲了必定吞吐量的緣由。

三、須要更大的堆空間。由於CMS標記階段應用程序的線程仍是執行的,那麼就會有堆空間繼續分配的問題,爲了保障CMS在回收堆空間以前還有空間分配給新加入的對象,必須預留一部分空間。CMS默認在老年代空間使用68%時候啓動垃圾回收。能夠經過-XX:CMSinitiatingOccupancyFraction=n來設置這個閥值。

 

七、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 收集器

內存溢出和內存泄露的區別

一、內存溢出

內存溢出指的是程序在申請內存的時候,沒有足夠大的空間能夠分配了。

二、內存泄露

內存泄露指的是程序在申請內存以後,沒有辦法釋放掉已經申請到內存,它始終佔用着內存,即被分配的對象可達但無用。內存泄露通常都是由於內存中有一塊很大的對象,可是沒法釋放。

從定義上能夠看出,內存泄露終將致使內存溢出。

注意,定位虛擬機問題內存問題的時候第一步就是要判斷究竟是內存溢出仍是內存泄露,前者好判斷,跟蹤堆棧信息就能夠了;後者比較複雜一點,通常都是老年代中的大對象沒釋放掉,要經過各類辦法找出老年代中的大對象沒有被釋放的緣由。

 

並行和併發的區別

這兩個名詞都是併發編程中的概念,在談論垃圾收集器的上下文語境中,能夠這麼理解這兩個名詞:

一、並行Parallel

多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態

二、併發Concurrent

指用戶線程與垃圾收集線程同時執行(但並不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上

 

Minor GC和Full GC的區別

一、新生代GC(Minor GC)

指發生在新生代的垃圾收集動做,由於大多數Java對象存活率都不高,因此Minor GC很是頻繁,通常回收速度也比較快

二、老年代GC(Major GC/Full GC)

指發生在老年代的垃圾收集動做,出現了Major GC,常常會伴隨至少一次的Minor GC(但並非絕對的)。Major GC的速度通常要比Minor GC慢上10倍以上

 

Client模式和Server模式的區別

部分商用虛擬機中,Java程序最初是經過解釋器對.class文件進行解釋執行的,當虛擬機發現某個方法或代碼塊運行地特別頻繁的時候,就會把這些代碼認定爲熱點代碼Hot Spot Code(這也是咱們使用的虛擬機HotSpot名稱的由來)。爲了提升熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各類層次的優化,完成這個任務的編譯器叫作即時編譯器(Just In Time Compiler,即JIT編譯器)。JIT編譯器並非虛擬機必需的部分,Java虛擬機規範並無要求要有JIT編譯器的存在,更沒有限定或指導JIT編譯器應該如何去實現。可是,JIT編譯器性能的好壞、代碼優化程度的高低倒是衡量一款商用虛擬機優秀與否的最關鍵指標之一。

解釋器和編譯器其實和編譯器各有優點:

一、當程序須要迅速啓動和執行的時候,解釋器能夠先發揮做用,省去編譯的時間,當即執行

二、在程序運行後,隨着時間的推移,編譯器逐漸發揮做用,把愈來愈多的代碼編譯成本地代碼以後,能夠獲取更高的執行效率

咱們使用的HotSpot中內置了兩個JIT編譯器,即C1編譯器和C2編譯器,默認採用的是解釋器和一個編輯器配合的方式進行工做。HotSpot在啓動的時候會根據自身版本以及宿主機器的硬件性能自動選擇運行模式,好比會檢測宿主機器是否爲服務器、好比J2SE會檢測主機是否有至少2個CPU和至少2GB的內存。

一、若是是,則虛擬機會以Server模式運行,該模式與C2編譯器共同運行,更注重編譯的質量,啓動速度慢,可是運行效率高,適合用在服務器環境下,針對生產環境進行了優化

二、若是不是,則虛擬機會以Client模式運行,該模式與C1編譯器共同運行,更注重編譯的速度,啓動速度快,更適合用在客戶端的版本下,針對GUI進行了優化

有兩種方法查看虛擬機是運行在Client模式下仍是Server模式下:

一、在程序命令行運行「java -version」命令,查看的是你本地安裝的虛擬機是信息

二、好比咱們用Eclipse或者MyEclipse運行程序,通常使用的都是工具自帶的JRE,虛擬機並非本地安裝的虛擬機。這時候怎麼辦呢,能夠經過在程序中運行下面的語句來查看虛擬機信息

System.out.println(System.getProperty("java.vm.name"));

我這裏的運行結果是

Java HotSpot(TM) 64-Bit Server VM

固然要改變虛擬機運行的模式也能夠,只須要改jvm.cfg就能夠了。咱們能夠從如下幾個地方找到jvm.cfg:

一、32位的JDK的文件路徑是  JAVA_HOME/jre/lib/i386/jvm.cfg

二、64位的JDK的文件路徑是  JAVA_HOME/jre/lib/amd64/jvm.cfg

三、MyEclipse在 .../Common/binary/com.sun.java.jdk.win32.x86_64_1.6.0.013/jre/lib/amd64/jvm.cfg

目前64位只支持Server模式,文件內容都是同樣的,上面的註釋不去管它,剩下的就是這些:

-server KNOWN
-client IGNORE
-hotspot ALIASED_TO -server
-classic WARN
-native ERROR
-green ERROR

因爲個人電腦裝的是64位JDK,因此是「-client INGORE」。同時支持Server模式和Client模式的,應該是「-server KNOWN」和「-client KNOWN」,通常只須要變動這兩個配置的前後順序便可,可是前提是JAVA_HOME/jre/bin目錄下同時存在server和client兩個文件夾,分別對應着各自的虛擬機,缺乏一個,切換後就會報錯。

相關文章
相關標籤/搜索