JVM 基礎知識

JVM 基礎知識(GC)

分類:

目錄(?)[+]html

幾年前寫過一篇關於JVM調優的文章,前段時間拿出來看了看,又添加了一些東西。忽然發現,基礎真的很重要。學習的過程是一個由表及裏,再由裏及表的過 程,所謂的「溫故而知新」。而真正能走完這個輪迴的人,也就能稱爲大牛或專家了。這個過程可能來來回回,這就是所謂「螺旋上升」,而每一次輪迴都有新的發 現。java

 

這回添加的東西主要集中在基礎的一些問題上,還有一些這兩年思考的問題。這些問題可能平時咱們不會刻意去想,可是真正看清楚了,卻發現仍是大有裨益的,但願對你們都有幫助~算法


1、基礎概念數組

數據類型緩存

Java虛擬機中,數據類型能夠分爲兩類:基本類型和引用類型。基本類型的變量保存原始值,即:他表明的值就是數值自己;而引用類型的變量保存引用值。「引用值」表明了某個對象的引用,而不是對象自己,對象自己存放在這個引用值所表示的地址的位置。服務器

基本類型包括:byte、short、int、long、char、float、double、Boolean、returnAddress數據結構

引用類型包括:類類型、接口類型、數組多線程


堆和棧併發

堆和棧是程序運行的關鍵,頗有必要把他們的關係說清楚。app

1)棧是運行時的單位,而堆是存儲的單位。

2)棧解決程序的運行問題,即程序如何執行,或者說如何處理數據;堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。


在Java中一個線程就會相應有一個線程棧與之對應,這點很容易理解,由於不一樣的線程執行邏輯有所不一樣,所以須要一個獨立的線程棧。而堆則是全部線程共享 的。棧由於是運行單位,所以裏面存儲的信息都是跟當前線程(或程序)相關信息的。包括局部變量、程序運行狀態、方法返回值等等;而堆只負責存儲對象信息。


爲何要把堆和棧區分出來呢?棧中不是也能夠存儲數據嗎?

1)從軟件設計的角度看,棧表明了處理邏輯,而堆表明了數據。這樣分開,使得處理邏輯更爲清晰。分而治之的思想。這種隔離、模塊化的思想在軟件設計的方方面面都有體現。

2)堆與棧的分離,使得堆中的內容能夠被多個棧共享(也能夠理解爲多個線程訪問同一個對象)。這種共享的收益是不少的。一方面這種共享提供了一種有效的數據交互方式(如:共享內存),另外一方面,堆中的共享常量和緩存能夠被全部棧訪問,節省了空間。

3)棧由於運行時的須要,好比保存系統運行的上下文,須要進行地址段的劃分。因爲棧只能向上增加,所以就會限制住棧存儲內容的能力。而堆不一樣,堆中的對象是能夠根據須要動態增加的,所以棧和堆的拆分,使得動態增加成爲可能,相應棧中只需記錄堆中的一個地址便可。

4)面向對象就是堆和棧的完美結合。其實,面向對象方式的程序與之前結構化的程序在執行上沒有任何區別。可是,面向對象的引入,使得對待問題的思考方式發生了改變,而更接近於天然方式的思考。當咱們把對象拆開,你會發現,對象的屬性其實就是數據,存放在堆中;而對象的行爲(方法),就是運行邏輯,放在棧中。咱們在編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。不得不認可,面向對象的設計,確實很美。


在Java中,Main函數就是棧的起始點,也是程序的起始點。

程序要運行老是有一個起點的。同C語言同樣,java中的Main就是那個起點。不管什麼java程序,找到main就找到了程序執行的入口


堆中存什麼?棧中存什麼?

堆中存的是對象。棧中存的是基本數據類型和堆中對象的引用。一個對象的大小是不可估計的,或者說是能夠動態變化的,可是在棧中,一個對象只對應了一個4byte的引用(堆棧分離的好處)

爲何不把基本類型放堆中呢?由於其佔用的空間通常是1~8個字節,須要空間比較少,並且由於是基本類型,因此不會出現動態增加的狀況——長度固定,所以 棧中存儲就夠了,若是把他存在堆中是沒有什麼意義的(還會浪費空間,後面說明)。能夠這麼說,基本類型和對象的引用都是存放在棧中,並且都是幾個字節的一 個數,所以在程序運行時,他們的處理方式是統一的。可是基本類型、對象引用和對象自己就有所區別了,由於一個是棧中的數據一個是堆中的數據。最多見的一個 問題就是,Java中參數傳遞時的問題。


Java中的參數傳遞時傳值呢?仍是傳引用?     

1)不要試圖與C進行類比,Java中沒有指針的概念

2)程序運行永遠都是在棧中進行的,於是參數傳遞時,只存在傳遞基本類型和對象引用的問題,不會直接傳對象自己。

明確以上兩點後。Java在方法調用傳遞參數時,由於沒有指針,因此它都是進行傳值調用(這點能夠參考C的傳值調用)。所以,不少書裏面都說Java是進行傳值調用,這點沒有問題,並且也簡化的C中複雜性。 

可是傳引用的錯覺是如何形成的呢?在運行棧中,基本類型和引用的處理是同樣的,都是傳值, 因此,若是是傳引用的方法調用,也同時能夠理解爲「傳引用值」的傳值調用,即引用的處理跟基本類型是徹底同樣的。可是當進入被調用方法時,被傳遞的這個引 用的值,被程序解釋(或者查找)到堆中的對象,這個時候纔對應到真正的對象。若是此時進行修改,修改的是引用對應的對象,而不是引用自己,即:修改的是堆 中的數據。因此這個修改是能夠保持的了。

對象,從某種意義上說,是由基本類型組成的。能夠把一個對象看做爲一棵樹,對象的屬性若是仍是對象,則仍是一顆樹(即非葉子節點),基本類型則爲樹的葉子 節點。程序參數傳遞時,被傳遞的值自己都是不能進行修改的,可是,若是這個值是一個非葉子節點(即一個對象引用),則能夠修改這個節點下面的全部內容。

 

堆和棧中,棧是程序運行最根本的東西。程序運行能夠沒有堆,可是不能沒有棧。而堆是爲棧進行數據存儲服務,說白了堆就是一塊共享的內存。不過,正是由於堆和棧的分離的思想,才使得Java的垃圾回收成爲可能。

Java中,棧的大小經過-Xss來設置,當棧中存儲數據比較多時,須要適當調大這個值,不然會出現java.lang.StackOverflowError異常。常見的出現這個異常的是沒法返回的遞歸,由於此時棧中保存的信息都是方法返回的記錄點。



Java對象的大小

基本數據的類型的大小是固定的,對於非基本類型的Java對象,其大小就值得商榷。

在Java中,一個空Object對象的大小是8byte,這個大小隻是保存堆中一個沒有任何屬性的對象的大小。看下面語句:

Object ob = new Object();

這樣在程序中完成了一個Java對象的生命,可是它所佔的空間爲:4byte + 8byte

4byte,是上面部分所說的Java棧中保存引用的所須要的空間。

8byte,是Java堆中對象的信息。由於全部的Java非基本類型的對象都須要默認繼承Object對象,所以不論什麼樣的Java對象,其大小都必須是大於8byte。

有了Object對象的大小,咱們就能夠計算其餘對象的大小了。

Class NewObject {

       int count;

       boolean flag;

       Object obj;

}

這裏須要注意一下:基本類型的包裝類型的大小,由於這種包裝類型已經成爲對象了,所以須要把他們做爲對象來看待。包裝類型的大小至少是12byte(聲明 一個空Object至少須要的空間),並且12byte沒有包含任何有效信息,同時,由於Java對象大小是8的整數倍,所以一個基本類型包裝類的大小至 少是16byte。這個內存佔用是很恐怖的,它是使用基本類型的N倍(N>2),有些類型的內存佔用更是誇張(隨便想下就知道了)。所以,可能的話 應儘可能少使用包裝類。在JDK5.0之後,由於加入了自動類型裝換,所以,Java虛擬機會在存儲方面進行相應的優化。


引用類型

對象引用類型分爲:強引用、軟引用、弱引用、虛引用

強引用:就是咱們通常聲明對象是時虛擬機生成的引用,強引用環境下,垃圾回收時須要嚴格判斷當前對象是否被強引用,若是被強引用,則不會被垃圾回收

軟引用軟 引用通常被作爲緩存來使用。與強引用的區別是,軟引用在垃圾回收時,虛擬機會根據當前系統的剩餘內存來決定是否對軟引用進行回收。若是剩餘內存比較緊張, 則虛擬機會回收軟引用所引用的空間;若是剩餘內存相對富裕,則不會進行回收。換句話說,虛擬機在發生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)併發收集:相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工做時,須要暫停整個運行環境,而只有垃圾回收程序在運行,所以,系統在垃圾回收時會有明顯的暫停,並且暫停時間會由於堆越大而越長。


如何區分垃圾

上面說到的「引用計數」法,經過統計控制生成對象和刪除對象時的引用數來判斷。垃圾回收程序收集計數爲0的對象便可。可是這種方法沒法解決循環引用。所 以,後來實現的垃圾判斷算法中,都是從程序運行的根節點出發,遍歷整個對象引用,查找存活的對象。那麼在這種方式的實現中,垃圾回收從哪兒開始的呢?即, 從哪兒開始查找哪些對象是正在被當前系統使用的。上面分析的堆和棧的區別,其中棧是真正進行程序執行地方,因此要獲取哪些對象正在被使用,則須要從 Java棧開始。同時,一個棧是與一個線程對應的,所以,若是有多個線程的話,則必須對這些線程對應的全部的棧進行檢查。

同時,除了棧外,還有系統運行時的寄存器等,也是存儲程序運行數據的。這樣,以棧或寄存器中的引用爲起點,咱們能夠找到堆中的對象,又從這些對象找到對堆 中其餘對象的引用,這種引用逐步擴展,最終以null引用或者基本類型結束,這樣就造成了一顆以Java棧中引用所對應的對象爲根節點的一顆對象樹,若是 棧中有多個引用,則最終會造成多顆對象樹。在這些對象樹上的對象,都是當前系統運行所須要的對象,不能被垃圾回收。而其餘剩餘對象,則能夠視爲沒法被引用 到的對象,能夠被當作垃圾進行回收。

所以,垃圾回收的起點是一些根對象(java棧, 靜態變量, 寄存器...)。而最簡單的Java棧就是Java程序執行的main函數。這種回收方式,也是上面提到的「標記-清除」的回收方式

 

如何處理碎片

因爲不一樣Java對象存活時間是不必定的,所以,在程序運行一段時間之後,若是不進行內存整理,就會出現零散的內存碎片。碎片最直接的問題就是會致使沒法 分配大塊的內存空間,以及程序運行效率下降。因此,在上面提到的基本垃圾回收算法中,「複製」方式和「標記-整理」方式,均可以解決碎片的問題。

 

如何解決同時存在的對象建立和對象回收問題

垃圾回收線程是回收內存的,而程序運行線程則是消耗(或分配)內存的,一個回收內存,一個分配內存,從這點看,二者是矛盾的。所以,在現有的垃圾回收方式 中,要進行垃圾回收前,通常都須要暫停整個應用(即:暫停內存的分配),而後進行垃圾回收,回收完成後再繼續應用。這種實現方式是最直接,並且最有效的解 決兩者矛盾的方式。

可是這種方式有一個很明顯的弊端,就是當堆空間持續增大時,垃圾回收的時間也將會相應的持續增大,對應應用暫停的時間也會相應的增大。一些對相應時間要求 很高的應用,好比最大暫停時間要求是幾百毫秒,那麼當堆空間大於幾個G時,就頗有可能超過這個限制,在這種狀況下,垃圾回收將會成爲系統運行的一個瓶頸。 爲解決這種矛盾,有了併發垃圾回收算法,使用這種算法,垃圾回收線程與程序運行線程同時運行。在這種方式下,解決了暫停的問題,可是由於須要在新生成對象 的同時又要回收對象,算法複雜性會大大增長,系統的處理能力也會相應下降,同時,「碎片」問題將會比較難解決。


爲何要分代

分代的垃圾回收策略,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。

在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區是能夠配置爲多個的(多於兩個),這樣能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能。

(2)年老代:

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

(3)持久代:

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

 


什麼狀況下觸發垃圾回收

因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。GC有兩種類型:Scavenge GC和Full GC

(1)Scavenge GC

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

 

(2)Full GC

    對整個堆進行整理,包括Young、Tenured和Perm。Full GC由於須要對整個對進行回收,因此比Scavenge GC要慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於FullGC的調節。有以下緣由可能致使Full GC:

· 年老代(Tenured)被寫滿

· 持久代(Perm)被寫滿 

· System.gc()被顯示調用 

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


分代垃圾回收流程示意

 

 

選擇合適的垃圾收集算法

(1)串行收集器

 

用單線程處理全部垃圾回收工做,由於無需多線程交互,因此效率比較高。可是,也沒法使用多處理器的優點,因此此收集器適合單處理器機器。固然,此收集器也能夠用在小數據量(100M左右)狀況下的多處理器機器上。可使用-XX:+UseSerialGC打開。

 

(2)並行收集器

 

 

對年輕代進行並行垃圾回收,所以能夠減小垃圾回收時間。通常在多線程多處理器機器上使用。使用-XX:+UseParallelGC.打開。並行收集器在 J2SE5.0第六6更新上引入,在Java SE6.0中進行了加強--能夠對年老代進行並行收集。若是年老代不使用併發收集的話,默認是使用單線程進行垃圾回收,所以會制約擴展能力。使用 -XX:+UseParallelOldGC打開。

使用-XX:ParallelGCThreads=<N>設置並行垃圾回收的線程數。此值能夠設置與機器處理器數量相等。

此收集器能夠進行以下配置:

最大垃圾回收暫停:指定垃圾回收時的最長暫停時間,經過-XX:MaxGCPauseMillis=<N>指定。<N>爲毫秒.若是指定了此值的話,堆大小和垃圾回收相關參數會進行調整以達到指定值。設定此值可能會減小應用的吞吐量。

吞吐量吞吐量爲垃圾回收時間與非垃圾回收時間的比值,經過-XX:GCTimeRatio=<N>來設定,公式爲1/(1+N)。例如,-XX:GCTimeRatio=19時,表示5%的時間用於垃圾回收。默認狀況爲99,即1%的時間用於垃圾回收。


(3)併發收集器

能夠保證大部分工做都併發進行(應用不中止),垃圾回收只暫停不多的時間,此收集器適合對響應時間要求比較高的中、大規模應用。使用-XX:+UseConcMarkSweepGC打開。

併發收集器主要減小年老代的暫停時間,他在應用不中止的狀況下使用獨立的垃圾回收線程,跟蹤可達對象。在每一個年老代垃圾回收週期中,在收集初期併發收集器 會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程當中多個線程同時進行垃圾回收工做。併發收集器使用處理器換來短 暫的停頓時間。在一個N個處理器的系統上,併發收集部分使用K/N個可用處理器進行回收,通常狀況下1<=K<=N/4。在只有一個處理器的 主機上使用併發收集器,設置爲incremental mode模式也可得到較短的停頓時間。 

浮動垃圾:因爲在應用運行的同時進行垃圾回收,因此有些垃圾可能在垃圾回收進行完成時產生,這樣就形成了「Floating Garbage」,這些垃圾須要在下次垃圾回收週期時才能回收掉。因此,併發收集器通常須要20%的預留空間用於這些浮動垃圾。

 Concurrent Mode Failure:併發收集器在應用運行時進行收集,因此須要保證堆在垃圾回收的這段時間有足夠的空間供程序使用,不然,垃圾回收還未完成,堆空間先滿了。這種狀況下將會發生「併發模式失敗」,此時整個應用將會暫停,進行垃圾回收。

啓動併發收集器:由於併發收集在應用運行時進行收集,因此必須保證收集完成以前有足夠的內存空間供程序使用,不然會出現「Concurrent Mode Failure」。經過設置-XX:CMSInitiatingOccupancyFraction=<N>指定還有多少剩餘堆時開始執行並 發收集

 

適用場景

(1)串行處理器:

--適用狀況:數據量比較小(100M左右);單處理器下而且對響應時間無要求的應用。 
--缺點:只能用於小型應用

 

(2)並行處理器:

--適用狀況:「對吞吐量有高要求」,多CPU、對應用響應時間無要求的中、大型應用。舉例:後臺處理、科學計算。 
--缺點:垃圾收集過程當中應用響應時間可能加長

 

(3)併發處理器:

--適用狀況:「對響應時間有高要求」,多CPU、對應用響應時間有較高要求的中、大型應用。舉例:Web服務器/應用服務器、電信交換、集成開發環境。


小結

並行收集器 )

 

concurrent collector
(
併發收集器)

介紹

使用單線程去完成全部的gc工做,沒有線程間的通訊,這種方式會相對高效

使用多線程的方式,利用多CUP來提升GC的效率,主要以到達必定的吞吐量爲目標

使用多線程的方式,利用多CUP來提升GC的效率,併發完成大部分工做,使得gc pause

適用場景

單處理器機器且沒有pause time的要求

適用於科學技術和後臺處理
有中規模/大規模數據集大小的應用且運行在多處理器上,關注吞吐量(throughput)

適合中大規模數據集的應用,應用服務器,電信領域,關注response time,而不是throughput

使用參數

Client模式下默認:

可以使用

 

強制使用參數:

-XX:+UseSerialGC

 

優勢:對server應用沒什麼優勢
缺點:慢,不能充分發揮硬件資源

Server模式下默認

--YGC:PS FGC:Parallel MSC

 

強制使用參數:

-XX:+UseParallelGC-XX:+UseParallelOldGC

--ParallelGC表明FGCParallel MSC

--ParallelOldGC表明FGCParallel Compacting

 

優勢:高效

缺點heap變大後形成暫停時間會變長

可用-XX:+UseConcMarkSweepGC強制指定

優勢:
對old進行回收時,對應用形成的暫停時間很是短,適合對latency要求比較高的應用
缺點:
1.內存碎片和浮動垃圾
2.old去的內存分配效率低
3.回收的整個耗時比較長
4.和應用爭搶CPU

內存回收觸發條件

YGC
eden空間不足


FGC
old空間不足
perm空間不足
顯示調用System.gc(),包括RMI等的定時觸發

YGC
eden空間不足


FGC
old空間不足
perm空間不足
顯示調用System.gc(),包括RMI等的定時觸發

YGC
eden空間不足


CMS GC
1.old Gen使用率大的比率,默認爲92%
2.配置了CMSClassUnloadingEnabled,Perm Gen的使用達到必定的比率默認爲92%
3.Hotspot本身根據估計決定是否要觸法
4.在配置了ExplictGCInvokesConcurrent的狀況下顯示調用了System.gc()

內存回收觸發工做

YGC
清空eden+from中全部no ref的對象佔用的內存
eden+from中的全部存活的對象copyto
在這個過程當中一些對象將晉升到old:
--to放不下的
--存活次數超過tenuring threshold
從新計算Tenuring Threshold;
單線程作以上動做,GC全程暫停應用
FGC
若是配置了CollectGen0First,則先觸發YGC
清空heapno ref的對象,permgen中已經被卸載的classloader中加載的class的信息
單線程作以上動做
全程暫停應用

YGC
serial動做基本相同,不一樣點:
1.多線程處理
2.YGC的最後不只從新計算Tenuring Threshold,還會從新調整EdenFrom的大小
FGC
1.如配置了ScavengeBeforeFullGC(默認),則先觸發YGC(??)
2.MSC:清空heap中的no ref對象,permgen中已經被卸載的classloader中加載的class信息,並進行壓縮
3.Compacting:清空heap中部分no ref的對象,permgen中已經被卸載的classloader中加載的class信息,並進行部分壓縮
多線程作以上動做.

YGC
serial動做基本相同,不一樣點:
1.多線程處理
CMSGC:
1.old gen到達比率時只清除old genno ref的對象所佔用的空間
2.perm gen到達比率時只清除已被清除的classloader加載的class信息
FGC
serial

細節參數

可用-XX:+UseSerialGC強制使用


-XX:SurvivorRatio=x,控制eden/s0/s1的大小
-XX:MaxTenuringThreshold,用於控制對象在新生代存活的最大次數


-XX:PretenureSizeThreshold=x,控制超過多大的字節的對象就在old分配.

-XX:SurvivorRatio=x,控制eden/s0/s1的大小
-XX:MaxTenuringThreshold,用於控制對象在新生代存活的最大次數

-XX:UseAdaptiveSizePolicy 去掉YGC後動態調整eden from已經tenuringthreshold的動做

-XX:ParallelGCThreads=4設置並行的線程數

-XX:CMSInitiatingOccupancyFraction 設置old gen使用到達多少比率時觸發
-XX:CMSInitiatingPermOccupancyFraction,設置Perm Gen使用到達多少比率時觸發
-XX:+UseCMSInitiatingOccupancyOnly禁止hostspot自行觸發CMS GC




3、典型配置舉例

如下配置主要針對分代垃圾回收算法而言。

堆大小設置

年輕代的設置很關鍵

JVM中最大堆大小有三方面限制:相關操做系統的數據模型(32-bt仍是64-bit)限制;系統的可用虛擬內存限制;系統的可用物理內存限制。32位 系統下,通常限制在1.5G~2G;64爲操做系統對內存無限制。在Windows Server 2003 系統,3.5G物理內存,JDK5.0下測試,最大可設置爲1478m。

典型設置:

java -Xmx3550m -Xms3550m -Xmn2g –Xss128k

-Xmx3550m:設置JVM最大可用內存爲3550M。

-Xms3550m:設置JVM促使內存爲3550m。此值能夠設置與-Xmx相同,以免每次垃圾回收完成後JVM從新分配內存。

-Xmn2g:設置年輕代大小爲2G。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代通常固定大小爲64m,因此增大年輕代後,將會減少年老代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。

-Xss128k:設置每一個線程的堆棧大小。JDK5.0之後每一個線程堆棧大小爲1M,之前每一個線程堆棧大小爲 256K。更具應用的線程所需內存大小進行調整。在相同物理內存下,減少這個值能生成更多的線程。可是操做系統對一個進程內的線程數仍是有限制的,不能無 限生成,經驗值在3000~5000左右。

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設置爲4,則年輕代與年老代所佔比值爲1:4,年輕代佔整個堆棧的1/5

-XX:SurvivorRatio=4:設置年輕代中Eden區與Survivor區的大小比值。設置爲4,則兩個Survivor區與一個Eden區的比值爲2:4,一個Survivor區佔整個年輕代的1/6

-XX:MaxPermSize=16m:設置持久代大小爲16m。

-XX:MaxTenuringThreshold=0:設置垃圾最大年齡。若是設置爲0的話,則年輕代對象不通過 Survivor區,直接進入年老代。對於年老代比較多的應用,能夠提升效率。若是將此值設置爲一個較大值,則年輕代對象會在Survivor區進行屢次 複製,這樣能夠增長對象再年輕代的存活時間,增長在年輕代即被回收的概論。


回收器選擇

JVM給了三種選擇:串行收集器、並行收集器、併發收集器

(1)串行收集器

適用於小數據量的狀況,因此這裏的選擇主要針對並行收集器和併發收集器。默認狀況下,JDK5.0之前都是使用串行收集器,若是想使用其餘收集器須要在啓動時加入相應參數。JDK5.0之後,JVM會根據當前系統配置進行判斷。


(2)並行收集器——吞吐量優先

如上文所述,並行收集器主要以到達必定的吞吐量爲目標,適用於科學技術和後臺處理等。

典型配置:

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

-XX:+UseParallelGC:選擇垃圾收集器爲並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用串行收集。

-XX:ParallelGCThreads=20:配置並行收集器的線程數,即:同時多少個線程一塊兒進行垃圾回收。此值最好配置與處理器數目相等。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式爲並行收集。JDK6.0支持對年老代並行收集。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=100:設置每次年輕代垃圾回收的最長時間,若是沒法知足此時間,JVM會自動調全年輕代大小,以知足此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy:設置此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直打開。


(3)併發收集器——響應時間優先

如上文所述,併發收集器主要是保證系統的響應時間,減小垃圾收集時的停頓時間。適用於應用服務器、電信領域等。

典型配置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:設置年老代爲併發收集。測試中配置這個之後,-XX:NewRatio=4的配置失效了,緣由不明。因此,此時年輕代大小最好用-Xmn設置。

-XX:+UseParNewGC: 設置年輕代爲並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設置,因此無需再設置此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction:因爲併發收集器不對內存空間進行壓縮、整理,因此運行一段時間之後會產生「碎片」,使得運行效率下降。此值設置運行多少次GC之後對內存空間進行壓縮、整理。

-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,可是能夠消除碎片


輔助信息

JVM提供了大量命令行參數,打印信息,供調試使用。主要有如下一些:

-XX:+PrintGC:輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails:輸出 形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可與上面兩個混合使用 
輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中斷的執行時間。可與上面混合使用。輸出形式:Application time: 0.5291524 seconds

-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期間程序暫停的時間。可與上面混合使用。輸出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC: 打印GC先後的詳細堆棧信息。輸出形式:

34.702: [GC {Heap before gc invocations=7:

def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)

from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)

to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)

tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)

the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)

compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:

def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)

  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)

  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)

tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)

the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)

compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

   ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

   rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

}

, 0.0757599 secs]

-Xloggc:filename:與上面幾個配合使用,把相關日誌信息記錄到文件以便分析。


4、常見配置彙總

堆設置

  -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後,對年老代進行壓縮


垃圾回收的瓶頸

傳統分代垃圾回收方式,已經在必定程度上把垃圾回收給應用帶來的負擔降到了最小,把應用的吞吐量推到了一個極限。可是他沒法解決的一個問題,就是Full GC所帶來的應用暫停。在一些對實時性要求很高的應用場景下,GC暫停所帶來的請求堆積和請求失敗是沒法接受的。這類應用可能要求請求的返回時間在幾百甚 至幾十毫秒之內,若是分代垃圾回收方式要達到這個指標,只能把最大堆的設置限制在一個相對較小範圍內,可是這樣有限制了應用自己的處理能力,一樣也是不可 接收的。

    分代垃圾回收方式確實也考慮了實時性要求而提供了併發回收器,支持最大暫停時間的設置,可是受限於分代垃圾回收的內存劃分模型,其效果也不是很理想。

    爲了達到實時性的要求(其實Java語言最初的設計也是在嵌入式系統上的),一種新垃圾回收方式呼之欲出,它既支持短的暫停時間,又支持大的內存空間分配。能夠很好的解決傳統分代方式帶來的問題。

 

增量收集的演進

    增量收集的方式在理論上能夠解決傳統分代方式帶來的問題。增量收集把對堆空間劃分紅一系列內存塊,使用時,先使用其中一部分(不會所有用完),垃圾收集時 把以前用掉的部分中的存活對象再放到後面沒有用的空間中,這樣能夠實現一直邊使用邊收集的效果,避免了傳統分代方式整個使用完了再暫停的回收的狀況。

    固然,傳統分代收集方式也提供了併發收集,可是他有一個很致命的地方,就是把整個堆作爲一個內存塊,這樣一方面會形成碎片(沒法壓縮),另外一方面他的每次 收集都是對整個堆的收集,沒法進行選擇,在暫停時間的控制上仍是很弱。而增量方式,經過內存空間的分塊,偏偏能夠解決上面問題。


Garbage Firest(G1)

這部分的內容主要參考這裏,這篇文章算是對G1算法論文的解讀。我也沒加什麼東西了。

目標:從設計目標看G1徹底是爲了大型應用而準備的。

支持很大的堆

高吞吐量

  --支持多CPU和垃圾回收線程

  --在主線程暫停的狀況下,使用並行收集

  --在主線程運行的狀況下,使用併發收集

實時目標:可配置在N毫秒內最多隻佔用M毫秒的時間進行垃圾回收

固然G1要達到實時性的要求,相對傳統的分代回收算法,在性能上會有一些損失。

 

 

G1 算法詳解

G1可謂博採衆家之長,力求到達一種完美。他吸收了增量收集優勢,把整個堆劃分爲一個一個等大小的區域(region)。內存的回收和劃分都以 region爲單位;同時,他也吸收了CMS的特色,把這個垃圾回收過程分爲幾個階段,分散一個垃圾回收過程;並且,G1也認同分代垃圾回收的思想,認爲 不一樣對象的生命週期不一樣,能夠採起不一樣收集方式,所以,它也支持分代的垃圾回收。爲了達到對回收時間的可預計性,G1在掃描了region之後,對其中的 活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間(要複製的活躍對象少了),由於活躍對象小,裏面能夠認爲多數都是垃 圾,因此這種方式被稱爲Garbage First(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)中,以後恢復全部應用線程。觸發這個步驟執行的條件爲:

    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暫停時間範圍內儘快的執行此步驟;

    在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,另外還有幾種狀況也是會觸發這個步驟的執行的:

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

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

之後JVM的調優或許跟多須要針對G1算法進行調優了。



參考資料

能整理出上面一些東西,也是由於站在巨人的肩上。下面是一些參考資料,供你們學習,你們有更好的,能夠繼續完善:)

· Java 理論與實踐: 垃圾收集簡史

· Java HotSpot VM Options

· 《深刻Java虛擬機》。雖然過去了不少年,但這本書依舊是經典。

這裏是本系列的最後一篇了,很高興你們可以喜歡這系列的文章。期間也提了不少問題,其中有些是我以前沒有想到的或者考慮欠妥的,感謝提出這些問題的朋友,我也學到的很多東西。




參考推薦:

Java 類的生命週期詳解

Java 內存模型及GC原理

eclipse.ini 內存設置

JVM調優總結(推薦)


JVM調優總結

java Object類佔用內存大小計算
相關文章
相關標籤/搜索