1. 程式人生 > >JVM GC Collector工作原理及優化

JVM GC Collector工作原理及優化

情況 .cn 次數 線程 update mode -1 verify 測試

JVM 調優主要是調整GC以及一些執行參數:

目標:

堆不要太大,不然單次GC的時間過長導致服務器無法響應的問題

壓力測試的時候TPS平穩

盡量避免full GC

檢查是否用了並行的垃圾回收器

參數:

-server執行,開啟優化

采用並行gc collector, -XX:+UseParallelGC +XX:+UseParallelOldGC +XX:+UseConcMarkSweepGC

-Xmx不要太大,不然單次gc的過程可能太長,大內存機器可以采用多個實例的方式

-Xms不要太小,不然jvm需要多次調整堆的大小,增加gc次數,影響了啟動性能。

同理-XX:MaxPermSize

-XX:NewRatio新生代與老年代的比例要合適,這個需要看應用類型,一般新生代的對象很快就會被GC掉了。

線程池的大小要合適,線程上下文切換也是很耗資源的。

不要去調整一些你不完全了解的參數。

還有-Xverify:none可以加快啟動速度,但字節碼問題查錯很麻煩。

字節碼調優應該避免吧,容易觸發一些jvm本身的bug,這些參數缺少實際場景的測試。

CMS垃圾收集器的相關參數,請讀取相關資料。貌似CMS不會去移動對象的去使得空間更加緊湊。

G1垃圾收集器的工作原理,G1可以Compact Heap Region以減少內存碎片問題。

相關鏈接:一個Oracle jvm部門裏面的員工的blog: https://blogs.oracle.com/jonthecollector/ 裏面的文章很有價值。

Memory Management In The Java HotSpot Virtual Machine

1、為什麽要使用自動內存管理

為了消除手動內存管理帶來的復雜性以及內存泄漏,dangling引用,內存碎片等一系列問題

2、動態內存分配是一個比較復雜的工作,因為它要使用內存的分配及釋放足夠快,同時還要考慮內存碎片問題。

3、垃圾回收器必須盡量少使得程序暫停,同時也需要在 回收耗時、空間大小,回收次數這幾個方面取得一個平衡,另外還需要控制內存碎片。

4、垃圾回收器必須是可擴展,它不應該成為程序性能的瓶頸,它應該可以在多線程/多CPU的環境並行得執行。

5、垃圾回收器最好能夠並發地進行內存回收工作,並發情況下,堆被劃分成幾個區域,這些區域會被並發地進行回收,由此減少了回收工作所引起的程序的暫停。

6、Compacting vs Non-Compacting vs Copying

7、垃圾回收器性能評判的幾個指標:GC時間占程序運行時間的比重,非GC時間占比,GC引起的暫停時間,回收的頻率,檢測到垃圾對象的速度,回收器工作消耗的內存大小。

8、內存分代回收,比較流行分為young generation(新生代)及old generation(老年代),大多數分配的對象不會存活得太久,通常只有一小部分對象可以進化到老年代。因此新生代的區域回收得比較頻繁,而老年代空間通過占用比較多的空間,因此各個區域會使用不同的算法進行垃圾回收。比如-XX:NewRatio=4表示 young generation : old generation = 1 : 4。在HotSpot Virutial Machine中,內存被分為三個區域,young generation, old generation和permenent generation,大部分對象都是在新生代進行分配的,有些對象會直接在老年代進行分配。新生代的空間由Eden生兩個survivor區域來組成,一個survivor用來存放至少存活超過一次young generation GC的對象,而另一個survivor空間是空的,直接下一次回收的時候會被使用。一個suvivor與Eden的大小比值可以用-XX:SurvivorRatio=n來表示(1/n=survivor : Eden)

技術分享

當old generation因塞滿而無法存放young generation升級上來的對象時,將觸發full GC,這時大部分的收集器會用老年代的算法去GC整個Heap(除了CMS Collector外, CMS Collector的老年代算法無法回收新生代區域)。

快速分配,在大片連接的內存塊中進行分配內存的效率是很高的,可以利用bump-the-pointer技術來進行分配,分配器會記住下一次分配的起點。對於多線程的程序來說,內存分配操作需要是線程安全的,如果使用全局鎖的話這會降低性能並造成性能瓶頸,相應的HotSpot采用一種叫做Thread-Local Allocation Buffers的技術(線程自己的內存分配緩沖區),使得減少獲取全局鎖的操作,通常TLAB占用大概1%的Edgen的大小。結合TLAB和bump-the-pointer技術,通常分配一個對象空間只需要10條本地指令。

Serial Collector

young generation使用Serial Collector進行回收的過程

當使用Serial Collector時候,young generation和old generation的回收工作都是使用單個CPU線性執行的,回收過程中將stop-the-world。下圖展示了Serial Collector對young generation進行回收時過程,Eden區域存活的對象被復制到那個空的Survivor塊(圖中用to標記),如果對象太大超出了Survivor的大小,那麽它將直接被copy到old generation區域,而非空的survivor(圖中用from標記)中仍然年輕的對象也被復制到空的survivor中(圖中用to標記),而相對比較“老”的對象則被復制到old generation區域中。

註意:如果"To" survivor被塞滿了話,Eden和"From"Survivor區域還沒有被copy的對象將直接被復制到old generation中(無論它們存活多少了多少代)。

技術分享

在完成了對young generation的回收之後,young generation中Eden區域和"From" survivor都被被清空,只有"To" survivor中有存活的對象。這時,"From"和"To" Survivor將對換,如下圖所示:

技術分享

註意,存在old generation引用young generation對象的情況,為了避免進行young gc的時候掃描old generation,老年代對new generation的引用被記錄在一個叫做card table的cache中。

old generation使用Serial Collector進行回收的過程:

當使用Serial Collector對old generation和permenent generation進行回收的時候,它將使用一種mark-sweep-compact的回收算法:

Mark階段:Collector檢測那些對象仍然存活。

Sweep階段:Collector掃描整個old generation或者permenent generation,

Compact階段:Collector將存活的對象“滑動”到old generation的首部,所有連續的空間放在old generation的尾部,這樣方便利用bump-the-pointer來實現快速地分配對象。如下圖所示:

技術分享

什麽時候應該使用Serial Collector。

大部分以Client-Style(java -client)運行的程序都使用這種收集器,這類程序對回收引種的程序暫停時候不敏感。在現在的硬件條件下,Serial Collector能夠管理64MB大小的堆空間(現在應該可以256MB了吧)。

在JavaSE5中,運行非server-class的機器上默認使用Serial Collector,但用戶可以使用-XX:+UseSerialGC命令參數來指定使用Serial Collector。

Parallel Collector(也被稱為Throughput Collector)

如今,很多程序運行在擁有大內存多CPU的機器上面,Parallel Collector就是為了在垃圾回收過程中充分利用CPU資源而開發的。

使用Parallel Collector對young generation進行垃圾回收的過程:

Parallel Collector使用一種類似於Serial Collector對young generation回收的算法的並行版本,回收時它仍然會stop-the-world,但在回收的過程中它並行地使用多個cpu並行地執行,由些來減少垃圾回收所占用的時候並提升程序運行時間的占比。

技術分享

使用Parallel Collector對old generation進行回收

Parallel Collector使用了與Serial Collector同樣的mark-sweep-compact的回收算法。

什麽時候使用Parallel Collector

運行在多cpu以及對停止時間不敏感的程序可以從使用parallel collector中受益,不頻繁,耗時較長的針對old generation區域回收的仍然會發生, 批量處理,計費,工資以及科學計算這類程序比較適合使用Parallel Collector。

JavaSE5中,運行server-class的機器上默認使用Parallel Collector,用戶可以使用-XX:UseParallelGC命令參數來顯示指定使用Parallel Collector

Parallel Compacting Collector

Parallel Compacting Collector在JavaSE5.0 update 6被引入,與Parallel Collector不同的是,它使用了一種新的算法來對old generation進行回收,最終Parallel Compacting Collector將取代Parallel Collector

回收young generation時,Parallel Compacting Collector使用了與Parallel Collector同樣的算法。

回收old generation和permenent generation時,Parallel Compacting Collector仍然會stop-the-world,但在整理存活對象的時候大部分是並行的。Parallel Compacting Collector使用三個階段進行,

1、young/old/permenent generation區域被劃分成幾個固定大小的區域

2、marking階段,程序中仍然可以引用到的存活的對象被劃分給幾個garbage collection threads中,然後mark工作是並發執行地,當一個對象被標記為存活的時候,它所在的regioin的大小將會被更新。

3、Summary階段,通過前幾次的收集,generation空間的首部會存活的對象會比較密集,通過compacting能回收的空間比較少,因此不值得在上面進行compacting,所以summary階段所做的第一件事就是檢驗regions的密度,從最左邊開始,直到碰到一個密度比較小,值得花時間去compacting的region,然後從這個region開始,compacting右邊的region。summary階段計算並存放被compacting region的新的首地址(這個階段並沒有真正地去Compacting)。註意:summary段是單線程執行的,盡管它可以實現為並發執行。但事實表明並發執行的

4、compacting階段,在這個階段中,利用上一階計算出來的Compacting信息,各個線程可以獨立地往region移動對象。Compacting完成之後,堆空間的後部將釋放出一片連續的空間。

什麽時候使用Parallel Compacting Collector

在Parallel Collector的基礎上,Parallel Compacting Collector進一步減少了由於回收old generation所消耗的時候,進一步滿足對垃圾回收引起的暫停時間 比較敏感的程序。但需要註意的是,Parallel Compacting Collector 可以不適合那些運行在大型機/刀片機的程序,這種機器上是不允許單獨一個程序占用幾個cpu過長時間,在這種環境下可以考慮利用參數-XX:ParallelGCThread=n,或者選擇另外的垃圾回收器。

需要使用-XX:+UseParallelOldGC來顯式指定使用Parallel Compacting Collector,這個參數的名字有點奇怪,這裏的"Old"是指old generation。

Concurrent Mark-Sweep (CMS) Collector

對於很多應用來說,應用運行時間占比(throughput)沒有響應時間這麽重要,young generation區域的回收通常不會造成太長時間的暫停。但是old generation的回收,盡管不是很頻繁,但通常會強制應用暫停比較長的時間。為了解決這個問題,HotSpot JVM 引入了一個叫做concurrent mark-sweep (CMS)的收集器,也叫做low-latency collector(低延遲回收器)

CMS回收young generation的過程:

CMS與Parallel Collector使用同樣的方式回收young generation

CMS回收old generation的過程:

CMS在回收old generation大多數時候都是在程序運行時並發地執行,在開始一次完整的回收之前,CMS需要暫停一下程序(stop-the-world),這個過程叫做初始標記(Initial Mark),這個過程中查找程序代碼中可以直接引用到的對象(通常是線程棧上的對象引用),然後在並發標記階段,CMS去標記所有可達的對象,因為這個工作是並發進行的,應用同時也在更新一些字段的引用,所以在並發標記之後需要來一個stop-the-world,將新產生的對象標記完整,這個過程被稱為remark,remark比initial mark更加耗時,所以一般使用多個線程並發地執行來提交效率。

在remark結束之後,所有的可達的對象都被標記了,然後接下來的並發掃描階段將回收垃圾,下圖闡述了CMS Collector與Serial mark-sweep-compact Collector之間的差別:

技術分享

因為有一些工作,比如在remark階段重新遍歷對象,增加了collector的工作量,所以CMS回收時占用的CPU和內存資源也更多,但它減少了應用的停止時間。

需要註意的是,CMS collector是唯一一個不會去compact(整理)內存的收集器,如下圖所示,這節省了一些時間,但因為這些空間並不連接,bump-the-pointer也不奏效了,因此它需要使用一個free列表去記錄可使用的空間,然後在分配時去查找這個list。因此分配空間的操作效率相對要低一些,同時,這也會造成回收young generation的一些負擔,因此回收young generation時需要從young generation裏面復制一些存活的對象到old generation中,內存碎片的風險也增加了。

另外,與其它收集器不同的是,當old generation滿了之後,它並不會發起一個old generation的回收,相反,它會嘗試在還沒有滿的時候發起一次回收(因為在concurrent mark階段程序是並發運行地),以便能夠用完之前能夠完成回收,否則它將轉為使用與parallel collector 和serial collector一樣stop-the-world方法去回收這部分空間。為了避免這一點,CMS Collector定時記錄了一些垃圾回收的數據,比如回收的速率,然後在恰當的時候觸發回收操作,避免在用完的時候再進行回收。另外,當old generation占用超過一定程度之後,CMS Collector也會去發起一次回收操作,可以用-XX:CMSInitiatingOccupanyFraction=n,n是old generation大小的占比,默認是68。

總之,CMS能夠減少大部分程序由於回收工作而被暫停的時間 ,但結果的代價是:回收young generation變慢了,程序運行時間占比下降,以及更大的堆空間消耗。

增量式的回收

CMS Collector可以運行在增量回收的模式下,這種模式下,young generation回收過程的時候被分為幾個小塊的時間段。以減少stop-the-world時間。

CMS Collector適合那些對暫時時間比較敏感,允許GC操作並發地使用CPU,而且有大量存活對象的應用,比如web server。

必須使用-XX:+UseConcMarkSweepGC來顯示指定使用CMS Collector,如果需要運行在增量模式下,必須使用-XX:+CMSIncrementalMode參數。

默認堆大小

在-server模式下,jvm的默認初始heap大小是1/64的物理內存(最多1GB),默認最大堆大小是1/4物理內存大小(最大1GB)

-client模式下,默認4MB初始heap大小及64MB的最大堆大小。當然這些都可以通過命令參數進行覆蓋。

其它參數

另外還可以使用-XX:MaxGCPauseMillis=n來指定GC造成的最大停頓時間,這個時間不一定能完成,不能完成的話,Collector會在堆未占滿的情況觸發回收操作。

另外也可以使用-XX:GCTimeRatio=n來設置GC時間占比(GC:程序運行時間),比如GCTimeRatio=4的情況下,GC時間將最大占用20%的時間。和-XX:MaxGCPauseMillis一樣,如果不能滿足要求,Collector將在geneartion占滿之前觸發回收操作。

JVM GC Collector工作原理及優化