Java JVM——8.堆

堆的核心概念

  堆針對一個 JVM 進程來講是惟一的,也就是一個進程只有一個JVM,可是進程包含多個線程,他們是共享同一堆空間的。html

  一個JVM實例只存在一個堆內存,堆也是Java內存管理的核心區域。java

  Java堆區在JVM啓動的時候即被建立,其空間大小也就肯定了。它是 JVM 管理的最大一塊內存空間。算法

  《Java虛擬機規範》規定,堆能夠處於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的。全部的線程共享Java堆,在堆中還能夠劃分線程私有的緩衝區(Thread Local Allocation Buffer,TLAB)。數組

堆內存的大小如何調節?

-Xms10m:設置最小堆內存爲10M。緩存

-Xmx10m:設置最大堆內存爲10M。安全

使用 Java VisualVM (jdk bin提供的插件)查看堆空間:多線程

 

  《Java虛擬機規範》中對Java堆的描述是:全部的對象實例以及數組都應當在運行時分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)併發

  要注意的是:「幾乎」全部的對象實例都在這裏分配內存——是從實際使用角度看的。由於還有一些對象是在棧上分配的。oracle

  數組和對象可能永遠不會存儲在棧上,由於棧幀中保存引用,這個引用指向對象或者數組在堆中的位置。app

  在方法結束後,堆中的對象不會立刻被移除,僅僅在垃圾收集的時候纔會被移除,也就是觸發了GC的時候,纔會進行回收。若是堆中對象立刻被回收,那麼用戶線程就會收到影響。

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

 


堆的內存分配

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

(等價)約定:

  新生區 -> 新生代 -> 年輕代 

  養老區 -> 老年區 -> 老年代

  永久區 -> 永久代

堆空間內部結構,JDK1.8以前從永久代 替換成 元空間:

 


設置堆內存大小與OOM

  Java堆區用於存儲Java對象實例,那麼堆的大小在JVM啓動時就已經設定好了,你們能夠經過選項"-Xmx"和"-Xms"來進行設置。

  「-Xms"用於表示堆區的起始內存,等價於-XX:InitialHeapSize。

  「-Xmx"用於表示堆區的最大內存,等價於-XX:MaxHeapSize。

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

  一般會將-Xms和-Xmx兩個參數配置相同的值,其目的是爲了可以在java垃圾回收機制清理完堆區後不須要從新分隔計算堆區的大小,從而提升性能

默認狀況下:

  初始內存大小:電腦物理內存大小/64。

  最大內存大小:電腦物理內存大小/4。

查看堆內存狀況

/** * -Xms 用來設置堆空間(年輕代+老年代)的初始內存大小 * -X:是jvm運行參數 * ms:memory start * -Xmx:用來設置堆空間(年輕代+老年代)的最大內存大小 * * @author: 若離 * */
public class Test { 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"); System.out.println("-Xmx:" + maxMemory + "M"); } }

運行結果:

查看堆內存的內存分配狀況

查看JVM堆內存GC狀況:

    jps(查看JVM進程)--> jstat -gc 進程id(查看JVM的GC狀況)

    jstat顯示結果:各參數所表明的內容

JVM堆內存GC狀況

配置JVM參數,查看堆內存分配狀況:(推薦)

    -XX:+PrintGCDetails

Eclipse下配置:

    當前項目程序編寫界面下點擊右鍵 --> Run As --> Run configurations --> Arguments --> VM arguments中填寫參數 --> Apply --> Run。

 堆內存分配狀況

OutOfMemoryError示例

public class Test { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); while(true) { list.add(666666666); } } }

運行結果:

 


年輕代和老年代

存儲在JVM中的Java對象能夠被劃分爲兩類:

  ➷ 一類是生命週期較短的瞬時對象,這類對象的建立和消亡都很是迅速,生命週期短的,及時回收便可。

  ➷ 另一類對象的生命週期卻很是長,在某些極端的狀況下還可以與JVM的生命週期保持一致。

  Java堆區進一步細分的話,能夠劃分爲年輕代(YoungGen)和老年代(oldGen),其中年輕代又能夠劃分爲Eden空間、Survivor0空間和Survivor1空間(有時也叫作from區、to區)。

下面這參數開發中通常不會調:

  ★ Eden:From:to - > 8:1:1

  ★ 新生代:老年代 - > 1 : 2

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

  ✎ 默認狀況下:-XX:NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3。

  ✎ 能夠自定義:-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5。

  當發如今整個項目中,生命週期長的對象偏多,那麼就能夠經過調整 老年代的大小,來進行調優。

  在HotSpot中,Eden空間和另外兩個survivor空間默認所佔的比例是8:1:1,固然開發人員能夠經過選項「-XX:SurvivorRatio」調整這個空間比例,好比:-XX:SurvivorRatio=8。

  幾乎全部的Java對象都是在Eden區被new出來的,絕大部分的Java對象的銷燬都在新生代進行了。(有些大的對象在Eden區沒法存儲時候,將直接進入老年代)。

IBM公司的專門研究代表,新生代中80%的對象都是「朝生夕死」的。

可使用選項"-Xmn"設置新生代最大內存大小。

這個參數通常使用默認值就能夠了。

 


圖解內存分配過程

概念

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

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

  ➷ 當伊甸園的空間填滿時,程序又須要建立對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(MinorGC),將伊甸園區中的再也不被其餘對象所引用的對象進行銷燬,而後將伊甸園中的剩餘對象移動到倖存者0區(或者1區,不肯定),再加載新的對象放到伊甸園區。

  ➷ 若是再次觸發垃圾回收,此時上次倖存下來的放到倖存者0區的對象,若是沒有被回收,將會和本次伊甸園區中倖存下來的對象一塊兒放到倖存者1區。

  ➷ 若是再次經歷垃圾回收,此時會將兩個有對象的區中倖存下來的對象從新放回倖存者0區。

  ➷ 以後,再放到倖存者1區,循環往復。

  ➷ 啥時候能去養老區呢?能夠設置次數,默認是15次。能夠經過設置參數:-XX:MaxTenuringThreshold= N 進行最大年齡的設置。

  ➷ 在養老區,相對清閒。當養老區內存不足時,再次觸發GC:Major GC,進行養老區的內存清理。

  ➷ 若養老區執行了Major GC以後,發現依然沒法進行對象的保存,就會產生OOM異常。

圖解過程

 

  當咱們進行一次垃圾收集後,紅色的將會被回收,而綠色的還會被佔用着,存放在S0(Survivor From)區。同時咱們給每一個對象設置了一個年齡計數器,第一次回收後就是1。

  同時Eden區繼續存放對象,當Eden區再次存滿的時候,又會觸發一個MinorGC操做,此時GC將會把 Eden和Survivor From中的對象進行一次收集,把存活的對象放到 Survivor To區,同時讓年齡 + 1。

 

  

  咱們繼續不斷的進行對象生成 和 垃圾回收,當Survivor中的對象的年齡達到15的時候,將會觸發一次 Promotion晉升的操做,也就是將年輕代中的對象 晉升到 老年代中。

 

注意事項

  ★ 在Eden區滿了的時候,纔會觸發MinorGC,而倖存者區滿了後,不會觸發MinorGC操做。

  ★ 若是Survivor區滿了後,將會觸發一些特殊的規則,也就是可能直接晉升老年代,具體分析請往下看。

對象分配的特殊狀況

代碼演示對象分配過程

  當咱們不斷的建立大對象時:

public class HeapInstanceTest { byte [] buffer = new byte[new Random().nextInt(1024 * 200)]; public static void main(String[] args) throws InterruptedException { ArrayList<HeapInstanceTest> list = new ArrayList<>(); while (true) { list.add(new HeapInstanceTest()); Thread.sleep(10); } } }

  設置參數:-Xms600m -Xmx600m

  而後cmd輸入:jvisualvm,打開VisualVM圖形化界面

  最終,在老年代和新生代都滿了,就出現OOM。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.atguigu.java.chapter08.HeapInstanceTest.<init>(HeapInstanceTest.java:13) at com.atguigu.java.chapter08.HeapInstanceTest.main(HeapInstanceTest.java:17)

經常使用的調優工具

  ➷ JDK命令行

  ➷ Eclipse:Memory Analyzer Tool

  ➷ Jconsole

  ➷ Visual VM(實時監控 推薦)

  ➷ Jprofiler(推薦)

  ➷ Java Flight Recorder(實時監控)

  ➷ GCViewer

  ➷ GCEasy

總結

  ✔ 針對倖存者s0,s1區的總結:複製以後有交換,誰空誰是to。

  ✔ 關於垃圾回收:頻繁在新生區收集,不多在老年代收集,幾乎再也不永久代或元空間進行收集(也就是方法區)。

  ✔ 新生代採用複製算法的目的:是爲了減小內存碎片。

 


Minor GC,Major GC,Full GC

  Minor GC:新生代的GC。

  Major GC:老年代的GC。

  Full GC:整堆收集,收集整個Java堆和方法區的垃圾收集。

  咱們都知道,JVM的調優的一個環節,也就是垃圾收集,咱們須要儘可能的避免垃圾回收,由於在垃圾回收的過程當中,容易出現STW的問題,而 Major GC 和 Full GC出現STW的時間,是Minor GC的10倍以上。

  JVM在進行GC時,並不是每次都對上面三個內存區域一塊兒回收的,大部分時候回收的都是指新生代。針對Hotspot VM的實現,它裏面的GC按照回收區域又分爲兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(FullGC)。

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

    ➷ 新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集。

    ➷ 老年代收集(MajorGC/oldGC):只是老年代的垃圾收集。

    目前,只有CMS GC會有單獨收集老年代的行爲。

    注意,不少時候Major GC會和Fu11GC混淆使用,須要具體分辨是老年代回收仍是整堆回收。

  ★ 混合收集(MixedGC):收集整個新生代以及部分老年代的垃圾收集。

    目前,只有G1 GC會有這種行爲。

  ★ 整堆收集(FullGC):收集整個java堆和方法區的垃圾收集。

Minor GC

  當年輕代空間不足時,就會觸發MinorGC,這裏的年輕代滿指的是Eden代滿,Survivor滿不會引起GC。(每次Minor GC會清理年輕代的內存。)

  由於Java對象大多都具有『朝生夕滅』的特性,因此Minor GC很是頻繁,通常回收速度也比較快。

  Minor GC會引起STW:暫停其它用戶的線程,等垃圾回收結束,用戶線程才恢復運行。

STW:stop the word。

Major GC

  指發生在老年代的GC,對象從老年代消失時,咱們說 「Major Gc」 或 「Full GC」 發生了。

  出現了Major GC,常常會伴隨至少一次的Minor GC(但非絕對的,在Paralle1 Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。

  也就是在老年代空間不足時,會先嚐試觸發Minor GC,若是以後空間還不足,則觸發Major GC。

  Major GC的速度通常會比MinorGc慢10倍以上,STW的時間更長,若是Major GC後,內存還不足,就報OOM了。

Full GC

觸發Fu11 GC執行的狀況有以下五種:

  ① 調用System.gc()時,系統建議執行Fu11 GC,可是沒必要然執行。

  ② 老年代空間不足。

  ③ 方法區空間不足。

  ④ 經過Minor GC後進入老年代的平均大小大於老年代的可用內存。

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

  注意:Full GC 是開發或調優中儘可能要避免的,這樣暫時時間會短一些。觸發OOM的時候,必定是進行了一次Full GC,由於只有在老年代空間不足時候,纔會爆出OOM異常。

 


堆空間分代思想

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

  經研究,不一樣對象的生命週期不一樣,70%-99%的對象是臨時對象。

新生代:有Eden、兩塊大小相同的survivor(又稱爲from/to,s0/s1)構成,to總爲空。

老年代:存放新生代中經歷屢次GC仍然存活的對象。

  其實不分代徹底能夠,分代的惟一理由就是優化GC性能。

  若是沒有分代,那全部的對象都在一塊,就如同把一個學校的人都關在一個教室,GC的時候要找到哪些對象沒用,這樣就會對堆的全部區域進行掃描,而不少對象都是朝生夕死的。

  若是分代的話,把新建立的對象放到某一地方,當GC的時候先把這塊存儲「朝生夕死」對象的區域進行回收,這樣就會騰出很大的空間出來。

 


內存分配策略

  若是對象在 Eden 出生並通過第一次 Minor GC 後仍然存活,而且能被 Survivor容納的話,將被移動到survivor空間中,並將對象年齡設爲1。

  對象在survivor區中每熬過一次Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲,其實每一個JVM、每一個GC都有所不一樣)時,就會被晉升到老年代。

  對象晉升老年代的年齡閥值,能夠經過選項 -XX:MaxTenuringThreshold來設置。

針對不一樣年齡段的對象分配原則以下所示:

  ✔ 優先分配到Eden

    開發中比較長的字符串或者數組,會直接存在老年代,可是由於新建立的對象都是『朝生夕死』的,因此這個大對象可能也很快被回收,可是由於老年代觸發Major GC的次數比 Minor GC要更少,所以可能回收起來就會比較慢。

  ✔ 大對象直接分配到老年代

    儘可能避免程序中出現過多的大對象。

  ✔ 長期存活的對象分配到老年代

  ✔ 動態對象年齡判斷

    若是Survivor區中相同年齡的全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

  ✔ 空間分配擔保: -XX:HandlePromotionFailure

  ★ 也就是通過Minor GC後,全部的對象都是存活的,由於Survivor比較小,因此就須要將Survivor沒法容納的對象,存放到老年代中。

 


爲對象分配內存:TLAB

堆空間都是共享的麼?

  不必定,由於還有TLAB這個概念,在堆中劃分出一塊區域,爲每一個線程所獨佔。

爲何有TLAB?

  TLAB:Thread Local Allocation Buffer,也就是爲每一個線程單獨分配了一個緩衝區。

  堆區是線程共享區域,任何線程均可以訪問到堆區中的共享數據。

  因爲對象實例的建立在JVM中很是頻繁,所以在併發環境下從堆區中劃份內存空間是線程不安全的,爲避免多個線程操做同一地址,須要使用加鎖等機制,進而影響分配速度。

什麼是TLAB?

  從內存模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM爲每一個線程分配了一個私有緩存區域,它包含在Eden空間內。

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

  據分析,全部OpenJDK衍生出來的JVM都提供了TLAB的設計。

  儘管不是全部的對象實例都可以在TLAB中成功分配內存,但JVM確實是將TLAB做爲內存分配的首選。

  在程序中,開發人員能夠經過選項「-XX:UseTLAB」設置是否開啓TLAB空間。默認狀況下,TLAB空間的內存很是小,僅佔有整個Eden空間的1%。固然,咱們能夠經過選項「-XX:TLABWasteTargetPercent」設置TLAB空間所佔用Eden空間的百分比大小。

  一旦對象在TLAB空間分配內存失敗時(放不下),JVM就會嘗試着經過使用加鎖機制確保數據操做的原子性,從而直接在Eden空間中分配內存。

TLAB分配過程

  對象首先是經過TLAB開闢空間,若是不能放入,那麼須要經過Eden來進行分配。

 


堆空間的參數設置

  ➷ -XX:+PrintFlagsInitial:查看全部的參數的默認初始值。

  ➷ -XX:+PrintFlagsFinal:查看全部的參數的最終值(可能會存在修改,再也不是初始值)。

  ➷ -Xms:初始堆空間內存。(默認爲物理內存的1/64)

  ➷ -Xmx:最大堆空間內存。(默認爲物理內存的1/4)

  ➷ -Xmn:設置新生代的大小。(初始值及最大值)

  ➷ -XX:NewRatio:配置新生代與老年代在堆結構的佔比。

  ➷ -XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例。

  ➷ -XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡。

  ➷ -XX:+PrintGCDetails:輸出詳細的GC處理日誌。

  ➷ 打印GC簡要信息:

    ① -XX:+PrintGC

    ② -verbose:gc

  ➷ -XX:HandlePromotionFalilure:是否設置空間分配擔保。

分配擔保

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

  ★ 若是大於,則這次Minor GC是安全的。

  ★ 若是小於,則虛擬機會查看-XX:HandlePromotionFailure設置值是否允擔保失敗。

    ☆ 若是HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小。

      若是大於,則嘗試進行一次Minor GC,但此次Minor GC依然是有風險的。

      若是小於,則改成進行一次Full GC。

    ☆ 若是HandlePromotionFailure=false,則改成進行一次Full GC。

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


堆是分配對象的惟一選擇麼?

逃逸分析

在《深刻理解Java虛擬機》中關於Java堆內存有這樣一段描述:

  隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化,全部的對象都分配到堆上也漸漸變得不那麼「絕對」了。

  在Java虛擬機中,對象是在Java堆中分配內存的,這是一個廣泛的常識。可是,有一種特殊狀況,那就是若是通過逃逸分析(Escape Analysis)後發現,一個對象並無逃逸出方法的話,那麼就可能被優化成棧上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最多見的堆外存儲技術

  此外,基於openJDk深度定製的TaoBaoVM,其中創新的GCIH(GC invisible heap)技術實現off-heap,將生命週期較長的Java對象從heap中移至heap外,而且GC不能管理GCIH內部的Java對象,以此達到下降GC的回收頻率和提高GC的回收效率的目的。

  如何將堆上的對象分配到棧呢?

  須要使用逃逸分析手段。這是一種能夠有效減小Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。經過逃逸分析,Java Hotspot編譯器可以分析出一個新的對象的引用的使用範圍從而決定是否要將這個對象分配到堆上。

逃逸分析的基本行爲就是分析對象動態做用域:

  • 當一個對象在方法中被定義後,對象只在方法內部使用,則認爲沒有發生逃逸。

  • 當一個對象在方法中被定義後,它被外部方法所引用,則認爲發生逃逸。例如:做爲調用參數傳遞到其餘地方中。

逃逸分析舉例

  沒有發生逃逸的對象,則能夠分配到棧上,隨着方法執行的結束,分配的棧空間隨棧幀一塊兒就被移除了。就像這樣的:

public void my_method() { V v = new V(); // use v // ....
    v = null; }

  像下面這樣的就發生逃逸了,所建立的對象傳到外面被引用了。

public static StringBuffer createStringBuffer(String s1, String s2) { StringBuffer nb = new StringBuffer(); nb.append(s1); nb.append(s2); return nb; }

  若是想要不發生逃逸,能夠這樣寫:

public static String createStringBuffer(String s1, String s2) { StringBuffer nb = new StringBuffer(); nb.append(s1); nb.append(s2); return nb.toString(); }

  傳到外面的並非new出來的對象,而是生成的字符串常量。

如何快速的判斷是否發生了逃逸分析?

  就看new的對象是否在方法外被調用。

參數設置

  在JDK 1.7 版本以後,HotSpot中默認就已經開啓了逃逸分析

若是使用的是較早的版本,開發人員則能夠經過:

  選項「-xx:+DoEscapeAnalysis":顯式開啓逃逸分析。

  選項「-xx:+PrintEscapeAnalysis":查看逃逸分析的篩選結果。

結論

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

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

  ➷ 棧上分配:將堆分配轉化爲棧分配。若是一個對象在子程序中被分配,要使指向該對象的指針永遠不會發生逃逸,對象多是棧上分配的候選,而不是堆上分配

  ➷ 同步省略:若是一個對象被發現只有一個線程被訪問到,那麼對於這個對象的操做能夠不考慮同步。

  ➷ 分離對象或標量替換:有的對象可能不須要做爲一個連續的內存結構存在也能夠被訪問到,那麼對象的部分(或所有)能夠不存儲在內存,而是存儲在CPU寄存器中。

棧上分配

  JIT編譯器在編譯期間根據逃逸分析的結果,發現若是一個對象並無逃逸出方法的話,就可能被優化成棧上分配。分配完成後,繼續在調用棧內執行,最後線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收了。

常見的棧上分配的場景:

  給成員變量賦值

  方法返回值

  實例引用傳遞

舉例

  咱們經過舉例來講明 開啓逃逸分析 和 未開啓逃逸分析時候的狀況。

class User { private String name; private String age; private String gender; private String phone; } public class StackAllocation { public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { alloc(); } long end = System.currentTimeMillis(); System.out.println("花費的時間爲:" + (end - start) + " ms"); // 爲了方便查看堆內存中對象個數,線程sleep
        Thread.sleep(10000000); } private static void alloc() { User user = new User(); } }

設置JVM參數,表示未開啓逃逸分析:

  -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails

運行結果,同時還觸發了GC操做

  花費的時間爲:664 ms

而後查看內存的狀況,發現有大量的User存儲在堆中

咱們在開啓逃逸分析

  -Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails

而後查看運行時間,咱們可以發現花費的時間快速減小,同時不會發生GC操做

  花費的時間爲:5 ms

而後在看內存狀況,咱們發現只有不多的User對象,說明User發生了逃逸,由於他們存儲在棧中,隨着棧的銷燬而消失

同步省略

  線程同步的代價是至關高的,同步的後果是下降併發性和性能。

  在動態編譯同步塊的時候,JIT編譯器能夠藉助逃逸分析來判斷同步塊所使用的鎖對象是否只可以被一個線程訪問而沒有被髮布到其餘線程。若是沒有,那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提升併發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除。

例以下面的代碼:

public void f() { Object hellis = new Object(); synchronized(hellis) { System.out.println(hellis); } }

  代碼中對hellis這個對象加鎖,可是hellis對象的生命週期只在f()方法中,並不會被其餘線程所訪問到,因此在JIT編譯階段就會被優化掉,優化成:

public void f() { Object hellis = new Object(); System.out.println(hellis); }

分離對象和標量替換

  標量(scalar)是指一個沒法再分解成更小的數據的數據。Java中的原始數據類型就是標量。

  相對的,那些還能夠分解的數據叫作聚合量(Aggregate),Java中的對象就是聚合量,由於他能夠分解成其餘聚合量和標量。

  在JIT階段,若是通過逃逸分析,發現一個對象不會被外界訪問的話,那麼通過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替,這個過程就是標量替換。

public static void main(String args[]) { alloc(); } class Point { private int x; private int y; } private static void alloc() { Point point = new Point(1,2); System.out.println("point.x" + point.x + ";point.y" + point.y); }

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

private static void alloc() { int x = 1; int y = 2; System.out.println("point.x = " + x + "; point.y=" + y); }

  能夠看到,Point這個聚合量通過逃逸分析後,發現他並無逃逸,就被替換成兩個聚合量了。那麼標量替換有什麼好處呢?

  這樣能夠大大減小堆內存的佔用。由於一旦不須要建立對象了,那麼就再也不須要分配堆內存了。 標量替換爲棧上分配提供了很好的基礎。

分析與總結

  關於逃逸分析的論文在1999年就已經發表了,但直到JDK1.6纔有實現,並且這項技術到現在也並非十分紅熟的。

  其根本緣由就是沒法保證逃逸分析的性能消耗必定能高於它的消耗。雖然通過逃逸分析能夠作標量替換、棧上分配、和鎖消除。可是逃逸分析自身也是須要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。一個極端的例子,就是通過逃逸分析以後,發現沒有一個對象是不逃逸的,那這個逃逸分析的過程就白白浪費掉了。

  雖然這項技術並不十分紅熟,可是它也是即時編譯器優化技術中一個十分重要的手段。注意到有一些觀點,認爲經過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,可是取決於JVM設計者的選擇。就目前來講,oracle Hotspot JVM中並未這麼作,這一點在逃逸分析相關的文檔裏已經說明,因此能夠明確全部的對象實例都是建立在堆上。

  目前不少書籍仍是基於JDK7之前的版本,JDK已經發生了很大變化,intern字符串的緩存和靜態變量曾經都被分配在永久代上,而永久代已經被元數據區取代。可是,intern字符串緩存和靜態變量並非被轉移到元數據區,而是直接在堆上分配,因此這一點一樣符合前面一點的結論:對象實例都是分配在堆上

 


小結

  年輕代是對象的誕生、成長、消亡的區域,一個對象在這裏產生、應用,最後被垃圾回收器收集、結束生命。

  老年代放置長生命週期的對象,一般都是從survivor區域篩選拷貝過來的Java對象。固然,也有特殊狀況,咱們知道普通的對象會被分配在TLAB上;若是對象較大,JVM會試圖直接分配在Eden其餘位置上;若是對象太大,徹底沒法在新生代找到足夠長的連續空閒空間,JVM就會直接分配到老年代。

  當GC只發生在年輕代中,回收年輕代對象的行爲被稱爲Minor Gc。

  當GC發生在老年代時則被稱爲Major Gc或者FullGC。通常的,Minor GC的發生頻率要比MajorGC高不少,即老年代中垃圾回收發生的頻率將大大低於年輕代。

相關文章
相關標籤/搜索