jvm的stack和heap,JVM內存模型,垃圾回收策略,分代收集,增量收集(轉)

 深刻Java虛擬機:JVM中的StackHeap(轉自:http://www.cnblogs.com/laoyangHJ/archive/2011/08/17/gc-Stack.htmlhtml

在JVM中,內存分爲兩個部分,Stack(棧)和Heap(堆),這裏,咱們從JVM的內存管理原理的角度來認識Stack和Heap,並經過這些原理認清Java中靜態方法和靜態屬性的問題。java

  通常,JVM的內存分爲兩部分:Stack和Heap。mysql

  Stack(棧)是JVM的內存指令區。Stack管理很簡單,push必定長度字節的數據或者指令,Stack指針壓棧相應的字節位移;pop必定字節長度數據或者指令,Stack指針彈棧。Stack的速度很快,管理很簡單,而且每次操做的數據或者指令字節長度是已知的。因此Java 基本數據類型,Java 指令代碼,常量都保存在Stack中。web

  Heap(堆)是JVM的內存數據區。Heap 的管理很複雜,每次分配不定長的內存空間,專門用來保存對象的實例。在Heap 中分配必定的內存來保存對象實例,實際上也只是保存對象實例的屬性值,屬性的類型和對象自己的類型標記等,並不保存對象的方法(方法是指令,保存在Stack中),在Heap 中分配必定的內存保存對象實例和對象的序列化比較相似。而對象實例在Heap 中分配好之後,須要在Stack中保存一個4字節的Heap 內存地址,用來定位該對象實例在Heap 中的位置,便於找到該對象實例。算法

  因爲Stack的內存管理是順序分配的,並且定長,不存在內存回收問題;而Heap 則是隨機分配內存,不定長度,存在內存分配和回收的問題;所以在JVM中另有一個GC進程,按期掃描Heap ,它根據Stack中保存的4字節對象地址掃描Heap ,定位Heap 中這些對象,進行一些優化(例如合併空閒內存塊什麼的),而且假設Heap 中沒有掃描到的區域都是空閒的,通通refresh(其實是把Stack中丟失了對象地址的無用對象清除了),這就是垃圾收集的過程;關於垃圾收集的更深刻講解請參考51CTO以前的文章《JVM內存模型及垃圾收集策略解析》。sql

JVM的體系結構數據庫

  咱們首先要搞清楚的是什麼是數據以及什麼是指令。而後要搞清楚對象的方法和對象的屬性分別保存在哪裏。編程

  1)方法自己是指令的操做碼部分,保存在Stack中;數組

  2)方法內部變量做爲指令的操做數部分,跟在指令的操做碼以後,保存在Stack中(其實是簡單類型保存在Stack中,對象類型在Stack中保存地址,在Heap 中保存值);上述的指令操做碼和指令操做數構成了完整的Java 指令。緩存

  3)對象實例包括其屬性值做爲數據,保存在數據區Heap 中。

  非靜態的對象屬性做爲對象實例的一部分保存在Heap 中,而對象實例必須經過Stack中保存的地址指針才能訪問到。所以可否訪問到對象實例以及它的非靜態屬性值徹底取決於可否得到對象實例在Stack中的地址指針。

  非靜態方法和靜態方法的區別:

  非靜態方法有一個和靜態方法很重大的不一樣:非靜態方法有一個隱含的傳入參數,該參數是JVM給它的,和咱們怎麼寫代碼無關,這個隱含的參數就是對象實例在Stack中的地址指針。所以非靜態方法(在Stack中的指令代碼)老是能夠找到本身的專用數據(在Heap 中的對象屬性值)。固然非靜態方法也必須得到該隱含參數,所以非靜態方法在調用前,必須先new一個對象實例,得到Stack中的地址指針,不然JVM將沒法將隱含參數傳給非靜態方法。

  靜態方法無此隱含參數,所以也不須要new對象,只要class文件被ClassLoader load進入JVM的Stack,該靜態方法便可被調用。固然此時靜態方法是存取不到Heap 中的對象屬性的。

  總結一下該過程:當一個class文件被ClassLoader load進入JVM後,方法指令保存在Stack中,此時Heap 區沒有數據。而後程序技術器開始執行指令,若是是靜態方法,直接依次執行指令代碼,固然此時指令代碼是不能訪問Heap 數據區的;若是是非靜態方法,因爲隱含參數沒有值,會報錯。所以在非靜態方法執行前,要先new對象,在Heap 中分配數據,並把Stack中的地址指針交給非靜態方法,這樣程序技術器依次執行指令,而指令代碼此時可以訪問到Heap 數據區了。

  靜態屬性和動態屬性:

  前面提到對象實例以及動態屬性都是保存在Heap 中的,而Heap 必須經過Stack中的地址指針纔可以被指令(類的方法)訪問到。所以能夠推斷出:靜態屬性是保存在Stack中的,而不一樣於動態屬性保存在Heap 中。正由於都是在Stack中,而Stack中指令和數據都是定長的,所以很容易算出偏移量,也所以無論什麼指令(類的方法),均可以訪問到類的靜態屬性。也正由於靜態屬性被保存在Stack中,因此具備了全局屬性。

  在JVM中,靜態屬性保存在Stack指令內存區,動態屬性保存在Heap數據內存區。

JVM內存模型及垃圾收集策略解析()

轉自http://www.cnblogs.com/laoyangHJ/archive/2011/08/17/jvm_model.html

JVM內存模型是Java的核心技術之一,以前51CTO曾爲你們介紹過JVM分代垃圾回收策略的基礎概念,如今不少編程語言都引入了相似Java JVM的內存模型和垃圾收集器的機制,下面咱們將主要針對Java中的JVM內存模型及垃圾收集的具體策略進行綜合的分析。

JVM內存模型

1.1 Java棧

Java棧是與每個線程關聯的,JVM在建立每個線程的時候,會分配必定的棧空間給線程。它主要用來存儲線程執行過程當中的局部變量,方法的返回值,以及方法調用上下文。棧空間隨着線程的終止而釋放。StackOverflowError:若是在線程執行的過程當中,棧空間不夠用,那麼JVM就會拋出此異常,這種狀況通常是死遞歸形成的。

1.2 堆

Java中堆是由全部的線程共享的一塊內存區域,堆用來保存各類JAVA對象,好比數組,線程對象等。

1.2.1 Generation

JVM堆通常又能夠分爲如下三部分:

 

◆ Perm

Perm代主要保存class,method,filed對象,這部門的空間通常不會溢出,除非一次性加載了不少的類,不過在涉及到熱部署的應用服務器的時候,有時候會遇到java.lang.OutOfMemoryError : PermGen space 的錯誤,形成這個錯誤的很大緣由就有多是每次都從新部署,可是從新部署後,類的class沒有被卸載掉,這樣就形成了大量的class對象保存在了perm中,這種狀況下,通常從新啓動應用服務器能夠解決問題。

◆ Tenured

Tenured區主要保存生命週期長的對象,通常是一些老的對象,當一些對象在Young複製轉移必定的次數之後,對象就會被轉移到Tenured區,通常若是系統中用了application級別的緩存,緩存中的對象每每會被轉移到這一區間。

◆ Young

Young區被劃分爲三部分,Eden區和兩個大小嚴格相同的Survivor區,其中Survivor區間中,某一時刻只有其中一個是被使用的,另一個留作垃圾收集時複製對象用,在Young區間變滿的時候,minor GC就會將存活的對象移到空閒的Survivor區間中,根據JVM的策略,在通過幾回垃圾收集後,任然存活於Survivor的對象將被移動到Tenured區間。

1.2.2 Sizing the Generations

JVM提供了相應的參數來對內存大小進行配置。正如上面描述,JVM中堆被分爲了3個大的區間,同時JVM也提供了一些選項對Young,Tenured的大小進行控制。

◆ Total Heap

-Xms :指定了JVM初始啓動之後初始化內存

-Xmx:指定JVM堆得最大內存,在JVM啓動之後,會分配-Xmx參數指定大小的內存給JVM,可是不必定所有使用,JVM會根據-Xms參數來調節真正用於JVM的內存

-Xmx -Xms之差就是三個Virtual空間的大小

◆ Young Generation

-XX:NewRatio=8意味着tenured 和 young的比值8:1,這樣eden+2*survivor=1/9

堆內存

-XX:SurvivorRatio=32意味着eden和一個survivor的比值是32:1,這樣一個Survivor就佔Young區的1/34.

-Xmn 參數設置了年輕代的大小

◆ Perm Generation

-XX:PermSize=16M -XX:MaxPermSize=64M

Thread Stack

-XX:Xss=128K

1.3 堆棧分離的好處

呵呵,其它的先不說了,就來講說面向對象的設計吧,固然除了面向對象的設計帶來的維護性,複用性和擴展性方面的好處外,咱們看看面向對象如何巧妙的利用了堆棧分離。若是從JAVA內存模型的角度去理解面向對象的設計,咱們就會發現對象它完美的表示了堆和棧,對象的數據放在堆中,而咱們編寫的那些方法通常都是運行在棧中,所以面向對象的設計是一種很是完美的設計方式,它完美的統一了數據存儲和運行。

JAVA垃圾收集器

2.1 垃圾收集簡史

垃圾收集提供了內存管理的機制,使得應用程序不須要在關注內存如何釋放,內存用完後,垃圾收集會進行收集,這樣就減輕了由於人爲的管理內存而形成的錯誤,好比在C++語言裏,出現內存泄露時很常見的。Java語言是目前使用最多的依賴於垃圾收集器的語言,可是垃圾收集器策略從20世紀60年代就已經流行起來了,好比Smalltalk,Eiffel等編程語言也集成了垃圾收集器的機制。

2.2 常見的垃圾收集策略

全部的垃圾收集算法都面臨同一個問題,那就是找出應用程序不可到達的內存塊,將其釋放,這裏面得不可到達主要是指應用程序已經沒有內存塊的引用了,而在JAVA中,某個對象對應用程序是可到達的是指:這個對象被根(根主要是指類的靜態變量,或者活躍在全部線程棧的對象的引用)引用或者對象被另外一個可到達的對象引用。

2.2.1 Reference Counting(引用計數)
 
引用計數是最簡單直接的一種方式,這種方式在每個對象中增長一個引用的計數,這個計數表明當前程序有多少個引用引用了此對象,若是此對象的引用計數變爲0,那麼此對象就能夠做爲垃圾收集器的目標對象來收集。

優勢:

簡單,直接,不須要暫停整個應用

缺點:

1.須要編譯器的配合,編譯器要生成特殊的指令來進行引用計數的操做,好比每次將對象賦值給新的引用,或者者對象的引用超出了做用域等。

2.不能處理循環引用的問題

2.2.2 跟蹤收集器

跟蹤收集器首先要暫停整個應用程序,而後開始從根對象掃描整個堆,判斷掃描的對象是否有對象引用,這裏面有三個問題須要搞清楚:

1.若是每次掃描整個堆,那麼勢必讓GC的時間變長,從而影響了應用自己的執行。所以在JVM裏面採用了分代收集,在新生代收集的時候minor gc只須要掃描新生代,而不須要掃描老生代。

2.JVM採用了分代收集之後,minor gc只掃描新生代,可是minor gc怎麼判斷是否有老生代的對象引用了新生代的對象,JVM採用了卡片標記的策略,卡片標記將老生代分紅了一塊一塊的,劃分之後的每個塊就叫作一個卡片,JVM採用卡表維護了每個塊的狀態,當JAVA程序運行的時候,若是發現老生代對象引用或者釋放了新生代對象的引用,那麼就JVM就將卡表的狀態設置爲髒狀態,這樣每次minor gc的時候就會只掃描被標記爲髒狀態的卡片,而不須要掃描整個堆。具體以下圖:
3.GC在收集一個對象的時候會判斷是否有引用指向對象,在JAVA中的引用主要有四種:Strong reference,Soft reference,Weak reference,Phantom reference.

◆ Strong Reference

強引用是JAVA中默認採用的一種方式,咱們平時建立的引用都屬於強引用。若是一個對象沒有強引用,那麼對象就會被回收。

public void testStrongReference(){  
Object referent = new Object();  
Object strongReference = referent;  
referent = null;  
System.gc();  
assertNotNull(strongReference);  
} 

  

◆ Soft Reference

軟引用的對象在GC的時候不會被回收,只有當內存不夠用的時候纔會真正的回收,所以軟引用適合緩存的場合,這樣使得緩存中的對象能夠儘可能的再內存中待長久一點。

Public void testSoftReference(){  
String  str =  "test";  
SoftReference<String> softreference = new SoftReference<String>(str);  
str=null;  
System.gc();  
assertNotNull(softreference.get());  
} 

Weak reference

弱引用有利於對象更快的被回收,假如一個對象沒有強引用只有弱引用,那麼在GC後,這個對象確定會被回收。

Public void testWeakReference(){  
String  str =  "test";  
WeakReference<String> weakReference = new WeakReference<String>(str);  
str=null;  
System.gc();  
assertNull(weakReference.get());  
}

Phantom reference

 

回收算法轉自http://pengjiaheng.iteye.com/blog/520228

按照基本回收策略分

引用計數(Reference Counting):

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

標記-清除(Mark-Sweep:

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

複製(Copying:

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

標記-整理(Mark-Compact:

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

按分區對待的方式分

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

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

按系統線程分

串行收集:串行收集使用單線程處理全部垃圾回收工做,由於無需多線程交互,實現容易,並且效率比較高。可是,其侷限性也比較明顯,即沒法使用多處理器的優點,因此此收集適合單處理器機器。固然,此收集器也能夠用在小數據量(100M左右)狀況下的多處理器機器上。

並行收集:並行收集使用多線程處理垃圾回收工做,於是速度快,效率高。並且理論上CPU數目越多,越能體現出並行收集器的優點。

 

併發收集:相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工做時,須要暫停整個運行環境,而只有垃圾回收程序在運行,所以,系統在垃圾回收時會有明顯的暫停,並且暫停時間會由於堆越大而越長。

如何區分垃圾

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

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

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

 

如何處理碎片

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

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

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

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

因爲不一樣對象的生命週期不同,所以在JVM的垃圾回收策略中有分代這一策略。本文介紹了分代策略的目標,如何分代,以及垃圾回收的觸發因素。

文章總結了JVM垃圾回收策略爲何要分代,如何分代,以及垃圾回收的觸發因素。

爲何要分代

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

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

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

如何分代

如圖所示:

        虛擬機中的共劃分爲三個代:年輕代(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 GC和Full GC。

Scavenge GC

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

        對整個堆進行整理,包括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後,對年老代進行壓縮

 垃圾回收的瓶頸

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

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

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

 增量收集的演進

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

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

 Garbage FirestG1

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

 目標

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

支持很大的堆

高吞吐量

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

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

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

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

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

算法詳解

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

 

回收步驟:

 初始標記(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構成的環。

 併發標記(Concurrent Marking

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

 最終標記暫停(Final Marking Pause

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

 存活對象計算及清除(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應用的瓶頸。目前JDK的垃圾回收算法,始終沒法解決垃圾回收時的暫停問題,由於這個暫停嚴重影響了程序的相應時間,形成擁塞或堆積。這也是後續JDK增長G1算法的一個重要緣由。

    固然,上面是從技術角度出發解決垃圾回收帶來的問題,可是從系統設計方面咱們就須要問一下了:

    咱們須要分配如此大的內存空間給應用嗎?

    咱們是否可以經過有效使用內存而不是經過擴大內存的方式來設計咱們的系統呢?    

咱們的內存中都放了什麼

    內存中須要放什麼呢?我的認爲,內存中須要放的是你的應用須要在不久的未來再次用到到的東西。想一想看,若是你在未來不用這些東西,何須放內存呢?放文件、數據庫不是更好?這些東西通常包括:

1. 系統運行時業務相關的數據。好比web應用中的session、即時消息的session等。這些數據通常在一個用戶訪問週期或者一個使用過程當中都須要存在。

2. 緩存。緩存就比較多了,你所要快速訪問的均可以放這裏面。其實上面的業務數據也能夠理解爲一種緩存。

3.  線程。

    所以,咱們是否是能夠這麼認爲,若是咱們不把業務數據和緩存放在JVM中,或者把他們獨立出來,那麼Java應用使用時所需的內存將會大大減小,同時垃圾回收時間也會相應減小。

    我認爲這是可能的。

 解決之道

 數據庫、文件系統

    把全部數據都放入數據庫或者文件系統,這是一種最爲簡單的方式。在這種方式下,Java應用的內存基本上等於處理一次峯值併發請求所需的內存。數據的獲取都在每次請求時從數據庫和文件系統中獲取。也能夠理解爲,一次業務訪問之後,全部對象均可以進行回收了。

    這是一種內存使用最有效的方式,可是從應用角度來講,這種方式很低效。

 內存-硬盤映射

    上面的問題是由於咱們使用了文件系統帶來了低效。可是若是咱們不是讀寫硬盤,而是寫內存的話效率將會提升不少。

    數據庫和文件系統都是實實在在進行了持久化,可是當咱們並不須要這樣持久化的時候,咱們能夠作一些變通——把內存當硬盤使。

    內存-硬盤映射很好很強大,既用了緩存又對Java應用的內存使用又沒有影響。Java應用仍是Java應用,他只知道讀寫的仍是文件,可是其實是內存。

    這種方式兼得的Java應用與緩存兩方面的好處。memcached的普遍使用也正是這一類的表明。

 同一機器部署多個JVM

    這也是一種很好的方式,能夠分爲縱拆和橫拆。縱拆能夠理解爲把Java應用劃分爲不一樣模塊,各個模塊使用一個獨立的Java進程。而橫拆則是一樣功能的應用部署多個JVM。

    經過部署多個JVM,能夠把每一個JVM的內存控制一個垃圾回收能夠忍受的範圍內便可。可是這至關於進行了分佈式的處理,其額外帶來的複雜性也是須要評估的。另外,也有支持分佈式的這種JVM能夠考慮,不要要錢哦:)

 程序控制的對象生命週期

    這種方式是理想當中的方式,目前的虛擬機尚未,純屬假設。即:考慮由編程方式配置哪些對象在垃圾收集過程當中能夠直接跳過,減小垃圾回收線程遍歷標記的時間。

    這種方式至關於在編程的時候告訴虛擬機某些對象你能夠在*時間後在進行收集或者由代碼標識能夠收集了(相似C、C++),在這以前你即使去遍歷他也是沒有效果的,他確定是還在被引用的。

    這種方式若是JVM能夠實現,我的認爲將是一個飛躍,Java即有了垃圾回收的優點,又有了C、C++對內存的可控性。

 線程分配

    Java的阻塞式的線程模型基本上能夠拋棄了,目前成熟的NIO框架也比較多了。阻塞式IO帶來的問題是線程數量的線性增加,而NIO則能夠轉換成爲常數線程。所以,對於服務端的應用而言,NIO仍是惟一選擇。不過,JDK7中爲咱們帶來的AIO是否能讓人眼前一亮呢?咱們拭目以待。

 其餘的JDK

    本文說的都是Sun的JDK,目前常見的JDK還有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高不少,不過Sun JDK6.0之後提升也很大。並且JRocket在垃圾回收方面,也具備優點,其可設置垃圾回收的最大暫停時間也是很吸引人的。不過,系統Sun的G1實現之後,在這方面會有一個質的飛躍。

相關文章
相關標籤/搜索