JVM原理和GC工作機制

JVM工作原理和特點主要是指操作系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境.

1.創建JVM裝載環境和配置

2.裝載JVM.dll

3.初始化JVM.dll並掛界到JNIENV(JNI調用接口)實例

4.調用JNIEnv實例裝載並處理class類。

  • JVM啓動流程

  • JVM基本結構

  • 內存模型

  • 編譯和解釋運行的概念

JVM啓動流程圖:

587f1e97-d4ed-4d7f-89c6-dabe944fed12

JVM結構、內存分配、垃圾回收算法、垃圾收集器。下面我們一一來看。


一、JVM結構

根據《java虛擬機規範》規定,JVM的基本結構一般如下圖所示:

96698196-0f6b-4ef6-9a20-85dd35d75f27

從左圖可知,JVM主要包括四個部分:

1.類加載器(ClassLoader):在JVM啓動時或者在類運行時將需要的class加載到JVM中。(右圖表示了從java源文件到JVM的整個過程,可配合理解。 關於類的加載機制,可以參考http://blog.csdn.net/tonytfjing/article/details/47212291

2.執行引擎:負責執行class文件中包含的字節碼指令(執行引擎的工作機制,這裏也不細說了,這裏主要介紹JVM結構);

3.內存區(也叫運行時數據區):是在JVM運行的時候操作所分配的內存區。運行時內存區主要可以劃分爲5個區域,如圖:

Java中的內存分配:

Java程序在運行時,需要在內存中的分配空間。爲了提高運算效率,就對數據進行了不同空間的劃分,因爲每一片區域都有特定的處理數據方式和內存管理方式。

具體劃分爲如下5個內存空間:(非常重要)

  • 方法區(Method Area):用於存儲類結構信息的地方,包括常量池、靜態變量、構造函數等。雖然JVM規範把方法區描述爲堆的一個邏輯部分, 但它卻有個別名non-heap(非堆),所以大家不要搞混淆了。方法區還包含一個運行時常量池。

  • java堆(Heap):存儲java實例或者對象的地方。這塊是GC的主要區域(後面解釋)。從存儲的內容我們可以很容易知道,方法區和堆是被所有java線程共享的。

  • java棧(Stack):java棧總是和線程關聯在一起,每當創建一個線程時,JVM就會爲這個線程創建一個對應的java棧。在這個java棧中又會包含多個棧幀,每運行一個方法就創建一個棧幀,用於存儲局部變量表、操作棧、方法返回值等。每一個方法從調用直至執行完成的過程,就對應一個棧幀在java棧中入棧到出棧的過程。所以java棧是現成私有的。

  • 程序計數器(PC Register):用於保存當前線程執行的內存地址。由於JVM程序是多線程執行的(線程輪流切換),所以爲了保證線程切換回來後,還能恢復到原先狀態,就需要一個獨立的計數器,記錄之前中斷的地方,可見程序計數器也是線程私有的。

  • 本地方法棧(Native Method Stack):和java棧的作用差不多,只不過是爲JVM使用到的native方法服務的。

4.本地方法接口:主要是調用C或C++實現的本地方法及返回結果。


二、內存分配

我覺得了解垃圾回收之前,得先了解JVM是怎麼分配內存的,然後識別哪些內存是垃圾需要回收,最後纔是用什麼方式回收。

Java的內存分配原理與C/C++不同,C/C++每次申請內存時都要malloc進行系統調用,而系統調用發生在內核空間,每次都要中斷進行切換,這需要一定的開銷,而Java虛擬機是先一次性分配一塊較大的空間,然後每次new時都在該空間上進行分配和釋放,減少了系統調用的次數,節省了一定的開銷,這有點類似於內存池的概念;二是有了這塊空間過後,如何進行分配和回收就跟GC機制有關了。

java一般內存申請有兩種:靜態內存和動態內存。很容易理解,編譯時就能夠確定的內存就是靜態內存,即內存是固定的,系統一次性分配,比如int類型變量;動態內存分配就是在程序執行時才知道要分配的存儲空間大小,比如java對象的內存空間。根據上面我們知道,java棧、程序計數器、本地方法棧都是線程私有的,線程生就生,線程滅就滅,棧中的棧幀隨着方法的結束也會撤銷,內存自然就跟着回收了。所以這幾個區域的內存分配與回收是確定的,我們不需要管的。但是java堆和方法區則不一樣,我們只有在程序運行期間才知道會創建哪些對象,所以這部分內存的分配和回收都是動態的。一般我們所說的垃圾回收也是針對的這一部分。

總之Stack的內存管理是順序分配的,而且定長,不存在內存回收問題;而Heap 則是爲java對象的實例隨機分配內存,不定長度,所以存在內存分配和回收的問題;


內存由 Perm 和 Heap 組成. 其中
Heap = {Old + NEW = { Eden , from, to }+Permanent }
JVM內存模型中分兩大塊,一塊是 NEW Generation, 另一塊是Old Generation. 在New Generation中,有一個叫Eden的空間,主要是用來存放新生的對象;還有兩個Survivor Spaces(from,to), 它們用來存放每次垃圾回收後存活下來的對象;’在Old Generation中,主要存放應用程序中生命週期長的內存對象;還有個Permanent Generation,主要用來放JVM自己的反射對象,比如類對象和方法對象等。


垃圾回收描述:
在New Generation塊中,垃圾回收一般用Copying的
算法速度快。每次GC的時候,存活下來的對象首先由Eden拷貝到某個Survivor Space, 當Survivor Space空間滿了後, 剩下的live對象就被直接拷貝到Old Generation中去。因此,每次GC後,Eden內存塊會被清空。在Old Generation塊中,垃圾回收一般用mark-compact的算法,速度慢些,但減少內存要求.
垃圾回收分多級,0級爲全部(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上爲部分垃圾回收,只會回收NEW中的垃圾,內存溢出通常發生於OLD段或Perm段垃圾回收後,仍然無內存空間容納新的Java對象的情況。


當一個URL被訪問時,內存申請過程如下:
A. JVM會試圖爲相關Java對象在Eden中初始化一塊內存區域
B. 當Eden空間足夠時,內存申請結束。否則到下一步
C. JVM試圖釋放在Eden中所有不活躍的對象(這屬於1或更高級的垃圾回收), 釋放後若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區
D. Survivor區被用來作爲Eden及OLD的中間交換區域,當OLD區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區
E. 當OLD區空間不夠時,JVM會在OLD區進行完全的垃圾收集(0級)
F. 完全垃圾收集後,若Survivor及OLD區仍然無法存放從Eden複製過來的部分對象,導致JVM無法在Eden區爲新對象創建內存區域,則出現”out of memory錯誤”


JVM調優建議:
ms/mx:定義YOUNG+OLD段的總尺寸,ms爲JVM啓動時YOUNG+OLD的內存大小;mx爲最大可佔用的YOUNG+OLD內存大小。在用戶生產環境上一般將這兩個值設爲相同,以減少運行期間系統在內存申請上所花的開銷。


NewSize/MaxNewSize:定義YOUNG段的尺寸,NewSize爲JVM啓動時YOUNG的內存大小;MaxNewSize爲最大可佔用的YOUNG內存大小。在用戶生產環境上一般將這兩個值設爲相同,以減少運行期間系統在內存申請上所花的開銷。


PermSize/MaxPermSize:定義Perm段的尺寸,PermSize爲JVM啓動時Perm的內存大小;MaxPermSize爲最大可佔用的Perm內存大小。在用戶生產環境上一般將這兩個值設爲相同,以減少運行期間系統在內存申請上所花的開銷。


SurvivorRatio:設置Survivor空間和Eden空間的比例
內存溢出的可能性


1. OLD段溢出
這種內存溢出是最常見的情況之一,產生的原因可能是:
1) 設置的內存參數過小(ms/mx, NewSize/MaxNewSize)
2) 程序問題
單個程序持續進行消耗內存的處理,如循環幾千次的字符串處理,對字符串處理應建議使用StringBuffer。此時不會報內存溢出錯,卻會使系統持續垃圾收集,無法處理其它請求,相關問題程序可通過Thread Dump獲取(見系統問題診斷一章)單個程序所申請內存過大,有的程序會申請幾十乃至幾百兆內存,此時JVM也會因無法申請到資源而出現內存溢出,對此首先要找到相關功能,然後交予程序員修改,要找到相關程序,必須在Apache日誌中尋找。
當Java對象使用完畢後,其所引用的對象卻沒有銷燬,使得JVM認爲他還是活躍的對象而不進行回收,這樣累計佔用了大量內存而無法釋放。由於目前市面上還沒有對系統影響小的內存分析工具,故此時只能和程序員一起定位。
2. Perm段溢出
通常由於Perm段裝載了大量的Servlet類而導致溢出,目前的解決辦法:
1) 將PermSize擴大,一般256M能夠滿足要求
2) 若別無選擇,則只能將servlet的路徑加到CLASSPATH中,但一般不建議這麼處理


3. C Heap溢出
系統對C Heap沒有限制,故C Heap發生問題時,Java進程所佔內存會持續增長,直到佔用所有可用系統內存
其他:
JVM有2個GC線程。第一個線程負責回收Heap的Young區。第二個線程在Heap不足時,遍歷Heap,將Young 區升級爲Older區。Older區的大小等於-Xmx減去-Xmn,不能將-Xms的值設的過大,因爲第二個線程被迫運行會降低JVM的性能。
爲什麼一些程序頻繁發生GC?有如下原因:
l         程序內調用了System.gc()或Runtime.gc()。
l         一些中間件軟件調用自己的GC方法,此時需要設置參數禁止這些GC。
l         Java的Heap太小,一般默認的Heap值都很小。
l         頻繁實例化對象,Release對象。此時儘量保存並重用對象,例如使用StringBuffer()和String()。
如果你發現每次GC後,Heap的剩餘空間會是總空間的50%,這表示你的Heap處於健康狀態。許多Server端的Java程序每次GC後最好能有65%的剩餘空間。



經驗之談:
1.Server端JVM最好將-Xms和-Xmx設爲相同值。爲了優化GC,最好讓-Xmn值約等於-Xmx的1/3[2]。
2.一個GUI程序最好是每10到20秒間運行一次GC,每次在半秒之內完成[2]。
注意:
1.增加Heap的大小雖然會降低GC的頻率,但也增加了每次GC的時間。並且GC運行時,所有的用戶線程將暫停,也就是GC期間,Java應用程序不做任何工作。
2.Heap大小並不決定進程的內存使用量。進程的內存使用量要大於-Xmx定義的值,因爲Java爲其他任務分配內存,例如每個線程的Stack等。
2.Stack的設定
每個線程都有他自己的Stack。


-Xss

每個線程的Stack大小


Stack的大小限制着線程的數量。如果Stack過大就好導致內存溢漏。-Xss參數決定Stack大小,例如-Xss1024K。如果Stack太小,也會導致Stack溢漏。


3.硬件環境
硬件環境也影響GC的效率,例如機器的種類,內存,swap空間,和CPU的數量。
如果你的程序需要頻繁創建很多transient對象,會導致JVM頻繁GC。這種情況你可以增加機器的內存,來減少Swap空間的使用[2]。
4.4種GC
第一種爲單線程GC,也是默認的GC。,該GC適用於單CPU機器。
第二種爲Throughput GC,是多線程的GC,適用於多CPU,使用大量線程的程序。第二種GC與第一種GC相似,不同在於GC在收集Young區是多線程的,但在Old區和第一種一樣,仍然採用單線程。-XX:+UseParallelGC參數啓動該GC。
第三種爲Concurrent Low Pause GC,類似於第一種,適用於多CPU,並要求縮短因GC造成程序停滯的時間。這種GC可以在Old區的回收同時,運行應用程序。-XX:+UseConcMarkSweepGC參數啓動該GC。
第四種爲Incremental Low Pause GC,適用於要求縮短因GC造成程序停滯的時間。這種GC可以在Young區回收的同時,回收一部分Old區對象。-Xincgc參數啓動該GC。



三、垃圾檢測、回收算法

垃圾收集器一般必須完成兩件事:檢測出垃圾;回收垃圾。怎麼檢測出垃圾?一般有以下幾種方法:

引用計數法:給一個對象添加引用計數器,每當有個地方引用它,計數器就加1;引用失效就減1。

好了,問題來了,如果我有兩個對象A和B,互相引用,除此之外,沒有其他任何對象引用它們,實際上這兩個對象已經無法訪問,即是我們說的垃圾對象。但是互相引用,計數不爲0,導致無法回收,所以還有另一種方法:

可達性分析算法:以根集對象爲起始點進行搜索,如果有對象不可達的話,即是垃圾對象。這裏的根集一般包括java棧中引用的對象、方法區常量池中引用的對象。


本地方法中引用的對象等。


總之,JVM在做垃圾回收的時候,會檢查堆中的所有對象是否會被這些根集對象引用,不能夠被引用的對象就會被垃圾收集器回收。一般回收算法也有如下幾種:



引用計數(Reference Counting):

比較古老的回收算法。原理是此對象有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數爲0的對象。此算法最致命的是無法處理循環引用的問題。

 

標記-清除(Mark-Sweep):

 

 標記清楚

 

此算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此算法需要暫停整個應用,同時,會產生內存碎片。



複製(Copying):

 

複製 

 

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


標記-整理(Mark-Compact):

 標記整理


此算法結合了“標記-清除”和“複製”兩個算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,把清除未標記對象並且把存活對象“壓縮”到堆的其中一塊,按順序排放。此算法避免了“標記-清除”的碎片問題,同時也避免了“複製”算法的空間問題。


按分區對待的方式分

增量收集(Incremental Collecting):實時垃圾回收算法,即:在應用進行的同時進行垃圾回收。不知道什麼原因JDK5.0中的收集器沒有使用這種算法的。

分代收集(Generational Collecting):基於對對象生命週期分析後得出的垃圾回收算法。把對象分爲年青代、年老代、持久代,對不同生命週期的對象使用不同的算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此算法的。


按系統線程分

串行收集:串行收集使用單線程處理所有垃圾回收工作,因爲無需多線程交互,實現容易,而且效率比較高。但是,其侷限性也比較明顯,即無法使用多處理器的優勢,所以此收集適合單處理器機器。當然,此收集器也可以用在小數據量(100M左右)情況下的多處理器機器上。

 

並行收集:並行收集使用多線程處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能體現出並行收集器的優勢。(串型收集的併發版本,需要暫停jvm) 並行paralise指的是多個任務在多個cpu中一起並行執行,最後將結果合併。效率是N倍。

 

併發收集:相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個運行環境,而只有垃圾回收程序在運行,因此,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因爲堆越大而越長。(和並行收集不同,併發只有在開頭和結尾會暫停jvm)併發concurrent指的是多個任務在一個cpu僞同步執行,但其實是串行調度的,效率並非直接是N倍。


分代垃圾回收

這是當前商業虛擬機常用的垃圾收集算法。分代的垃圾回收策略,是基於這樣一個事實:不同的對象的生命週期是不一樣的。因此,不同生命週期的對象可以採取不同的收集方式,以便提高回收效率。

在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鉤,因此生命週期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命週期會比較短,比如:String對象,由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。

 試想,在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因爲每次回收都需要遍歷所有存活對象,但實際上,對於生命週期長的對象而言,這種遍歷是沒有效果的,因爲可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收採用分治的思想,進行代的劃分,把不同生命週期的對象放在不同代上,不同代上採用最適合它的垃圾回收方式進行回收。

jvm分代


如圖所示:

 

    虛擬機中的共劃分爲三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。


年輕代:

    所有新生成的對象首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另外一個Survivor區,當這個Survivor區也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的對象,將被複制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。同時,根據程序需要,Survivor區是可以配置爲多個的(多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。


年老代:

    在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。


持久代:

    用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如hibernate等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=<N>進行設置。


什麼情況下觸發垃圾回收 

由於對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種類型:Scavenge GCFull GC


Scavenge GC

    一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因爲大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裏需要使用速度快、效率高的算法,使Eden去能儘快空閒出來。


Full GC

    對整個堆進行整理,包括Young、Tenured和Perm。Full GC因爲需要對整個對進行回收,所以比Scavenge GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:

· 年老代(Tenured)被寫滿

· 持久代(Perm)被寫滿 

· System.gc()被顯示調用 

·上一次GC之後Heap的各域分配策略動態變化

分代垃圾回收流程1

 

 

 

 

分代垃圾回收流程2

 

 

 

分代垃圾回收流程3

 

 

分代垃圾回收流程

= G1 ===================================

傳說中的G1,傳說中的low-pause垃圾收集。Java SE 6的update14版本中已經包含測試版,可以在啓動時加JVM參數來啓用

-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC


http://www.blogjava.net/BlueDavy/archive/2009/03/11/259230.html

本文摘自《構建高性能的大型分佈式Java應用》一書,Garbage First簡稱G1,它的目標是要做到儘量減少GC所導致的應用暫停的時間,讓應用達到準實時的效果,同時保持JVM堆空間的利用率,將作爲CMS的替代者在JDK 7中閃亮登場,其最大的特色在於允許指定在某個時間段內GC所導致的應用暫停的時間最大爲多少,例如在100秒內最多允許GC導致的應用暫停時間爲1秒,這個特性對於準實時響應的系統而言非常的吸引人,這樣就再也不用擔心繫統突然會暫停個兩三秒了。

G1要做到這樣的效果,也是有前提的,一方面是硬件環境的要求,必須是多核的CPU以及較大的內存(從規範來看,512M以上就滿足條件了),另外一方面是需要接受吞吐量的稍微降低,對於實時性要求高的系統而言,這點應該是可以接受的。

爲了能夠達到這樣的效果,G1在原有的各種GC策略上進行了吸收和改進,在G1中可以看到增量收集器和CMS的影子,但它不僅僅是吸收原有GC策略的優點,並在此基礎上做出了很多的改進,簡單來說,G1吸收了增量GC以及CMS的精髓,將整個jvm Heap劃分爲多個固定大小的region,掃描時採用Snapshot-at-the-beginning的併發marking算法(具體在後面內容詳細解釋)對整個heap中的region進行mark,回收時根據region中活躍對象的bytes進行排序,首先回收活躍對象bytes小以及回收耗時短(預估出來的時間)的region,回收的方法爲將此region中的活躍對象複製到另外的region中,根據指定的GC所能佔用的時間來估算能回收多少region,這點和以前版本的Full GC時得處理整個heap非常不同,這樣就做到了能夠儘量短時間的暫停應用,又能回收內存,由於這種策略在回收時首先回收的是垃圾對象所佔空間最多的region,因此稱爲Garbage First。

看完上面對於G1策略的簡短描述,並不能清楚的掌握G1,在繼續詳細看G1的步驟之前,必須先明白G1對於JVM Heap的改造,這些對於習慣了劃分爲new generation、old generation的大家來說都有不少的新意。

G1將Heap劃分爲多個固定大小的region,這也是G1能夠實現控制GC導致的應用暫停時間的前提,region之間的對象引用通過remembered set來維護,每個region都有一個remembered set,remembered set中包含了引用當前region中對象的region的對象的pointer,由於同時應用也會造成這些region中對象的引用關係不斷的發生改變,G1採用了Card Table來用於應用通知region修改remembered sets,Card Table由多個512字節的Card構成,這些Card在Card Table中以1個字節來標識,每個應用的線程都有一個關聯的remembered set log,用於緩存和順序化線程運行時造成的對於card的修改,另外,還有一個全局的filled RS buffers,當應用線程執行時修改了card後,如果造成的改變僅爲同一region中的對象之間的關聯,則不記錄remembered set log,如造成的改變爲跨region中的對象的關聯,則記錄到線程的remembered set log,如線程的remembered set log滿了,則放入全局的filled RS buffers中,線程自身則重新創建一個新的remembered set log,remembered set本身也是一個由一堆cards構成的哈希表。

儘管G1將Heap劃分爲了多個region,但其默認採用的仍然是分代的方式,只是僅簡單的劃分爲了年輕代(young)和非年輕代,這也是由於G1仍然堅信大多數新創建的對象都是不需要長的生命週期的,對於應用新創建的對象,G1將其放入標識爲young的region中,對於這些region,並不記錄remembered set logs,掃描時只需掃描活躍的對象,G1在分代的方式上還可更細的劃分爲:fully young或partially young,fully young方式暫停的時候僅處理young regions,partially同樣處理所有的young regions,但它還會根據允許的GC的暫停時間來決定是否要加入其他的非young regions,G1是運行到fully-young方式還是partially young方式,外部是不能決定的,在啓動時,G1採用的爲fully-young方式,當G1完成一次Concurrent Marking後,則切換爲partially young方式,隨後G1跟蹤每次回收的效率,如果回收fully-young中的regions已經可以滿足內存需要的話,那麼就切換回fully young方式,但當heap size的大小接近滿的情況下,G1會切換到partially young方式,以保證能提供足夠的內存空間給應用使用。

除了分代方式的劃分外,G1還支持另外一種pure G1的方式,也就是不進行代的劃分,pure方式和分代方式的具體不同在下面的具體執行步驟中進行描述。

掌握了這些概念後,繼續來看G1的具體執行步驟:

1.         Initial Marking

G1對於每個region都保存了兩個標識用的bitmap,一個爲previous marking bitmap,一個爲next marking bitmap,bitmap中包含了一個bit的地址信息來指向對象的起始點。

開始Initial Marking之前,首先併發的清空next marking bitmap,然後停止所有應用線程,並掃描標識出每個region中root可直接訪問到的對象,將region中top的值放入next top at mark start(TAMS)中,之後恢復所有應用線程。

觸發這個步驟執行的條件爲:

l  G1定義了一個JVM Heap大小的百分比的閥值,稱爲h,另外還有一個H,H的值爲(1-h)*Heap Size,目前這個h的值是固定的,後續G1也許會將其改爲動態的,根據jvm的運行情況來動態的調整,在分代方式下,G1還定義了一個u以及soft limit,soft limit的值爲H-u*Heap Size,當Heap中使用的內存超過了soft limit值時,就會在一次clean up執行完畢後在應用允許的GC暫停時間範圍內儘快的執行此步驟;

l  在pure方式下,G1將marking與clean up組成一個環,以便clean up能充分的使用marking的信息,當clean up開始回收時,首先回收能夠帶來最多內存空間的regions,當經過多次的clean up,回收到沒多少空間的regions時,G1重新初始化一個新的marking與clean up構成的環。

2.         Concurrent Marking

按照之前Initial Marking掃描到的對象進行遍歷,以識別這些對象的下層對象的活躍狀態,對於在此期間應用線程併發修改的對象的以來關係則記錄到remembered set logs中,新創建的對象則放入比top值更高的地址區間中,這些新創建的對象默認狀態即爲活躍的,同時修改top值。

3.         Final Marking Pause

當應用線程的remembered set logs未滿時,是不會放入filled RS buffers中的,在這樣的情況下,這些remebered set logs中記錄的card的修改就會被更新了,因此需要這一步,這一步要做的就是把應用線程中存在的remembered set logs的內容進行處理,並相應的修改remembered sets,這一步需要暫停應用,並行的運行。

4.         Live Data Counting and Cleanup

值得注意的是,在G1中,並不是說Final Marking Pause執行完了,就肯定執行Cleanup這步的,由於這步需要暫停應用,G1爲了能夠達到準實時的要求,需要根據用戶指定的最大的GC造成的暫停時間來合理的規劃什麼時候執行Cleanup,另外還有幾種情況也是會觸發這個步驟的執行的:

l  G1採用的是複製方法來進行收集,必須保證每次的”to space”的空間都是夠的,因此G1採取的策略是當已經使用的內存空間達到了H時,就執行Cleanup這個步驟;

l  對於full-young和partially-young的分代模式的G1而言,則還有情況會觸發Cleanup的執行,full-young模式下,G1根據應用可接受的暫停時間、回收young regions需要消耗的時間來估算出一個yound regions的數量值,當JVM中分配對象的young regions的數量達到此值時,Cleanup就會執行;partially-young模式下,則會盡量頻繁的在應用可接受的暫停時間範圍內執行Cleanup,並最大限度的去執行non-young regions的Cleanup。

這一步中GC線程並行的掃描所有region,計算每個region中低於next TAMS值中marked data的大小,然後根據應用所期望的GC的短延時以及G1對於region回收所需的耗時的預估,排序region,將其中活躍的對象複製到其他region中。

 

G1爲了能夠儘量的做到準實時的響應,例如估算暫停時間的算法、對於經常被引用的對象的特殊處理等,G1爲了能夠讓GC既能夠充分的回收內存,又能夠儘量少的導致應用的暫停,可謂費盡心思,從G1的論文中的性能評測來看效果也是不錯的,不過如果G1能允許開發人員在編寫代碼時指定哪些對象是不用mark的就更完美了,這對於有巨大緩存的應用而言,會有很大的幫助,G1將隨JDK 6 Update 14 beta發佈。

= CMS ==================================

http://www.iteye.com/topic/1119491

1.總體介紹:


CMS(Concurrent Mark-Sweep)是以犧牲吞吐量爲代價來獲得最短回收停頓時間的垃圾回收器。併發意味着除了開頭和結束階段,需要暫停JVM,其它時間gc和應用一起執行。對於要求服務器響應速度的應用上,這種垃圾回收器非常適合。在啓動JVM參數加上-XX:+UseConcMarkSweepGC ,這個參數表示對於老年代的回收採用CMS。CMS採用的基礎算法是:標記—清除。默認會開啓 -XX :+UseParNewGC,在年輕代使用並行複製收集。

2.CMS過程:

  • 初始標記(STW initial mark)

  • 併發標記(Concurrent marking)

  • 併發預清理(Concurrent precleaning)

  • 重新標記(STW remark)

  • 併發清理(Concurrent sweeping)

  • 併發重置(Concurrent reset)

初始標記 :在這個階段,需要虛擬機停頓正在執行的任務,官方的叫法STW(Stop The Word)。這個過程從垃圾回收的"根對象"開始,只掃描到能夠和"根對象"直接關聯的對象,並作標記。所以這個過程雖然暫停了整個JVM,但是很快就完成了。

併發標記 :這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記。併發標記階段,應用程序的線程和併發標記的線程併發執行,所以用戶不會感受到停頓。

併發預清理 :併發預清理階段仍然是併發的。在這個階段,虛擬機查找在執行併發標記階段新進入老年代的對象(可能會有一些對象從新生代晉升到老年代, 或者有一些對象被分配到老年代)。通過重新掃描,減少下一個階段"重新標記"的工作,因爲下一個階段會Stop The World。

重新標記 :這個階段會暫停虛擬機,收集器線程掃描在CMS堆中剩餘的對象。掃描從"跟對象"開始向下追溯,並處理對象關聯。

併發清理 :清理垃圾對象,這個階段收集器線程和應用程序線程併發執行。

併發重置 :這個階段,重置CMS收集器的數據結構,等待下一次垃圾回收。

 

CSM執行過程: 

3.CMS缺點

  • CMS回收器採用的基礎算法是Mark-Sweep。所有CMS不會整理、壓縮堆空間。這樣就會有一個問題:經過CMS收集的堆會產生空間碎片。 CMS不對堆空間整理壓縮節約了垃圾回收的停頓時間,但也帶來的堆空間的浪費。爲了解決堆空間浪費問題,CMS回收器不再採用簡單的指針指向一塊可用堆空 間來爲下次對象分配使用。而是把一些未分配的空間彙總成一個列表,當JVM分配對象空間的時候,會搜索這個列表找到足夠大的空間來hold住這個對象。

  • 需要更多的CPU資源。從上面的圖可以看到,爲了讓應用程序不停頓,CMS線程和應用程序線程併發執行,這樣就需要有更多的CPU,單純靠線程切 換是不靠譜的。並且,重新標記階段,爲空保證STW快速完成,也要用到更多的甚至所有的CPU資源。當然,多核多CPU也是未來的趨勢!

  • CMS的另一個缺點是它需要更大的堆空間。因爲CMS標記階段應用程序的線程還是在執行的,那麼就會有堆空間繼續分配的情況,爲了保證在CMS回 收完堆之前還有空間分配給正在運行的應用程序,必須預留一部分空間。也就是說,CMS不會在老年代滿的時候纔開始收集。相反,它會嘗試更早的開始收集,已 避免上面提到的情況:在回收完成之前,堆沒有足夠空間分配!默認當老年代使用68%的時候,CMS就開始行動了。 – XX:CMSInitiatingOccupancyFraction =n 來設置這個閥值。

總得來說,CMS回收器減少了回收的停頓時間,但是降低了堆空間的利用率。



= 調試工具 ==================================


jmap


jmap -heap pid  (不能觀察G1模式)


using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 2147483648 (2048.0MB)
   NewSize          = 268435456 (256.0MB)
   MaxNewSize       = 268435456 (256.0MB)
   OldSize          = 805306368 (768.0MB)
   NewRatio         = 7
   SurvivorRatio    = 8
   PermSize         = 134217728 (128.0MB)
   MaxPermSize      = 134217728 (128.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 241631232 (230.4375MB)
   used     = 145793088 (139.03912353515625MB)
   free     = 95838144 (91.39837646484375MB)
   60.33702133340114% used
Eden Space:
   capacity = 214827008 (204.875MB)
   used     = 132689456 (126.54252624511719MB)
   free     = 82137552 (78.33247375488281MB)
   61.7657236095752% used
From Space:
   capacity = 26804224 (25.5625MB)
   used     = 13103632 (12.496597290039062MB)
   free     = 13700592 (13.065902709960938MB)
   48.886444166411984% used
To Space:
   capacity = 26804224 (25.5625MB)
   used     = 0 (0.0MB)
   free     = 26804224 (25.5625MB)
   0.0% used
concurrent mark-sweep generation: (old區)
   capacity = 1879048192 (1792.0MB)
   used     = 1360638440 (1297.6059341430664MB)
   free     = 518409752 (494.3940658569336MB)
   72.41104543209076% used
Perm Generation:
   capacity = 134217728 (128.0MB)
   used     = 65435064 (62.40373992919922MB)
   free     = 68782664 (65.59626007080078MB)
   48.75292181968689% used



jmap -histo:live pid


num     #instances         #bytes  class name
----------------------------------------------
   1:       3148147      209172848  [B
   2:       2584345      144723320  java.lang.ref.SoftReference
   3:       2578827      123783696  sun.misc.CacheEntry
   4:        781560      112544640  com.sun.NET.ssl.internal.ssl.SSLSessionImpl
   5:       1385200       89970592  [C
   6:        783287       87807200  [Ljava.util.Hashtable$Entry;
   7:       1421399       56855960  java.lang.String
   8:            12       56828880  [Lsun.misc.CacheEntry;
   9:       2343358       56240592  com.sun.net.ssl.internal.ssl.SessionId
  10:        783185       50123840  java.util.Hashtable
  11:        783094       50118016  java.lang.ref.Finalizer
  12:        287243       36086720  [Ljava.lang.Object;
  13:        263376       33712128  org.apache.commons.pool.impl.GenericObjectPool



jstat


jstat -gccause 31169 60000 1000


(sweep 1,2) (Eden) (Old) (Perm) (Young GC, GCTime)(Full GC, GCTime)

  S0         S1     E         O         P     YGC     YGCT     FGC    FGCT      GCT             LGCC                 GCC                 
 48.80   0.00  68.94  69.55  48.86  30202  725.319 51835 5083.298 5808.616 unknown GCCause      No GC               
 47.98   0.00  37.47  69.61  48.86  30206  725.385 51835 5083.298 5808.682 unknown GCCause      No GC               
 50.73   0.00  51.72  69.65  48.86  30210  725.459 51835 5083.298 5808.757 unknown GCCause      No GC               
  0.00  50.02  82.67  69.60  48.84  30213  725.508 51836 5091.572 5817.081 unknown GCCause      No GC               


jstat -gcutil $pid


  S0         S1     E         O       P       YGC     YGCT    FGC    FGCT     GCT   
 74.79   0.00  95.15   0.86  37.35      2        0.112     0        0.000      0.112


O = old occupied

YGC = young gc time ( new part )

YGCT = young gc total cost time

FGC = full gc time ( old part )

FGCT = full gc total cost time

GCT = all gc cost time


jvisualvm

window下啓動遠程監控,並在被監控服務端,啓動jstatd服務。


創建安全策略文件,並命名爲jstatd.all.policy
grant codebase "file:${java.home}/../lib/tools.jar" {
    permission java.security.AllPermission;
};

jstatd -J-Djava.security.policy=jstatd.all.policy -p 8080 &


======================== Tunning =================

典型配置:

-server -Xmx2g -Xms2g -Xmn512m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章