JVM運行時數據區--堆


一個進程對應一個jvm實例,一個運行時數據區,又包含多個線程,這些線程共享了方法區和堆,每個線程包含了程序計數器、本地方法棧和虛擬機棧。

核心概述

1.一個jvm實例只存在一個堆內存,堆也是java內存管理的核心區域

2.Java堆區在JVM啟動的時候即被創建,其空間大小也就確定了。是JVM管理的最大一塊內存空間(堆內存的大小是可以調節的)

3.《Java虛擬機規范》規定,堆可以處於物理上不連續的內存空間中,但在邏輯上它應該被視為連續的。

4.所有的線程共享java堆,在這里還可以划分線程私有的緩沖區(TLAB:Thread Local Allocation Buffer).(面試問題:堆空間一定是所有線程共享的么?不是,TLAB線程在堆中獨有的)

5.《Java虛擬機規范》中對java堆的描述是:所有的對象實例以及數組都應當在運行時分配在堆上。從實際使用的角度看,“幾乎”所有的對象的實例都在這里分配內存 (‘幾乎’是因為可能存儲在棧上,另見逃逸分析)

6。數組或對象永遠不會存儲在棧上,因為棧幀中保存引用,這個引用指向對象或者數組在堆中的位置。

7.在方法結束后,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除

8.堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域

配置堆內存及查看jvm進程

編寫HeapDemo/HeapDemo1代碼:

public class HeapDemo {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}

首先對虛擬機進行配置,如圖 Run-Edit configurations:

在jdk目錄,jdk1.8.0_171.jdk/Contents/Home/bin下找到jvisualvm 運行(或者直接終端運行jvisualvm),查看進程,可以看到我們設置的配置信息:

 可以看到HeapDemo配置-Xms10m, 分配的10m被分配給了新生代3m和老年代7m:

 分析SimpleHeap的jvm情況

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);

        int[] arr = new int[10];

        Object[] arr1 = new Object[10];
    }
}

堆的細分內存結構

JDK 7以前: 邏輯上分為新生區+養老區+永久區(即Xms/Xmx分配的內存物理上沒有涉及永久區)

  • Young Generation Space:又被分為Eden區和Survior區
  • Tenure generation Space: ==Old/Tenure==
  • Permanent Space: ==Perm==

 

JDK 8以后: 邏輯上分為新生區+養老區+元空間(即Xms/Xmx分配的內存物理上沒有涉及元空間)
  • Young Generation Space:又被分為Eden區和Survior區 
  • Tenure generation Space: ==Old/Tenure==
  • Meta Space: ==Meta==

設置堆內存大小與OOM

1.Java堆區用於存儲java對象實例,堆的大小在jvm啟動時就已經設定好了,可以通過 "-Xmx"和 "-Xms"來進行設置

  • -Xms 用來設置堆空間(年輕代+老年代)的初始內存大小,等價於 -XX:InitialHeapSize
    • -X 是jvm的運行參數
    • ms 是memory start
  • -Xmx 用於設置堆的最大內存,等價於 -XX:MaxHeapSize

2.一旦堆區中的內存大小超過 -Xmx所指定的最大內存時,將會拋出OOM異常。

  • 默認情況下,初始內存大小:物理內存大小/64;最大內存大小:物理內存大小/4。
  • 手動設置:-Xms600m -Xmx600m

3.通常會將-Xms和-Xmx兩個參數配置相同的值,其目的就是為了能夠在java垃圾回收機制清理完堆區后不需要重新分隔計算堆區的大小,從而提高性能。

  • 比如說:默認空余堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制;空余堆內存大於70%時,JVM會減少堆直到 -Xms的最小限制。
    因此服務器一般設置-Xms、-Xmx相等以避免在每次GC 后調整堆的大小

4.查看設置的堆內存參數:

  • 方式一: 終端輸入jps , 然后jstat -gc 進程id。
  • 方式二:(控制台打印)Edit Configurations->VM Options 添加 -XX:+PrintGCDetails

查看堆內存大小測試代碼

public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虛擬機中的堆內存總量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虛擬機試圖使用的最大堆內存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M
        System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M

        System.out.println("系統內存大小為:" + initialMemory * 64.0 / 1024 + "G");//系統內存大小為:15.3125G
        System.out.println("系統內存大小為:" + maxMemory * 4.0 / 1024 + "G");//系統內存大小為:14.22265625G

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

堆大小分析

設置堆大小為600m,打印出的結果為575m,這是因為幸存者區S0和S1各占據了25m,但是他們始終有一個是空的,存放對象的是伊甸園區和一個幸存者區。

 

OOM示例

java.lang.OutOfMemoryError: Java heap space。代碼示例:

/**
 * -Xms600m -Xmx600m
 */
public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while(true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(1024 * 1024)));
        }
    }
}

class Picture{
    private byte[] pixels;

    public Picture(int length) {
        this.pixels = new byte[length];
    }
}

年輕代與老年代

1.存儲在JVM中的java對象可以被划分為兩類:

  • 一類是生命周期較短的瞬時對象,這類對象的創建和消亡都非常迅速
  • 另外一類對象時生命周期非常長,在某些情況下還能與JVM的生命周期保持一致

2.Java堆區進一步細分可以分為年輕代(YoungGen)和老年代(OldGen)。其中年輕代可以分為Eden空間、Survivor0空間和Survivor1空間(有時也叫frmo區,to區)

3.配置新生代與老年代在堆結構的占比

  • 默認-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個堆的1/5

4.在hotSpot中,Eden空間和另外兩個Survivor空間缺省所占的比例是8:1:1(測試的時候是6:1:1),開發人員可以通過選項 -XX:SurvivorRatio 調整空間比例,如-XX:SurvivorRatio=8

5.幾乎所有的Java對象都是在Eden區被new出來的

6.絕大部分的Java對象都銷毀在新生代了(IBM公司的專門研究表明,新生代80%的對象都是“朝生夕死”的)

7.可以使用選項-Xmn設置新生代最大內存大小(這個參數一般使用默認值就好了)

測試代碼:
/**
 * -Xms600m -Xmx600m
 *
 * -XX:NewRatio : 設置新生代與老年代的比例。默認值是2.
 * -XX:SurvivorRatio :設置新生代中Eden區與Survivor區的比例。默認值是8
 * -XX:-UseAdaptiveSizePolicy :關閉自適應的內存分配策略 '-'關閉,'+'打開  (暫時用不到)
 * -Xmn:設置新生代的空間的大小。 (一般不設置)
 *
 */
public class EdenSurvivorTest {
    public static void main(String[] args) {
        System.out.println("我只是來打個醬油~");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

圖解對象分配的一般過程

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

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

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

3.然后將伊甸園中的剩余的幸存對象移動到幸存者0區。

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

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

6.啥時候能去養老區呢?可以設置次數。默認是15次。·可以設置參數:-XX:MaxTenuringThreshold=進行設置。

7.在養老區,相對悠閑。當老年區內存不足時,再次觸發GC:Major GC,進行養老區的內存清理。

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

總結:針對幸存者s0,s1區:復制之后有交換,誰空誰是to。
           關於垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不再永久區/元空間收集。

注意:只有伊甸園滿了才會觸minorGC/youngGC,而幸存者區滿了是絕對不會觸發minorGC的。

對象分配的特殊情況

 

 代碼舉例:

public class HeapInstanceTest {
    byte[] buffer = new byte[new Random().nextInt(1024 * 200)];

    public static void main(String[] args) {
        ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
        while (true) {
            list.add(new HeapInstanceTest());
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

對應堆空間分配過程:

常用調優工具

1.JDK命令行

2.Eclipse:Memory Analyzer Tool

3.Jconsole

4.VisualVM

5.Jprofiler

6.Java Flight Recorder

7.GCViewer

8.GC Easy

Minor GC、Major GC、Full GC

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

針對hotSpot VM的實現,它里面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)

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

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

2.整堆收集(Full GC):收集整個java堆和方法區的垃圾收集。

不同GC的觸發機制

年輕代GC(Minor GC)觸發機制
  • 當年輕代空間不足時,就會觸發Minor GC,這里的年輕代滿指的是Eden代滿,Survivor滿不會引發GC.(每次Minor GC會清理年輕代的內存,Survivor是被動GC,不會主動GC)
  • 因為Java隊形大多都具備朝生夕滅的特性,所以Monor GC 非常頻繁,一般回收速度也比較快,這一定義既清晰又利於理解。
  • Minor GC 會引發STW(Stop the World),暫停其他用戶的線程,等垃圾回收結束,用戶線程才恢復運行。
老年代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執行的情況有以下五種
    • ①調用System.gc()時,系統建議執行Full GC,但是不必然執行
    • ②老年代空間不足
    • ③方法區空間不足
    • ④通過Minor GC后進入老年代的平均大小,“大於”老年代的可用內存
    • ⑤由Eden區,Survivor S0(from)區向S1(to)區復制時,對象大小由於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
  • 說明:Full GC 是開發或調優中盡量要避免的,這樣暫停時間會短一些

堆空間分代思想

為什么要把Java堆分代?不分代就不能正常工作了么

  • 經研究,不同對象的生命周期不同。70%-99%的對象都是臨時對象。
    • 新生代:有Eden、Survivor構成(s0,s1 又稱為from to),to總為空
    • 老年代:存放新生代中經歷多次依然存活的對象
  • 其實不分代完全可以,分代的唯一理由就是優化GC性能。如果沒有分代,那所有的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描,而很多對象都是朝生夕死的,如果分代的話,把新創建的對象放到某一地方,當GC的時候先把這塊存儲“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。

內存分配策略總結

  • 如果對象在Eden出生並經過第一次Minor GC后依然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,把那個將對象年齡設為1.對象在Survivor區中每熬過一次MinorGC,年齡就增加一歲,當它的年齡增加到一定程度(默認15歲,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代中
    • 對象晉升老年代的年齡閾值,可以通過選項 -XX:MaxTenuringThreshold來設置
  • 針對不同年齡段的對象分配原則如下:
    • 優先分配到Eden
    • 大對象直接分配到老年代
      • 我們要盡量避免程序中出現過多的大對象
    • 長期存活的對象分配到老年代
    • 動態對象年齡判斷
      • 如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入到老年代。無需等到MaxTenuringThreshold中要求的年齡

大對象直接進入老年代舉例

分配60m堆空間,新生代 20m ,Eden 16m, s0 2m, s1 2m,buffer對象20m,Eden 區無法存放buffer, 直接晉升老年代。

/** 測試:大對象直接進入老年代
 * -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
 */
public class YoungOldAreaTest {
    // 新生代 20m ,Eden 16m, s0 2m, s1 2m
    // 老年代 40m
    public static void main(String[] args) {
        //Eden 區無法存放buffer  晉升老年代
        byte[] buffer = new byte[1024 * 1024 * 20];//20m
    }
}

日志輸出:

空間分配擔保機制

簡單解釋一下為什么會出現這種情況: 因為給 allocation2 分配內存的時候 eden 區內存幾乎已經被分配完了,我們剛剛講了當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC.GC 期間虛擬機又發現 allocation1 無法存入 Survivor 空間,所以只好通過 分配擔保機制:把新生代的對象提前轉移到老年代中去,老年代上的空間足夠存放 allocation1,所以不會出現 Full GC。執行 Minor GC 后,后面分配的對象如果能夠存在 eden 區的話,還是會在 eden 區分配內存。可以執行如下代碼驗證:

public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
        allocation1 = new byte[32000*1024];
        allocation2 = new byte[1000*1024];
        allocation3 = new byte[1000*1024];
        allocation4 = new byte[1000*1024];
        allocation5 = new byte[1000*1024];
    }
}

 

新概念:TLAB(堆當中的線程私有緩存區域)

為什么有TLAB(Thread Local Allocation Buffer)

  • 眾所周知堆區是線程共享區域,任何線程都可以訪問到堆區中的共享數據。由於對象實例的創建在JVM中非常頻繁,因此在並發環境下從堆區中划分內存空間是線程不安全的。
  • 一般為了避免多個線程操作同一地址,需要使用加鎖等機制,進而影響分配速度。
  • 為了解決這一問題,TLAB應運而生。

什么是TLAB

  • 從內存模型而不是垃圾收集的角度,對Eden區域繼續進行划分,JVM為每個線程分配了一個私有緩存區域,它包含在Eden空間內
  • 多線程同時分配內存時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱之為快速分配策略
  • 所有OpenJDK衍生出來的JVM都提供了TLAB的設計

 

 

TLAB說明

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

代碼演示

  • 運行程序后,終端輸入jps查看TLABArgsTest進程id
  • jinfo -flag UseTLAB 64566(進程id),輸出-XX:+UseTLAB,證明TLAB默認是開啟的
/**
 * 測試-XX:UseTLAB參數是否開啟的情況:默認情況是開啟的
 */
public class TLABArgsTest {
    public static void main(String[] args) {
        System.out.println("我只是來打個醬油~");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

TLAB對象分配過程

 

堆空間的參數設置

  • -XX:PrintFlagsInitial: 查看所有參數的默認初始值
  • -XX:PrintFlagsFinal:查看所有的參數的最終值(可能會存在修改,不再是初始值)
    • 具體查看某個參數的指令:
      • jps:查看當前運行中的進程
      • jinfo -flag SurvivorRatio 進程id: 查看新生代中Eden和S0/S1空間的比例
  • -Xms: 初始堆空間內存(默認為物理內存的1/64)
  • -Xmx: 最大堆空間內存(默認為物理內存的1/4)
  • -Xmn: 設置新生代大小(初始值及最大值)
  • -XX:NewRatio: 配置新生代與老年代在堆結構的占比
  • -XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例
  • -XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡(默認15)
  • -XX:+PrintGCDetails:輸出詳細的GC處理日志
    • 打印gc簡要信息:① -XX:+PrintGC ② -verbose:gc
  • -XX:HandlePromotionFailure:是否設置空間分配擔保

HandlePromotionFailure參數說明

在發生Minor Gc之前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代所有對象的總空間。

  • 如果大於,則此次Minor GC是安全的
  • 如果小於,則虛擬機會查看-XX:HandlePromotionFailure設置值是否允許擔保失敗。(==JDK 7以后的規則HandlePromotionFailure可以認為就是true==)
    • 如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小。
      • √如果大於,則嘗試進行一次Minor GC,只是嘗試看能否觸發分配擔保(我們肯定希望的是分配擔保成功Eden-->old/tentired),但這次Minor GC依然是有風險的;
      • √如果小於,則改為進行一次Full GC。
    • √如果HandlePromotionFailure=false,則改為進行一次Full GC。
注意:在JDK6 Update24之后(JDK7),HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察openJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,但是在代碼中已經不會再使用它。
JDK6 Update24之后的規則變為:只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。

 


 

 

 


免責聲明!

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



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