深入JVM 原理(二)JVM 垃圾收集-堆內存

運行時數據區就是我們的java內存管理,我們java能管理的地方只在java運行時數據區,其他我們無法控制。

java運行時數據區的大小,我們可根據自己的需求自行更改控制,進行調優,但其中

棧內存是線程獨享。

堆內存是保存對象信息,是所有線程共享的。

所以,我們所說的java內存調優都是在運行時數據區進行的,即共享的數據區越大越好,所以,關鍵是在堆內存中,如果我們要真正做到對程序的理解,就需要對堆內存進行一定的控制。

 

下面我們來看堆內存的結構:

備註: 永久代就是方法區。  

有認爲方法區(永久代)不在堆內存。有的認爲方法區也是堆內存的一塊區域。

 

 

一定要記住在 JDK1.8 之後將最初的永久代內存空間取消了,使用元空間代替。

以下爲1.8之前的內存空間組成:(取消永久代的目的:是爲了將 HotSpot與JRockit 兩個虛擬機標準聯合成一個。)
在整個的JVM堆內存之中實際上將內存分爲三塊:
年輕代:新對象和沒達到一定年齡的對象都在年輕代;
老年代:被長時間使用的對象,老年代的內存空間比年輕代更大
元空間:像一些方法中的操作臨時對象等,直接使用物理內存;最初的永久代是需要在JVM堆內存裏面進行劃分;這樣可以解決內存溢出的問題。

 

JVM堆內存分爲2塊:Permanent Space(1.8後Meta space)Heap Space

  • Permanent 即永久代(Permanent Generation),主要存放的是Java類定義信息,與垃圾收集器要收集的Java對象關係不大。
  • Heap = { Old + NEW = {Eden, from, to} },Old 即 年老代(Old Generation),New 即 年輕代(Young Generation)。年老代和年輕代的劃分對垃圾收集影響比較大。

1、年輕代

所有新生成的對象首先都是放在年輕代。年輕代的目標就是儘可能快速的收集掉那些生命週期短的對象。年輕代一般分3個區,1個Eden區,2個Survivor區(from(s0) 和 to(s1))。 一般默認Eden:s0:s1  = 8:1:1 的比例分配。

大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當一個Survivor區滿時,此區的存活對象將被複制到另外一個Survivor區,當另一個Survivor區也滿了的時候,從前一個Survivor區複製過來的並且此時還存活的對象,將可能被複制到年老代。

2個Survivor區是對稱的,沒有先後關係,所以同一個Survivor區中可能同時存在從Eden區複製過來對象,和從另一個Survivor區複製過來的對象;而複製到年老區的只有從另一個Survivor區過來的對象。而且,因爲需要交換的原因,Survivor區至少有一個是空的。特殊的情況下,根據程序需要,Survivor區是可以配置爲多個的(多於2個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。

針對年輕代的垃圾回收即Young GC

2、年老代

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

針對年老代的垃圾回收即Full GC

3、永久代(1.8後 Meta space元空間)

用於存放靜態類型數據,如Java Class, Method 等。持久代對垃圾回收沒有顯著影響。但是有些應用可能動態生成或調用一些Class,例如Hibernate CGLib 等,在這種時候往往需要設置一個比較大的持久代空間來存放這些運行過程中動態增加的類型。

對於整個GC流程裏,最需要處理的就是年輕代和老年代的內存清理操作,而元空間(永久代)都不在GC範圍內;

 

所以,當一組對象生成時,內存申請過程如下

  1. 當現在有一個新的對象產生,那麼對象一定需要內存空間,於是現在就需要爲該對象進行內存空間的申請。
  2. 首先會判斷伊甸園區是否有內存空間,如果此時有內存空間,則將新對象保存在伊甸園區;
  3. 但如果伊甸園區的內存空間不足,那麼會自動執行一個 Minor GC 操作,將伊甸園區無用的內存空間進行清理,當清理之後會繼續判斷伊甸園區的內存空間是否充足?充足則將新的對象進行空間分配;
  4. 如果執行了 Minor GC 之後發現伊甸園區的內存依然不足,那麼這個時候會進行存貨區判斷,如果存活區有剩餘空間,則將伊甸園區的部分對象保存在存活區,那麼隨後繼續判斷伊甸園區的內存空間是否充足,如何內存充足,則在伊甸園區進行空間分配;
  5. 如果此時存活區也已經沒有內存空間了,則開始判斷老年區,如果此時老年區的空間充足,則將存活區中的活躍對象保存在老年代,而後存活區就會存現有空餘空間,隨後,伊甸園區將活躍對象保存在存活區之中,而後在伊甸園區裏爲新對象開闢內存空間;
  6. 如果這個時候老年代也滿了,那麼這個時候將產生 Major GC(Full GC),進行老年代的內存清理;
  7. 如果老年代執行了 Full GC 之後,依然無法進行對象的保存,就會產生 OOM()異常「OutOfMemoryError」。

 

 

OOM(「Outof Memory」)異常一般主要有如下2種原因

1. 年老代溢出,表現爲:java.lang.OutOfMemoryError:Javaheapspace

這是最常見的情況,產生的原因可能是:設置的內存參數Xmx過小或程序的內存泄露及使用不當問題。

例如循環上萬次的字符串處理、創建上千萬個對象、在一段代碼內申請上百M甚至上G的內存。還有的時候雖然不會報內存溢出,卻會使系統不間斷的垃圾回收,也無法處理其它請求。這種情況下除了檢查程序、打印堆內存等方法排查,還可以藉助一些內存分析工具,比如MAT就很不錯。


2. 持久代溢出,表現爲:java.lang.OutOfMemoryError:PermGenspace

通常由於持久代設置過小,動態加載了大量Java類而導致溢出,解決辦法唯有將參數 -XX:MaxPermSize 調大(一般256m能滿足絕大多數應用程序需求)。將部分Java類放到容器共享區(例如Tomcatshare lib)去加載的辦法也是一個思路,但前提是容器裏部署了多個應用,且這些應用有大量的共享類庫。

 

來個面試題,看看上面知識點掌握程度

請解釋「StackOverflowError」和「OutOfMemoryError」的區別:

1、stackoverflow: 每當java程序啓動一個新的線程時,java虛擬機會爲他分配一個棧,java棧以幀爲單位保持線程運行狀態;當線程調用一個方法是,jvm壓入一個新的棧幀到這個線程的棧中,只要這個方法還沒返回,這個棧幀就存在。
如果方法的嵌套調用層次太多(如遞歸調用),隨着java棧中的幀的增多,最終導致這個線程的棧中的所有棧幀的大小的總和大於-Xss設置的值,而產生生StackOverflowError溢出異常。
2、OutOfMemoryError: 如上。

 

 

4、 參數說明

  • -Xmx3550m:設置JVM最大堆內存爲3550M。
  • -Xms3550m:設置JVM初始堆內存爲3550M。此值可以設置與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配內存。
  • -Xss128k:設置每個線程的棧大小。JDK5.0以後每個線程棧大小爲1M,之前每個線程棧大小爲256K。應當根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。需要注意的是:當這個值被設置的較大(例如>2MB)時將會在很大程度上降低系統的性能。
  • -Xmn2g:設置年輕代大小爲2G。在整個堆內存大小確定的情況下,增大年輕代將會減小年老代,反之亦然。此值關係到JVM垃圾回收,對系統性能影響較大,官方推薦配置爲整個堆大小的3/8。
  • -XX:NewSize=1024m:設置年輕代初始值爲1024M。
  • -XX:MaxNewSize=1024m:設置年輕代最大值爲1024M。
  • -XX:PermSize=256m:設置持久代初始值爲256M。
  • -XX:MaxPermSize=256m:設置持久代最大值爲256M。
  • -XX:NewRatio=4:設置年輕代(包括1個Eden和2個Survivor區)與年老代的比值。表示年輕代比年老代爲1:4。
  • -XX:SurvivorRatio=4:設置年輕代中Eden區與Survivor區的比值。表示2個Survivor區(JVM堆內存年輕代中默認有2個大小相等的Survivor區)與1個Eden區的比值爲2:4,即1個Survivor區佔整個年輕代大小的1/6。
  • -XX:MaxTenuringThreshold=7:表示一個對象如果在Survivor區(救助空間)移動了7次還沒有被垃圾回收就進入年老代。如果設置爲0的話,則年輕代對象不經過Survivor區,直接進入年老代,對於需要大量常駐內存的應用,這樣做可以提高效率。如果將此值設置爲一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象在年輕代存活時間,增加對象在年輕代被垃圾回收的概率,減少Full GC的頻率,這樣做可以在某種程度上提高服務穩定性。

 

未完,待續......