認識GC

 
1、基礎概念
  • 初步認知
  1. 什麼是java虛擬機,什麼是java的虛擬機實例?java的虛擬機至關於咱們的一個java類,而java虛擬機實例,至關咱們new一個java類,不過java虛擬機不是經過new這個關鍵字而是經過java.exe或者javaw.exe來啓動一個虛擬機實例。
  2. java的虛擬機種有兩種線程,一種叫叫守護線程,一種叫非守護線程main函數就是個非守護線程,虛擬機的gc就是一個守護線程。java虛擬機的生命週期,當一個java應用main函數啓動時虛擬機也同時被啓動,而只有當在虛擬機實例中的全部非守護進程都結束時,java虛擬機實例才結束生命。
  3. java虛擬機的體系結構

 

  • 數據類型
Java虛擬機中,數據類型能夠分爲兩類:基本類型和引用類型。基本類型的變量保存原始值,即:他表明的值就是數值自己;而引用類型的變量保存引用值。「引用值」表明了某個對象的引用,而不是對象自己,對象自己存放在這個引用值所表示的地址的位置,因此修改對象的引用數據變量不會對對象自己數據有任何改變。
基本類型包括:byte、short、int、long、char、float、double、Boolean、returnAddress;
引用類型包括:類類型、接口類型、數組;
 

 

1)棧是運行時的單位,而堆是存儲的單位。
2)棧解決程序的運行問題,即程序如何執行,或者說如何處理數據;堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。
3)從軟件設計的角度看,棧表明了處理邏輯,而堆表明了數據。
4)堆與棧的分離,使得堆中的內容能夠被多個棧共享(也能夠理解爲多個線程訪問同一個對象)。
5) 對象的屬性其實就是數據,存放在堆中;而對象的行爲(方法),就是運行邏輯,放在棧中。
堆:存儲new出來的對象
棧:基本數據類型和堆中對象的引用
 
  • 引用類型
對象引用類型分爲:強引用、軟引用、弱引用、虛引用
強引用: 就是咱們通常聲明對象是時虛擬機生成的引用,強引用環境下,垃圾回收時須要嚴格判斷當前對象是否被強引用,若是被強引用,則不會被垃圾回收
軟引用 軟引用通常被作爲緩存來使用。與強引用的區別是,軟引用在垃圾回收時,虛擬機會根據當前系統的剩餘內存來決定是否對軟引用進行回收。若是剩餘內存比較緊張,則虛擬機會回收軟引用所引用的空間;若是剩餘內存相對富裕,則不會進行回收。換句話說,虛擬機在發生OutOfMemory時,確定是沒有軟引用存在的。
弱引用 弱引用與軟引用相似,都是做爲緩存來使用。但與軟引用不一樣,弱引用在進行垃圾回收時,是必定會被回收掉的,所以其生命週期只存在於一個垃圾回收週期內。
 強引用不用說,咱們系統通常在使用時都是用的強引用。而「軟引用」和「弱引用」比較少見。他們通常被做爲緩存使用,並且通常是在內存大小比較受限的狀況下作爲緩存。由於若是內存足夠大的話,能夠直接使用強引用做爲緩存便可,同時可控性更高。於是,他們常見的是被使用在桌面應用系統的緩存。
 
2、垃圾回收算法
 
1)按照基本回收策略分
(1)引用計數(Reference Counting):
比較古老的回收算法。原理是此對象有一個引用,即增長一個計數,刪除一個引用則減小一個計數。垃圾回收時,引用收集計數爲0的對象。此算法最致命的是沒法處理循環引用的問題。
 
(2)標記-清除(Mark-Sweep)

 

此算法執行分兩階段。第一階段從引用根節點開始標記全部被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此算法須要暫停整個應用,同時,會產生內存碎片。
 
(3)複製(Copying):

 

此算法把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另一個區域中。次算法每次只處理正在使用中的對象,所以複製成本比較小,同時複製過去之後還能進行相應的內存整理,不會出現「碎片」問題。固然,此算法的缺點也是很明顯的,就是須要兩倍內存空間。
 
(4)標記-整理(Mark-Compact):

 

此算法結合了「標記-清除」和「複製」兩個算法的優勢。也是分兩階段,第一階段從根節點開始標記全部被引用對象,第二階段遍歷整個堆,把清除未標記對象而且把存活對象「壓縮」到堆的其中一塊,按順序排放。此算法避免了「標記-清除」的碎片問題,同時也避免了「複製」算法的空間問題。
 
2)按分區對待的方式分
增量收集(Incremental Collecting)實時垃圾回收算法,即:在應用進行的同時進行垃圾回收。不知道什麼緣由JDK5.0中的收集器沒有使用這種算法的。
分代收集(Generational Collecting)基於對對象生命週期分析後得出的垃圾回收算法。把對象分爲年青代、年老代、持久代,對不一樣生命週期的對象使用不一樣的算法(上述方式中的一個)進行回收。如今的垃圾回收器(從J2SE1.2開始)都是使用此算法的。
 
3)按系統線程分
(1)串行收集串行收集使用單線程處理全部垃圾回收工做,由於無需多線程交互,實現容易,並且效率比較高。可是,其侷限性也比較明顯,即沒法使用多處理器的優點,因此此收集適合單處理器機器。固然,此收集器也能夠用在小數據量(100M左右)狀況下的多處理器機器上。
(2)並行收集並行收集使用多線程處理垃圾回收工做,於是速度快,效率高。並且理論上CPU數目越多,越能體現出並行收集器的優點。
(3)併發收集相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工做時,須要暫停整個運行環境,而只有垃圾回收程序在運行,所以,系統在垃圾回收時會有明顯的暫停,並且暫停時間會由於堆越大而越長。
 
  • 爲何要分代
分代的垃圾回收策略,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。
在Java程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關,好比Http請求中的Session對象、線程、Socket鏈接,這類對象跟業務直接掛鉤,所以生命週期比較長。可是還有一些對象,主要是程序運行過程當中生成的臨時變量,這些對象生命週期會比較短,好比:String對象,因爲其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次便可回收。
試想,在不進行對象存活時間區分的狀況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,由於每次回收都須要遍歷全部存活對象,但實際上,對於生命週期長的對象而言,這種遍歷是沒有效果的,由於可能進行了不少次遍歷,可是他們依舊存在。所以,分代垃圾回收採用分治的思想,進行代的劃分,把不一樣生命週期的對象放在不一樣代上,不一樣代上採用最適合它的垃圾回收方式進行回收。
 
  • 如何分代
  •  

如上圖,虛擬機中的共劃分爲三個代:年輕代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)
其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。
  1. 年輕代
全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象 。年輕代分三個區。一個Eden區,兩個Survivor區(通常而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另一個Survivor區,當這個Survivor區也滿了的時候,從第一個Survivor區複製過來的而且此時還存活的對象,將被複制「年老區(Tenured)」。須要注意,Survivor的兩個區是對稱的,沒前後關係,因此同一個區中可能同時存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。並且,Survivor區總有一個是空的。同時,根據程序須要,Survivor區是能夠配置爲多個的(多於兩個),這樣能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能。
  1. 年老代
在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。
  1. 持久代(Java 8改成元空間)
用於存放靜態文件,現在Java類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。持久代大小經過-XX:MaxPermSize=<N>進行設置。
隨着JDK8的到來,JVM再也不有PermGen。但類的元數據信息(metadata)還在,只不過再也不是存儲在連續的堆空間上,而是移動到叫作「Metaspace」的本地內存(Native memory)中。 緣由:類的元數據信息轉移到Metaspace的緣由是PermGen很難調整。PermGen中類的元數據信息在每次FullGC的時候可能會被收集,但成績很難使人滿意。並且應該爲PermGen分配多大的空間很難肯定,由於PermSize的大小依賴於不少因素,好比JVM加載的class的總數,常量池的大小,方法的大小等。
 
因爲類的元數據能夠在本地內存(native memory)以外分配,因此其最大可利用空間是整個系統內存的可用空間。這樣,你將再也不會遇到OOM錯誤,溢出的內存會涌入到交換空間。最終用戶能夠爲類元數據指定最大可利用的本地內存空間,JVM也能夠增長本地內存空間來知足類元數據信息的存儲。
 

 

  • GC有兩種類型:Scavenge GC和Full GC
(1)Scavenge GC
通常狀況下,當新對象生成,而且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,而且把尚且存活的對象移動到Survivor區。
 
(2)Full GC
    對整個堆進行整理,包括Young、Tenured和Perm。Full GC由於須要對整個對進行回收,因此比Scavenge GC要慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於FullGC的調節。有以下緣由可能致使Full GC:
· 年老代(Tenured)被寫滿
· 持久代(Perm)被寫滿 
· System.gc()被顯示調用 
·上一次GC以後Heap的各域分配策略動態變化
·內存空間碎片太多
 
  • 常見配置彙總
堆設置
  -Xms:初始堆大小
  -Xmx:最大堆大小
  -XX:NewSize=n:設置年輕代大小
  -XX:NewRatio=n:設置年輕代和年老代的比值。如:爲3,表示年輕代與年老代比值爲1:3,年輕代佔整個年輕代年老代和的1/4
  -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
  -XX:MaxPermSize=n:設置持久代大小
 
收集器設置
  -XX:+UseSerialGC:設置串行收集器
  -XX:+UseParallelGC:設置並行收集器
  -XX:+UseParalledlOldGC:設置並行年老代收集器
  -XX:+UseConcMarkSweepGC:設置併發收集器
 
垃圾回收統計信息
  -XX:+PrintGC
  -XX:+PrintGCDetails
  -XX:+PrintGCTimeStamps
  -Xloggc:filename
 
並行收集器設置
  -XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。
  -XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
  -XX:GCTimeRatio=n:設置垃圾回收時間佔程序運行時間的百分比。公式爲1/(1+n)
 
併發收集器設置
  -XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU狀況。
  -XX:ParallelGCThreads=n:設置併發收集器年輕代收集方式爲並行收集時,使用的CPU數。並行收集線程數。
 
調優總結
年輕代大小選擇
響應時間優先的應用:儘量設大,直到接近系統的最低響應時間限制(根據實際狀況選擇)。在此種狀況下,年輕代收集發生的頻率也是最小的。同時,減小到達年老代的對象。
吞吐量優先的應用:儘量的設置大,可能到達Gbit的程度。由於對響應時間沒有要求,垃圾收集能夠並行進行,通常適合8CPU以上的應用。
 
年老代大小選擇
響應時間優先的應用:年老代使用併發收集器,因此其大小須要當心設置,通常要考慮併發會話率會話持續時間等一些參數。若是堆設置小了,能夠會形成內存碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;若是堆大了,則須要較長的收集時間。最優化的方案,通常須要參考如下數據得到:
  1. 併發垃圾收集信息
  2. 持久代併發收集次數
  3. 傳統GC信息
  4. 花在年輕代和年老代回收上的時間比例
減小年輕代和年老代花費的時間,通常會提升應用的效率
吞吐量優先的應用:通常吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。緣由是,這樣能夠儘量回收掉大部分短時間對象,減小中期的對象,而年老代盡存放長期存活對象。
 
較小堆引發的碎片問題
由於年老代的併發收集器使用標記、清除算法,因此不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣能夠分配給較大的對象。可是,當堆空間較小時,運行一段時間之後,就會出現「碎片」,若是併發收集器找不到足夠的空間,那麼併發收集器將會中止,而後使用傳統的標記、清除方式進行回收。若是出現「碎片」,可能須要進行以下配置:
1. -XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啓對年老代的壓縮。
2. -XX:CMSFullGCsBeforeCompaction=0:上面配置開啓的狀況下,這裏設置多少次Full GC後,對年老代進行壓縮
 
 
  • G1GC、ParallelGC和CMSGC區別
概念:G1 GC 是一種新的垃圾回收策略,從 JDK7 開始,主要適用於服務器端的JVM,和大內存的應用,其目標是達到相似 CMS 的高吞吐量。G1 中依然有分代管理的思想,主要採用分塊管理的思想,經過將內存分爲不超過2048個塊,每塊大小在 1M-32M 之間, Eden、Survivor space 和 年老代 都是一系列不連續的邏輯區域,顆粒度更細,選擇最須要回收的段進行gc。
 
吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)
  1. Parallel收集器
採用多線程來經過掃描並壓縮堆,
特色:停頓時間短,回收效率高,對吞吐量要求高。
適用場景:大型應用,科學計算,大規模數據採集等。
經過JVM參數 XX:+USeParNewGC 打開併發標記掃描垃圾回收器。
 
2.CMS收集器
採用「標記-清除」算法實現,使用多線程的算法去掃描堆,對發現未使用的對象進行回收。
(1)初始標記
(2)併發標記
(3)併發預處理
(4)從新標記
(5)併發清除
(6)併發重置
特色:響應時間優先,減小垃圾收集停頓時間,把一次長暫停,改成2次短暫停。
適應場景:服務器、電信領域等。
經過JVM參數 -XX:+UseConcMarkSweepGC設置
 

 

 
3.G1收集器
在G1中,堆一整塊內存空間,被分爲多個連續的heap區(regions)。採用G1算法進行回收,吸取了CMS收集器特色。
特色:支持很大的堆,高吞吐量,減小gc耗時
年老代不存在清理階段而是採用併發標記的方式,在標記過程當中進行清理。
--支持多CPU和垃圾回收線程
--在主線程暫停的狀況下,使用並行收集
--在主線程運行的狀況下,使用併發收集
實時目標:可配置在N毫秒內最多隻佔用M毫秒的時間進行垃圾回收
經過JVM參數 –XX:+UseG1GC 使用G1垃圾回收器
-XX:MaxGCPauseMillis=200 - 設置最大GC停頓時間(GC pause time)指標(target). 這是一個軟性指標(soft goal), JVM 會盡力 去達成這個目標. 因此有時候這個目標並不能達成. 默認值爲 200 毫秒.
-XX:InitiatingHeapOccupancyPercent=45 - 啓動併發GC時的堆內存佔用百分比. G1用它來觸發併發GC週期,基於整個堆的使用率,而不僅是某一代內存的使用比例。值爲 0 則表示「一直執行GC循環)'. 默認值爲 45 (例如, 所有的 45% 或者使用了45%).
 
相關文章
相關標籤/搜索