java堆內存詳解


http://www.importnew.com/14630.html

java堆的特點
《深入理解java虛擬機》是什么描述java堆的

  • Java堆(Java Heap)是java虛擬機所管理的內存中最大的一塊
  • java堆被所有線程共享的一塊內存區域
  • 虛擬機啟動時創建java堆
  • java堆的唯一目的就是存放對象實例。
  • java堆是垃圾收集器管理的主要區域。
  • 從內存回收的角度來看, 由於現在收集器基本都采用分代收集算法, 所以Java堆可以細分為:新生代(Young)和老年代(Old)。 新生代又被划分為三個區域Eden、From Survivor, To Survivor等。無論怎么划分,最終存儲的都是實例對象, 進一步划分的目的是為了更好的回收內存, 或者更快的分配內存。
  • java堆的大小是可擴展的, 通過-Xmx和-Xms控制。
  • 如果堆內存不夠分配實例對象, 並且對也無法在擴展時, 將會拋出outOfMemoryError異常。


----------------------------------------------------------------

參考文獻:
http://www.importnew.com/14630.html Java 堆內存
http://blog.csdn.net/ylyg050518/article/details/52244994 Java虛擬機(二)——Java堆內存划分

堆內存划分:

 

  • 堆大小 = 新生代 + 老年代。堆的大小可通過參數–Xms(堆的初始容量)、-Xmx(堆的最大容量) 來指定。
  • 其中,新生代 ( Young ) 被細分為 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名為 from 和 to,以示區分。默認的,Edem : from : to = 8 : 1 : 1 。(可以通過參數 –XX:SurvivorRatio 來設定 。
  • 即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。
  • JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為對象服務,所以無論什么時候,總是有一塊 Survivor 區域是空閑着的。
  • 新生代實際可用的內存空間為 9/10 ( 即90% )的新生代空間。

 

堆的垃圾回收方式
java堆是GC垃圾回收的主要區域。 GC分為兩種: Minor GC、Full GC(也叫做Major GC).

Minor GC(簡稱GC)
Minor GC是發生在新生代中的垃圾收集動作, 所采用的是復制算法
GC一般為堆空間某個區發生了垃圾回收,
新生代(Young)幾乎是所有java對象出生的地方。即java對象申請的內存以及存放都是在這個地方。java中的大部分對象通常不會長久的存活, 具有朝生夕死的特點。
當一個對象被判定為“死亡”的時候, GC就有責任來回收掉這部分對象的內存空間。
新生代是收集垃圾的頻繁區域。

  回收過程如下:

當對象在 Eden ( 包括一個 Survivor 區域,這里假設是 from 區域 ) 出生后,在經過一次 Minor GC 后,如果對象還存活,並且能夠被另外一塊 Survivor 區域所容納(上面已經假設為 from 區域,這里應為 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用復制算法將這些仍然還存活的對象復制到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然后清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些對象的年齡設置為1,以后對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成為老年代。
但這也不是一定的,對於一些較大的對象 ( 即需要分配一塊較大的連續內存空間 ) 則是直接進入到老年代。

Full GC
Full GC 基本都是整個堆空間及持久代發生了垃圾回收,所采用的是標記-清除算法
現實的生活中,老年代的人通常會比新生代的人 “早死”。堆內存中的老年代(Old)不同於這個,老年代里面的對象幾乎個個都是在 Survivor 區域中熬過來的,它們是不會那么容易就 “死掉” 了的。因此,Full GC 發生的次數不會有 Minor GC 那么頻繁,並且做一次 Full GC 要比進行一次 Minor GC 的時間更長,一般是Minor GC的 10倍以上。
另外,標記-清除算法收集垃圾的時候會產生許多的內存碎片 ( 即不連續的內存空間 ),此后需要為較大的對象分配內存空間時,若無法找到足夠的連續的內存空間,就會提前觸發一次 GC 的收集動作
---------------------------------------------------------------------
下面我們來分析一下GC日志:
如何在eclipse中打印GC日志? 參考這篇文章: 如何在eclipse中打印gc日志.
我們首先寫一段代碼:
public static void main(String[] args) {
Object obj = new Object();
System.gc();
System.out.println();
obj = new Object();
obj = new Object();
System.gc();
System.out.println();
}
在Run as - Run Configuration中配置參數,使得控制台能夠顯示 GC 相關的日志信息,執行上面代碼,下面是其中一次執行的結果。

 

-verbose:gc
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails

 

--------------------------------------------------------------
再看下面這個圖:
第一次System.gc()

 第二次執行System.gc()

 

對比這兩次gc的結果:
從Full GC中可以看出, 新生代的可用內存約為38M, 老年代可用內存約為86M, 堆的可用總內存約為124M.
可以看出>新生代內存占用jvm堆內存的1/3, 老年代內存占用jvm堆總內存的2/3. GC堆新生代內存回收比較樂觀. 對老年代,以及方法區的回收並不明顯, 或者說不如新生代.

除此之外:
再來觀察: 第一次gc, 新生代回收情況是665K->648K, 而老年代將新生代的648K回收至0K. 放入了老年代, 所以老年代是從0K增加到470K. 所以,新生代回收內存情況是665K->648K, 老年代回收情況是648K->470K.

第二次gc看出, Full GC處理時間是GC的17倍.

--------------------------------------------------------------

JVM參數選項
下面只列舉其中的幾個常用和容易掌握的配置選項:

 

 


--------------------------------------------------------------

 

我們再來分析一個案例:
------------源代碼 開始------------
public static void main(String[] args) {
new Test2().doTest();
}

public void doTest() {
Integer M = new Integer(1024 * 1024 * 1); // 單位, 兆(M)
byte[] bytes = new byte[1 * M]; // 申請 1M 大小的內存空間
bytes = null; // 斷開引用鏈
System.gc(); // 通知 GC 收集垃圾
System.out.println();
bytes = new byte[1 * M]; // 重新申請 1M 大小的內存空間
bytes = new byte[1 * M]; // 再次申請 1M 大小的內存空間
System.gc();
System.out.println();
}
------------源代碼 結束------------

 

執行結果:

 

從打印結果可以看出
1. 新生代內存空間約為38M, 其中eden空間約為33M, from Survivor 空間約為5M, to Survivor空間約為5M.
2. 這里我們設置的-Xmn為43M, 也就是說指定的新生代的空間是43M, 那為什么打印結果顯示的時38M呢?另外的5M哪里去了?
其實是這樣的: 新生代 = Eden + From Survivor + To Survivor = 33 + 5+ 5 = 43M. 可見新生代的內存空間確實是43M, 按照Xmn分配得來的.
3. 而且這里指定了SurvivorRatio = 8. 因此, eden = 8/10 的新生代空間 = 8/10 * 43 = 38M。
from = to = 1/10 的新生代空間 = 1/10 * 43 = 5M。
4. 堆信息中新生代的 total 18432K 是這樣來的: eden + 1 個 survivor = 33K + 5K = 18432K,即約為 38M。
5. 因為 jvm 每次只是用新生代中的 eden 和 一個 survivor,因此新生代實際的可用內存空間大小為所指定的 90%。
  因此可以知道,這里新生代的內存空間指的是新生代可用的總的內存空間,而不是指整個新生代的空間大小。
6. 另外,可以看出老年代的內存空間為 86016K ( 約 86M ),堆大小 = 新生代 + 老年代。因此在這里,老年代 = 堆大小 – 新生代 = 124 – 38 = 86M。(注: 這里應該是減去43的, 可我這里減去43以后,就不是86了, 可能是不能版本的虛擬機, 細節不一樣)
7. 最后,這里還指定了 PermSize = 21m,PermGen 即永久代 ( 方法區 ),它還有一個名字,叫非堆,主要用來存儲由 jvm 加載的類文件信息、常量、靜態變量等。
-----------------------------------------------------------------

 

打個盹,回到 doTest() 方法中,可以看到代碼在第 17、21、22 這三行中分別申請了一塊 1M 大小的內存空間,並在 19 和 23 這兩行中分別顯式的調用了 System.gc()。從控制台打印的信息來看,每次調 System.gc(),是先進行 Minor GC,然后再進行 Full GC。

第 19 行觸發的 Minor GC 收集分析:
從信息 PSYoungGen : 1689K -> 632K,可以知道,在第 17 行為 bytes 分配的內存空間已經被回收完成。
引起 GC 回收這 1M 內存空間的因素是第 18 行的 bytes = null; bytes 為 null 表明之前申請的那 1M 大小的內存空間現在已經沒有任何引用變量在使用它了,並且在內存中它處於一種不可到達狀態 ( 即沒有任何引用鏈與 GC Roots 相連 )。那么,當 Minor GC 發生的時候,GC 就會來回收掉這部分的內存空間。

第 19 行觸發的 Full GC 收集分析:
在 Minor GC 的時候,信息顯示 PSYoungGen : 1689K -> 632K,再看看 Full GC 中顯示的 PSYoungGen : 632K -> 0K,可以看出,Full GC 后,新生代的內存使用變成0K 了

剛剛說到 Full GC 后,新生代的內存使用從 632K 變成 0K 了,那么這 632K 到底哪去了 ? 難道都被 GC 當成垃圾回收掉了 ? 當然不是了。我們在 main 方法中 new 了一個 Test 類的實例,這里的 Test 類的實例屬於對象,它應該被分配到新生代內存當中,現在還在調用這個實例的 doTest 方法呢,GC 不可能在這個時候來回收它的。

接着往下看 Full GC 的信息,會發現一個很有趣的現象,PSOldGen: 0K -> 470K,可以看到,Full GC 后,老年代的內存使用從 0K 變成了 160K,想必你已經猜到大概是怎么回事了。當 Full GC 進行的時候,默認的方式是盡量清空新生代 ( YoungGen ),因此在調 System.gc() 時,新生代 ( YoungGen ) 中存活的對象會提前進入老年代。

第 23 行觸發的 Minor GC 收集分析:
從信息 PSYoungGen : 4045K -> 1088K,可以知道,在第 21 行創建的,大小為 1M 的數組被 GC 回收了。在第 22 行創建的,大小也為 1M 的數組由於 bytes 引用變量還在引用它,因此,它暫時未被 GC 回收。

第 23 行觸發的 Full GC 收集分析:
在 Minor GC 的時候,信息顯示 PSYoungGen : 4045K -> 1088K,Full GC 中顯示的 PSYoungGen : 1088K -> 0K,以及 PSOldGen: 470K -> 1494K,可以知道,新生代 ( YoungGen ) 中存活的對象又提前進入老年代了。

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM