【無爲原創】萬字圖文詳解java的堆內存及OOM的解決方案,看完還不懂,從此絕筆不寫了!

 

 

目錄如下:

什麼是JVM的堆
    是不是所有的Java對象都放在堆上?
    線程和堆的關係
堆的內部結構
    面試題
    新生代與老年代
如何設置堆的大小?
    新生代與老年代的比例
    設置Eden、倖存者的比例
    常用參數
對象分配
    金句:
    分配過程
        內存分配策略(或對象提升(promotion)規則):
    對象分配原則
Minor GC 、Major GC、Full GC
    Minor GC觸發機制
    老年代GC(Major GC/Full GC)觸發機制:
    Full GC觸發機制:
OOM如何解決
爲什麼需要把Java堆分代?不分代就不能正常工作了嗎?
    什麼是TLAB(快速分配策略)?
    爲什麼有TLAB(Thread Local Allocation Buffer)快速分配策略?
    什麼是TLAB?
    TLAB的說明:

什麼是JVM的堆

  • 一個JVM實例只存在一個堆內存,堆也是Java內存管理的核心區域。
  • Java 堆區在JVM啓動的時候即被創建,其空間大小也就確定了。是JVM管理的最大一塊內存空間。
  • 堆內存的大小是可以調節的。
  • 《Java虛擬機規範》規定,堆可以處於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的
  • 堆,是GC ( Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。
  • 在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候纔會被移除。

是不是所有的Java對象都放在堆上?

《Java虛擬機規範》中對Java堆的描述是:所有的對象實例以及數組都應當在運行時分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated ) 數組和對象可能永遠不會存儲在棧上,因爲棧幀中保存引用,這個引用指向對象或者數組在堆中的位置。

我要說的是:“幾乎”所有的對象實例都在這裏分配內存。——從實際使用角度看的。

舉例:

public class SimpleHeap {
    private int id;

    public SimpleHeap(int id) {
        this.id = id;
    }

    public void show() {
        System.out.println("My ID is " + id);
    }

    public static void main(String[] args) {
        SimpleHeap sl = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);
    }
}

 

 

線程和堆的關係

所有的線程共享Java堆,在這裏還可以劃分線程私有的緩衝區(Thread Local Allocation Buffer, TLAB)。

堆的內部結構

現代垃圾收集器大部分都基於分代收集理論設計,堆空間細分爲:

 

  • Java 7及之前堆內存邏輯上分爲三部分:新生區+養老區+永久區
    • Young Generation Space 新生區 Young/New
      • 又被劃分爲Eden區和Survivor區
  • Tenure generation space 養老區 Old/Tenure
  • Permanent Space 永久區 Perm

 

 

  • Java 8及之後堆內存邏輯上分爲三部分:新生區+養老區+元空間
    • Young Generation Space 新生區 Young/New
      • 又被劃分爲Eden區和Survivor區
    • Tenure generation space 養老區 Old/Tenure
    • Meta Space 元空間 Meta

約定: 新生區<=>新生代<=>年輕代
養老區<=>老年區<=>老年代
永久區<=>永久代

面試題

  • Java 堆的結構是什麼樣子的?(獵聘)
  • JVM內存爲什麼要分成新生代,老年代,持久代。新生代中爲什麼要分爲Eden和Survivor(字節跳動)
  • 堆裏面的分區:Eden,survival (from+ to),老年代,各自的特點。(京東-物流)
  • 堆的結構?爲什麼兩個survivor區? (螞蟻金服)
  • Eden和Survior的比例分配 (螞蟻金服)
  • JVM內存分區,爲什麼要有新生代和老年代 (小米)
  • JVM的內存結構,Eden和Survivor比例。 (京東)
  • JVM內存爲什麼要分成新生代,老年代,持久代。新生代中爲什麼要分爲Eden和Survivor。 (京東)
  • JVM內存分區,爲什麼要有新生代和老年代? (美團)
  • JVM的內存結構,Eden和Survivor比例。 (京東)

新生代與老年代

  • 存儲在JVM中的Java對象可以被劃分爲兩類:
    • 一類是生命週期較短的瞬時對象,這類對象的創建和消亡都非常迅速
    • 另外一類對象的生命週期卻非常長,在某些極端的情況下還能夠與JVM的生命週期保持一致。
  • Java堆區進一步細分的話,可以劃分爲年輕代(YoungGen)和老年代(OldGen)
  • 其中年輕代又可以劃分爲Eden空間、Survivor0空間和Survivor1空間(有時也叫做from區、to區)。

 

 

幾乎所有的Java對象都是在Eden區被new出來的。 絕大部分的Java對象的銷燬都在新生代進行了。 IBM 公司的專門研究表明,新生代中 80% 的對象都是“朝生夕死”的。

如何設置堆的大小?

新生代與老年代的比例

 

 

  • 配置新生代與老年代在堆結構的佔比。
    • 默認-XX:NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3
    • 可以修改-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5
  • 可以使用選項”-Xmn”設置新生代最大內存大小 這個參數一般使用默認值就可以了。

設置Eden、倖存者的比例

  • 在HotSpot中,Eden空間和另外兩個Survivor空間缺省所佔的比例是8:1:1
  • 當然開發人員可以通過選項“-XX:SurvivorRatio”調整這個空間比例。比如-XX:SurvivorRatio=8

常用參數

堆空間大小的設置: 
-Xms:初始內存 (默認爲物理內存的1/64);
-Xmx:最大內存(默認爲物理內存的1/4);

設置新生代的大小。(初始值及最大值)
-Xmn
通常默認即可。

配置新生代與老年代在堆結構的佔比。賦的值即爲老年代的佔比,剩下的1給新生代
默認-XX:NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3
-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5

在HotSpot中,Eden空間和另外兩個Survivor空間缺省所佔的比例是8:1
開發人員可以通過選項“-XX:SurvivorRatio”調整這個空間比例。比如-XX:SurvivorRatio=8

設置新生代垃圾的最大年齡。超過此值,仍未被回收的話,則進入老年代。
默認值爲15
-XX:MaxTenuringThreshold=0:表示年輕代對象不經過Survivor區,直接進入老年代。對於老年代比較多的應用,可以提高效率。
如果將此值設置爲一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象在年輕代的存活時間,增加在年輕代即被回收的概率。

輸出詳細的GC處理日誌
-XX:+PrintGcDetail

-XX:HandlePromotionFailure
在發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代所有對象的總空間,
 如果大於,則此次Minor GC是安全的
 如果小於,則虛擬機會查看-XX:HandlePromotionFailure設置值是否允許擔保失敗。
如果HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小,如果大於,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;如果小於或者HandlePromotionFailure=false,則改爲進行一次Full GC。
----------------------------
在JDK 6 Update 24之後,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察OpenJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,但是在代碼中已經不會再使用它。JDK 6 Update 24之後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。

-XX:+PrintFlagsFinal  :查看所有的參數的最終值(可能會存在修改,不再是初始值)
具體查看某個參數的指令:  jps:查看當前運行中的進程
                      jinfo -flag SurvivorRatio 進程id

對象分配

爲新對象分配內存是一件非常嚴謹和複雜的任務,JVM的設計者們不僅需要考慮內存如何分配、在哪裏分配等問題,並且由於內存分配算法與內存回收算法密切相關,所以還需要考慮GC執行完內存回收後是否會在內存空間中產生內存碎片。

金句:

針對倖存者s0,s1區的總結:複製之後有交換,誰空誰是to. 關於垃圾回收: 頻繁在新生區收集 很少在養老區收集 幾乎不在永久區/元空間收集

 

 

分配過程

1.new的對象先放伊甸園區。此區有大小限制。

2.當伊甸園的空間填滿時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC/YGC),將伊甸園區中的不再被其他對象所引用的對象進行銷燬。再加載新的對象放到伊甸園區

3.然後將伊甸園中的剩餘對象移動到倖存者0區。

4.如果再次觸發垃圾回收,此時上次倖存下來的放到倖存者0區的,如果沒有回收,就會放到倖存者1區。

5.如果再次經歷垃圾回收,此時會重新放回倖存者0區,接着再去倖存者1區。

6.啥時候能去養老區呢?可以設置次數。默認是15次。

可以設置參數:-XX:MaxTenuringThreshold= 設置對象晉升老年代的年齡閾值。 7.在養老區,相對悠閒。當養老區內存不足時,再次觸發GC:Major GC,進行養老區的內存清理。

8.若養老區執行了Major GC之後發現依然無法進行對象的保存,就會產生OOM異常

java.lang.OutOfMemoryError: Java heap space

 

內存分配策略(或對象提升(promotion)規則):

如果對象在Eden 出生並經過第一次MinorGC 後仍然存活,並且能被Survivor 容納的話,將被移動到Survivor 空間中,並將對象年齡設爲1 。對象在Survivor 區中每熬過一次MinorGC , 年齡就增加1歲,當它的年齡增加到一定程度(默認爲15 歲,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代中。

對象分配原則

針對不同年齡段的對象分配原則如下所示:

  • 優先分配到Eden
  • 大對象直接分配到老年代
    • 儘量避免程序中出現過多的大對象
  • 長期存活的對象分配到老年代
  • 動態對象年齡判斷
    • 如果Survivor 區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代,無須等到 MaxTenuringThreshold 中要求的年齡。
  • 空間分配擔保
    • -XX:HandlePromotionFailure
/** 測試:大對象直接進入老年代
 * -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
 */
public class YoungOldAreaTest {
    public static void main(String[] args) {
        byte[] buffer = new byte[1024 * 1024 * 20];//20m

    }
}

 

 

Minor GC 、Major GC、Full GC

JVM在進行GC時,並非每次都對上面三個內存(新生代、老年代;方法區)區域一起回收的,大部分時候回收的都是指新生代。

針對HotSpot VM的實現,它裏面的GC按照回收區域又分爲兩大種類型:

  • 一種是部分收集(Partial GC)
  • 一種是整堆收集(Full GC)

部分收集:不是完整收集整個Java堆的垃圾收集。 其中又分爲:

  • 新生代收集(Minor GC / Young GC):只是新生代(Eden\S0,S1)的垃圾收集
  • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。

目前,只有CMS GC會有單獨收集老年代的行爲。 注意,很多時候Major GC會和Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收。 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集。 目前,只有G1 GC會有這種行爲 整堆收集(Full GC):收集整個java堆和方法區的垃圾收集。

Minor GC觸發機制

  • 當年輕代空間不足時,就會觸發Minor GC。這裏的年輕代滿指的是Eden區滿,Survivor滿不會引發GC。(每次 Minor GC 會清理年輕代的內存。)
  • 因爲 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。這一定義既清晰又易於理解。
  • Minor GC會引發STW,暫停其它用戶的線程,等垃圾回收結束,用戶線程才恢復運行。

 

 

老年代GC(Major GC/Full GC)觸發機制:

  • 指發生在老年代的GC,對象從老年代消失時,我們說“Major GC”或“Full GC”發生了。

    • 出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。
    • 也就是在老年代空間不足時,會先嚐試觸發Minor GC。如果之後空間還不足,則觸發Major GC
  • Major GC的速度一般會比Minor GC慢10倍以上,STW的時間更長。

  • 如果Major GC 後,內存還不足,就報OOM了。

    Full GC觸發機制:

    觸發Full GC 執行的情況有如下五種:

(1)調用System.gc()時,系統建議執行Full GC,但是不必然執行

(2)老年代空間不足

(3)方法區空間不足

(4)通過Minor GC後進入老年代的平均大小大於老年代的可用內存

(5)由Eden區、survivor space0(From Space)區向survivor space1(To Space)區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

說明:full gc是開發或調優中儘量要避免的。這樣暫時時間會短一些。

public class OOMTest {
    public static void main(String[] args) {
        String str = "www.atguigu.com";
        //將參數調整的小一些,這樣問題會出現的比較早。
        // -Xms8m -Xmx8m -XX:+PrintGCDetails
        while(true){
            str += str + new Random().nextInt(88888888) +
                    new Random().nextInt(999999999);
        }
    }
}
/**

 * 測試MinorGC 、 MajorGC、FullGC
 * -Xms10m -Xmx10m -XX:+PrintGCDetails
 */
public class GCTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "atguigu.com";
            while (true) {
                list.add(a);
                a = a + a;
                i++;
            }
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println("遍歷次數爲:" + i);
        }
    }
}

OOM如何解決

1、要解決OOM異常或heap space的異常,一般的手段是首先通過內存映像分析工具(如Eclipse Memory Analyzer)對dump 出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)。

2、如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots 的引用鏈。於是就能找到泄漏對象是通過怎樣的路徑與GC Roots 相關聯並導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots 引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置。

3、如果不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx 與-Xms),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。

爲什麼需要把Java堆分代?不分代就不能正常工作了嗎?

其實不分代完全可以,分代的唯一理由就是優化GC性能。如果沒有分代,那所有的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而很多對象都是朝生夕死的,如果分代的話,把新創建的對象放到某一地方,當GC 的時候先把這塊存儲“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。

什麼是TLAB(快速分配策略)?

爲什麼有TLAB(Thread Local Allocation Buffer)快速分配策略?

  • 堆區是線程共享區域,任何線程都可以訪問到堆區中的共享數據
  • 由於對象實例的創建在JVM中非常頻繁,因此在併發環境下從堆區中劃分內存空間是線程不安全的
  • 爲避免多個線程操作同一地址,需要使用加鎖等機制,進而影響分配速度。

所以,多線程同時分配內存時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱之爲快速分配策略

什麼是TLAB?

從內存模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM爲每個線程分配了一個私有緩存區域,它包含在Eden空間內。 據我所知所有OpenJDK衍生出來的JVM都提供了TLAB的設計。

 

 

TLAB的說明:

  • 儘管不是所有的對象實例都能夠在TLAB中成功分配內存,但JVM確實是將TLAB作爲內存分配的首選。
  • 在程序中,開發人員可以通過選項“-XX:+/-UseTLAB”設置是否開啓TLAB空間。
  • 默認情況下,TLAB空間的內存非常小,僅佔有整個Eden空間的1%,當然我們可以通過選項“-XX:TLABWasteTargetPercent”設置TLAB空間所佔用Eden空間的百分比大小。
  • 一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試着通過使用加鎖機制確保數據操作的原子性,從而直接在Eden空間中分配內存。

 

 

 

 

https://zhuanlan.zhihu.com/p/445716284

 

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