Java內存分配機制詳解

文章轉載自:http://www.cnblogs.com/zhguang/p/3257367.html


本文僅載抄了部分內容,若想知道JVM內存全量信息,請查看原文

Java內存分配機制

這裏所說的內存分配,主要指的是在堆上的分配,一般的,對象的內存分配都是在堆上進行,但現代技術也支持將對象拆成標量類型(標量類型即原子類型,表示單個值,可以是基本類型或String等),然後在棧上分配,在棧上分配的很少見,我們這裏不考慮。

  Java內存分配和回收的機制概括的說,就是:分代分配,分代回收。對象將根據存活的時間被分爲:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區)。如下圖(來源於《成爲JavaGC專家part I》,http://www.importnew.com/1993.html):

    

  年輕代(Young Generation):對象被創建時,內存的分配首先發生在年輕代(大對象可以直接被創建在年老代),大部分的對象在創建後很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制清理掉(IBM的研究表明,98%的對象都是很快消亡的),這個GC機制被稱爲Minor GC或叫Young GC。注意,Minor GC並不代表年輕代內存不足,它事實上只表示在Eden區上的GC。

  年輕代上的內存分配是這樣的,年輕代可以分爲3個區域:Eden區(伊甸園,亞當和夏娃偷喫禁果生娃娃的地方,用來表示內存首次分配的區域,再貼切不過)和兩個存活區(Survivor 0 、Survivor 1)。內存分配過程爲(來源於《成爲JavaGC專家part I》,http://www.importnew.com/1993.html):

    

  1. 絕大多數剛創建的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,因此在其上分配內存極快;
  2. 最初一次,當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,並將剩餘的對象複製到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);
  3.  下次Eden區滿了,再執行一次Minor GC,將消亡的對象清理掉,將存活的對象複製到Survivor1中,然後清空Eden區;
  4.  將Survivor0中消亡的對象清理掉,將其中可以晉級的對象晉級到Old區,將存活的對象也複製到Survivor1區,然後清空Survivor0區;
  5. 當兩個存活區切換了幾次(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代,但這只是個最大值,並不代表一定是這個值)之後,仍然存活的對象(其實只有一小部分,比如,我們自己定義的對象),將被複制到老年代。

  從上面的過程可以看出,Eden區是連續的空間,且Survivor總有一個爲空。經過一次GC和複製,一個Survivor中保存着當前還活着的對象,而Eden區和另一個Survivor區的內容都不再需要了,可以直接清空,到下一次GC時,兩個Survivor的角色再互換。因此,這種方式分配內存和清理內存的效率都極高,這種垃圾回收的方式就是著名的“停止-複製(Stop-and-copy)”清理法(將Eden區和一個Survivor中仍然存活的對象拷貝到另一個Survivor中),這不代表着停止複製清理法很高效,其實,它也只在這種情況下高效,如果在老年代採用停止複製,則挺悲劇的

  在Eden區,HotSpot虛擬機使用了兩種技術來加快內存分配。分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),這兩種技術的做法分別是:由於Eden區是連續的,因此bump-the-pointer技術的核心就是跟蹤最後創建的一個對象,在對象創建時,只需要檢查最後一個對象後面是否有足夠的內存即可,從而大大加快內存分配速度;而對於TLAB技術是對於多線程而言的,將Eden區分爲若干段,每個線程使用獨立的一段,避免相互影響。TLAB結合bump-the-pointer技術,將保證每個線程都使用Eden區的一段,並快速的分配內存。

  年老代(Old Generation):對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC後存活了下來),則會被複制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時,將執行Major GC,也叫 Full GC。  

   可以使用-XX:+UseAdaptiveSizePolicy開關來控制是否採用動態控制策略,如果動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。

  如果對象比較大(比如長字符串或大數組),Young空間不足,則大對象會直接分配到老年代上(大對象可能觸發提前GC,應少用,更應避免使用短命的大對象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。

  可能存在年老代對象引用新生代對象的情況,如果需要執行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——”card table“,所有老年代對象引用新生代對象的記錄都記錄在這裏。Young GC時,只要查這裏即可,不用再去查全部老年代,因此性能大大提高。

Java GC機制

GC機制的基本算法是:分代收集,這個不用贅述。下面闡述每個分代的收集方法。

  

  年輕代:

  事實上,在上一節,已經介紹了新生代的主要垃圾回收方法,在新生代中,使用“停止-複製”算法進行清理,將新生代內存分爲2部分,1部分 Eden區較大,1部分Survivor比較小,並被劃分爲兩個等量的部分。每次進行清理時,將Eden區和一個Survivor中仍然存活的對象拷貝到 另一個Survivor中,然後清理掉Eden和剛纔的Survivor。

  這裏也可以發現,停止複製算法中,用來複制的兩部分並不總是相等的(傳統的停止複製算法兩部分內存相等,但新生代中使用1個大的Eden區和2個小的Survivor區來避免這個問題)

  由於絕大部分的對象都是短命的,甚至存活不到Survivor中,所以,Eden區與Survivor的比例較大,HotSpot默認是 8:1,即分別佔新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下來的內存超過了10%,則需要將一部分對象分配到 老年代。用-XX:SurvivorRatio參數來配置Eden區域Survivor區的容量比值,默認是8,代表Eden:Survivor1:Survivor2=8:1:1.

  老年代:

  老年代存儲的對象比年輕代多得多,而且不乏大對象,對老年代進行內存清理時,如果使用停止-複製算法,則相當低效。一般,老年代用的算法是標記-整理算法,即:標記出仍然存活的對象(存在引用的),將所有存活的對象向一端移動,以保證內存的連續。
     在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,如果大於,則直接觸發一次Full GC,否則,就查看是否設置了-XX:+HandlePromotionFailure(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍內存分配失敗;如果不允許,則仍然進行Full GC(這代表着如果設置-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多內存,所以,最好不要這樣做)。

  方法區(永久代):

  永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收。對於無用的類進行回收,必須保證3點:

  1. 類的所有實例都已經被回收
  2. 加載類的ClassLoader已經被回收
  3. 類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)
     永久代的回收並不是必須的,可以通過參數來設置是否對類進行回收。HotSpot提供-Xnoclassgc進行控制
     使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看類加載和卸載信息
     -verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;
     -XX:+TraceClassUnLoading需要fastdebug版HotSpot支持

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章