JVM_05 運行時數據區2-堆

完整JVM學習筆記請戳

1.核心概述

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

  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,垃圾收集器)執行垃圾回收的重點區域

1.1 配置jvm及查看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目錄,個人是/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/bin下找到jvisualvm 運行(或者直接終端運行jvisualvm),查看進程,能夠看到咱們設置的配置信息
  • 能夠看到HeapDemo配置-Xms10m, 分配的10m被分配給了新生代3m和老年代7m

1.2 分析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];
    }
}
複製代碼

1.3 堆的細份內存結構

  • JDK 7之前: 新生區+養老區+永久區
    • Young Generation Space:又被分爲Eden區和Survior區 ==Young/New==
    • Tenure generation Space: ==Old/Tenure==
    • Permanent Space: ==Perm==
  • JDK 8之後: 新生區+養老區+元空間
    • Young Generation Space:又被分爲Eden區和Survior區 ==Young/New==
    • Tenure generation Space: ==Old/Tenure==
    • Meta Space: ==Meta==

2.設置堆內存大小與OOM

  • Java堆區用於存儲java對象實例,堆的大小在jvm啓動時就已經設定好了,能夠經過 "-Xmx"和 "-Xms"來進行設置
    • -Xms 用於表示堆的起始內存,等價於 -XX:InitialHeapSize
      • -Xms 用來設置堆空間(年輕代+老年代)的初始內存大小
        • -X 是jvm的運行參數
        • ms 是memory start
    • -Xmx 用於設置堆的最大內存,等價於 -XX:MaxHeapSize
  • 一旦堆區中的內存大小超過 -Xmx所指定的最大內存時,將會拋出OOM異常
  • ==一般會將-Xms和-Xmx兩個參數配置相同的值,其目的就是爲了可以在java垃圾回收機制清理完堆區後不須要從新分隔計算堆區的大小,從而提升性能==
  • 默認狀況下,初始內存大小:物理內存大小/64;最大內存大小:物理內存大小/4
    • 手動設置:-Xms600m -Xmx600m
  • 查看設置的參數:
    • 方式一: ==終端輸入jps== , 而後 ==jstat -gc 進程id==
    • 方式二:(控制檯打印)Edit Configurations->VM Options 添加 ==-XX:+PrintGCDetails==

2.1 查看堆內存大小

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();
        }
    }
}
複製代碼

2.2 堆大小分析

設置堆大小爲600m,打印出的結果爲575m,這是由於倖存者區S0和S1各佔據了25m,可是他們始終有一個是空的,存放對象的是伊甸園區和一個倖存者區
git

2.3 OOM

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

/**
 * -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];
    }
}
複製代碼

3.年輕代與老年代

  • 存儲在JVM中的java對象能夠被劃分爲兩類:面試

    • 一類是生命週期較短的瞬時對象,這類對象的建立和消亡都很是迅速
    • 另一類對象時生命週期很是長,在某些狀況下還能與JVM的生命週期保持一致
  • Java堆區進一步細分能夠分爲年輕代(YoungGen)和老年代(OldGen)算法

  • 其中年輕代能夠分爲Eden空間、Survivor0空間和Survivor1空間(有時也叫frmo區,to區)
    數組

  • 配置新生代與老年代在堆結構的佔比緩存

    • 默認-XX:NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3
    • 能夠修改-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5
  • 在hotSpot中,Eden空間和另外兩個Survivor空間缺省所佔的比例是8:1:1(測試的時候是6:1:1),開發人員能夠經過選項 -XX:SurvivorRatio 調整空間比例,如-XX:SurvivorRatio=8安全

  • 幾乎全部的Java對象都是在Eden區被new出來的bash

  • 絕大部分的Java對象都銷燬在新生代了(IBM公司的專門研究代表,新生代80%的對象都是「朝生夕死」的)多線程

  • 可使用選項-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();
        }
    }
}
複製代碼

4.圖解對象分配過程

4.1

爲新對象分配內存是件很是嚴謹和複雜的任務,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==
==關於垃圾回收:頻繁在新生區收集,不多在養老區收集,幾乎再也不永久區/元空間收集。==

4.2 對象分配的特殊狀況

4.3 代碼舉例

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();
            }
        }
    }
}
複製代碼

對應堆空間分配過程

4.4 經常使用調優工具

  • JDK命令行
  • Eclipse:Memory Analyzer Tool
  • Jconsole
  • VisualVM
  • Jprofiler
  • Java Flight Recorder
  • GCViewer
  • GC Easy

5.Minor GC、Major GC、Full GC

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

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

  • 部分收集:不是完整收集整個Java堆的垃圾收集。其中又分爲:
    • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
    • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
      • 目前,只有CMS GC會有單獨收集老年代的行爲
      • 注意,==不少時候Major GC 會和 Full GC混淆使用,須要具體分辨是老年代回收仍是整堆回收==
    • 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集
      • 目前,以後G1 GC會有這種行爲
  • 整堆收集(Full GC):收集整個java堆和方法區的垃圾收集
  • 年輕代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 是開發或調優中儘可能要避免的,這樣暫停時間會短一些

代碼演示

Young GC ->Full GC -> OOM

/** 測試GC分代回收
 * 測試MinorGC 、 MajorGC、FullGC
 * -Xms9m -Xmx9m -XX:+PrintGCDetails
 */
public class GCTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "testGC";
            while (true) {
                list.add(a);
                a = a + a;
                i++;
            }

        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println("遍歷次數爲:" + i);
        }
    }
}

複製代碼

日誌輸出

6.堆空間分代思想

爲何要把Java堆分代?不分代就不能正常工做了麼

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

7.內存分配策略

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

代碼示例

分配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
    }
}
複製代碼

日誌輸出

8.爲對象分配內存:TLAB(線程私有緩存區域)

爲何有TLAB(Thread Local Allocation Buffer)

  • 堆區是線程共享區域,任何線程均可以訪問到堆區中的共享數據
  • 因爲對象實例的建立在JVM中很是頻繁,淫纔在併發環境下從堆區中劃份內存空間是線程不安全的
  • 爲避免多個線程操做同一地址,須要使用加鎖等機制,進而影響分配速度

什麼是TLAB

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

說明

  • 儘管不是全部的對象實例都可以在TLAB中成功分配內存,單JV明確是是將TLAB做爲內存分配的首選
  • 在程序中,開發人員能夠經過選項「-XX:UseTLAB「 設置是夠開啓TLAB空間
  • 默認狀況下,TLAB空間的內存很是小,僅佔有整個EDen空間的1%,固然咱們能夠經過選項 」-XX:TLABWasteTargetPercent「 設置TLAB空間所佔用Eden空間的百分比大小
  • 一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試着經過使用加鎖機制確保數據操做的原子性,從而直接在Eden空間中分配了內存

代碼演示

  • 終端輸入 jsp,查看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對象分配過程

9.小結堆空間的參數設置

  • -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:是否設置空間分配擔保

說明

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

  • 若是大於,則這次Minor GC是安全的
  • 若是小於,則虛擬機會查看-XX:HandlePromotionFailure設置值是否容許擔保失敗。(==JDK 7之後的規則HandlePromotionFailure能夠認爲就是true==)
    • 若是HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小。
      • √若是大於,則嘗試進行一次Minor GC,但此次Minor GC依然是有風險的;
      • √若是小於,則改成進行一次Fu11 GC。
    • √若是HandlePromotionFailure=false,則改成進行一次Fu11 GC。

在JDK6 Update24以後(JDK7),HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察openJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,可是在代碼中已經不會再使用它。JDK6 Update24以後的規則變爲==只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。==

10.堆是分配對象的惟一選擇麼(不是)

  在《深刻理解Java虛擬機》中關於Java堆內存有這樣一段描述:隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,==棧上分配、標量替換優化技術==將會致使一些微妙的變化,全部的對象都分配到堆上也漸漸變得不那麼「絕對」了。
  在Java虛擬機中,對象是在Java堆中分配內存的,這是一個廣泛的常識。可是,有一種特殊狀況,那就是若是==通過逃逸分析(Escape Analysis)後發現,一個對象並無逃逸出方法的話,那麼就可能被優化成棧上分配==。這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最多見的堆外存儲技術。
  此外,前面提到的基於OpenJDK深度定製的TaoBaoVM,其中創新的GCIH(GCinvisible heap)技術實現off-heap,將生命週期較長的Java對象從heap中移至heap外,而且GC不能管理GCIH內部的Java對象,以此達到下降GC的回收頻率和提高GC的回收效率的目的。

  • 如何將堆上的對象分配到棧,須要使用逃逸分析手段。
  • 這是一種能夠有效減小Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。
  • 經過逃逸分析,Java Hotspot編譯器可以分析出一個新的對象的引用的使用範圍從而決定是否要將這個對象分配到堆上。
  • 逃逸分析的基本行爲就是分析對象動態做用域:
    • 當一個對象在方法中被定義後,==對象只在方法內部使用,則認爲沒有發生逃逸==。
    • 當一個對象在方法中被定義後,它被外部方法所引用,則認爲發生逃逸。例如做爲調用參數傳遞到其餘地方中。
  • ==如何快速的判斷是否發生了逃逸分析,就看new的對象實體是否有可能在方法外被調用==

代碼分析

public void method(){
    V v = new V();
    //use V
    //......
    v = null;
}
複製代碼

沒有發生逃逸的對象,則能夠分配到棧上,隨着方法執行的結束,棧空間就被移除。

public static StringBuffer createStringBuffer(String s1,String s2){
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}
複製代碼

因爲上述方法返回的sb在方法外被使用,發生了逃逸,上述代碼若是想要StringBuffer sb不逃出方法,能夠這樣寫:

public static String createStringBuffer(String s1,String s2){
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}
複製代碼

逃逸分析

/**
 * 逃逸分析
 *
 *  如何快速的判斷是否發生了逃逸分析,就看new的對象實體是否有可能在方法外被調用。
 */
public class EscapeAnalysis {

    public EscapeAnalysis obj;

    /*
    方法返回EscapeAnalysis對象,發生逃逸
     */
    public EscapeAnalysis getInstance(){
        return obj == null? new EscapeAnalysis() : obj;
    }
    /*
    爲成員屬性賦值,發生逃逸
     */
    public void setObj(){
        this.obj = new EscapeAnalysis();
    }
    //思考:若是當前的obj引用聲明爲static的?仍然會發生逃逸。

    /*
    對象的做用域僅在當前方法中有效,沒有發生逃逸
     */
    public void useEscapeAnalysis(){
        EscapeAnalysis e = new EscapeAnalysis();
    }
    /*
    引用成員變量的值,發生逃逸
     */
    public void useEscapeAnalysis1(){
        EscapeAnalysis e = getInstance();
        //getInstance().xxx()一樣會發生逃逸
    }
}
複製代碼

參數設置

  • 在JDK 6u23版本以後,HotSpot中默認就已經開啓了逃逸分析
  • 若是使用了較早的版本,開發人員能夠經過
    • -XX:DoEscapeAnalysis 顯式開啓逃逸分析
    • -XX:+PrintEscapeAnalysis查看逃逸分析的篩選結果

結論
開發中能使用局部變量的,就不要使用在方法外定義

代碼優化

使用逃逸分析,編譯器能夠對代碼作以下優化:

  1. 棧上分配:將堆分配轉化爲棧分配。若是一個對象在子線程中被分配,要使指向該對象的指針永遠不會逃逸,對象多是棧分配的候選,而不是堆分配
  2. 同步省略:若是一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操做能夠不考慮同步
  3. 分離對象或標量替換:有的對象可能不須要做爲一個連續的內存結構存在也能夠北方問道,那麼對象的部分(或所有)能夠不存儲在內存,而是存儲在CPU寄存器中。

棧上分配

  • JIT編譯器在編譯期間根據逃逸分析的結果,發現若是一個對象並無逃逸出方法的話,就可能被優化成棧上分配。分配完成以後,繼續在調用棧內執行,最後線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須機型垃圾回收了
  • 常見的棧上分配場景:給成員變量賦值、方法返回值、實例引用傳遞

代碼分析
如下代碼,關閉逃逸分析(-XX:-DoEscapeAnalysi),維護10000000個對象,若是開啓逃逸分析,只維護少許對象(JDK7 逃逸分析默認開啓)

/**
 * 棧上分配測試
 * -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
 */
public class StackAllocation {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        // 查看執行時間
        long end = System.currentTimeMillis();
        System.out.println("花費的時間爲: " + (end - start) + " ms");
        // 爲了方便查看堆內存中對象個數,線程sleep
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }

    private static void alloc() {
        User user = new User();//未發生逃逸
    }

    static class User {

    }
}
複製代碼

同步省略

  • 線程同步的代價是至關高的,同步的後果是下降併發性和性能
  • 在動態編譯同步塊的時候,JIT編譯器能夠藉助逃逸分析來判斷同步塊所使用的鎖對象是否只可以被一個線程訪問而沒有被髮布到其餘線程。若是沒有,那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提升併發性和性能。這個取消同步的過程就叫同步省略,也叫==鎖消除==
/**
 * 同步省略說明
 */
public class SynchronizedTest {
    public void f() {
        Object hollis = new Object();
        synchronized(hollis) {
            System.out.println(hollis);
        }
    }
    //代碼中對hollis這個對象進行加鎖,可是hollis對象的生命週期只在f()方法中
    //並不會被其餘線程所訪問控制,因此在JIT編譯階段就會被優化掉。
    //優化爲 ↓
    public void f2() {
        Object hollis = new Object();
        System.out.println(hollis);
    }
}

複製代碼

分離對象或標量替換

  • ==標量Scalar==是指一個沒法在分解成更小的數據的數據。Java中的原始數據類型就是標量
  • 相對的,那些還能夠分解的數據叫作==聚合量(Aggregate)==,Java中對象就是聚合量,由於它能夠分解成其餘聚合量和標量
  • 在JIT階段,若是通過逃逸分析,發現一個對象不會被外界訪問的話,那麼通過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來替代。這個過程就是標量替換
public class ScalarTest {
    public static void main(String[] args) {
        alloc();   
    }
    public static void alloc(){
        Point point = new Point(1,2);
    }
}
class Point{
    private int x;
    private int y;
    public Point(int x,int y){
        this.x = x;
        this.y = y;
    }
}
複製代碼

以上代碼,通過標量替換後,就會變成

public static void alloc(){
    int x = 1;
    int y = 2;
}
複製代碼

   能夠看到,Point這個聚合量通過逃逸分析後,發現他並無逃逸,就被替換成兩個標量了。那麼標量替換有什麼好處呢?就是能夠大大減小堆內存的佔用。由於一旦不須要建立對象了,那麼就再也不須要分配堆內存了。
   標量替換爲棧上分配提供了很好的基礎。

測試代碼

/**
 * 標量替換測試
 *  -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
 */
public class ScalarReplace {
    public static class User {
        public int id;//標量(沒法再分解成更小的數據)
        public String name;//聚合量(String還能夠分解爲char數組)
    }

    public static void alloc() {
        User u = new User();//未發生逃逸
        u.id = 5;
        u.name = "www.atguigu.com";
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("花費的時間爲: " + (end - start) + " ms");
    }
}
複製代碼

逃逸分析小結

  • 關於逃逸分析的論文在1999年就已經發表了,但直到JDK1.6纔有實現,並且這項技術到現在也並非十分紅熟的。
  • 其根本緣由就是沒法保證逃逸分析的性能消耗必定能高於他的消耗。雖然通過逃逸分析能夠作標量替換、棧上分配、和鎖消除。可是逃逸分析自身也是須要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。
  • 一個極端的例子,就是通過逃逸分析以後,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
  • 雖然這項技術並不十分紅熟,可是它也是即時編譯器優化技術中一個十分重要的手段。
  • 注意到有一些觀點,認爲經過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,可是取決於JVM設計者的選擇。據我所知,Oracle HotspotJVM中並未這麼作,這一點在逃逸分析相關的文檔裏已經說明,因此能夠明確全部的對象實例都是建立在堆上。
  • 目前不少書籍仍是基於JDK7之前的版本,JDK已經發生了很大變化,intern字符串的緩存和靜態變量曾經都被分配在永久代上,而永久代已經被元數據區取代。可是,intern字符串緩存和靜態變量並非被轉移到元數據區,而是直接在堆上分配,因此這一點一樣符合前面一點的結論:==對象實例都是分配在堆上==。
  • 年輕代是對象的誕生、省長、消亡的區域,一個對象在這裏產生、應用、最後被垃圾回收器收集、結束生命
  • 老年代防止長生命週期對象,一般都是從Survivor區域篩選拷貝過來的Java對象。固然,也有特殊狀況,咱們知道普通的對象會被分配在TLAB上,若是對象較大,JVM會試圖直接分配在Eden其餘位置上;若是對象他打,徹底沒法在新生代找到足夠長的連續空閒空間,JVM就會直接分配到老年代
  • 當GC只發生在年輕代中,回收年輕對象的行爲被稱爲MinorGC。當GC發生在老年代時則被稱爲MajorGC或者FullGC。通常的,MinorGC的發生頻率要比MajorGC高不少,即老年代中垃圾回收發生的頻率大大低於年輕代


JVM學習代碼及筆記(陸續更新中...)

【代碼】
github.com/willShuhuan…
【筆記】
JVM_01 簡介
JVM_02 類加載子系統
JVM_03 運行時數據區1- [程序計數器+虛擬機棧+本地方法棧]
JVM_04 本地方法接口
JVM_05 運行時數據區2-堆
JVM_06 運行時數據區3-方法區
JVM_07 運行時數據區4-對象的實例化內存佈局與訪問定位+直接內存
JVM_08 執行引擎(Execution Engine)
JVM_09 字符串常量池StringTable
JVM_10 垃圾回收1-概述+相關算法
JVM_11 垃圾回收2-垃圾回收相關概念
JVM_12 垃圾回收3-垃圾回收器

相關文章
相關標籤/搜索