JVM詳解(八)堆空間

在這裏感謝尚硅谷JVM(宋紅康),在此記錄一下自己詳細對學習筆記,希望對你有所幫助。

視頻地址
代碼地址

JVM學習路線和內容回顧

image-20200523203003162

image-20200523204000784

LV:本地變量表

OS:操作數棧

DL:動態鏈接

RA:返回地址

堆空間的概述——進程中的唯一性

image-20200523204316746

一個進程對應一個jvm實例,一個運行時數據區。一個進程中的多個線程共享同一個方法區、堆空間,各自擁有程序計數器、本地方法棧、虛擬機棧

●一個JVM實例只存在一個堆內存,堆也是Java內存管 理的核心區域。
●Java 堆區在JVM啓動的時候即被創建,其空間大小也就確定了。是JVM管理的最大一塊內存空間。
➢堆內存的大小是可以調節的。
●《Java虛擬機規範》規定,堆可以處於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的。(涉及到物理內存和虛擬內存)
●所有的線程共享Java堆,在這裏還可以劃分線程私有的緩衝區(ThreadLocal Allocation Buffer, TLAB) 。

1.準備:設置不同的堆空間大小

image-20200523210916854

image-20200523210939487

image-20200523210958825

2.分別跑起來,接下來我們來看一下這兩個進程

之後在jdk1.8/contents/homt/bin目錄下找到jvisualvm

image-20200523211323483

image-20200523211426660

若沒有visual gc,則點擊菜單中工具裏的插件,此時就有了

image-20200523212238752

堆空間關於對象創建和GC的概述

●《Java虛擬機規範》中對Java堆的描述是:所有的對象實例以及數組都應當在運行時分配在堆上。| (The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated )
➢我要說的是:“幾乎”所有的對象實例都在這裏分配內存。從實際使用角度看的。
●數組和對象可能永遠不會存儲在棧上,因爲棧幀中保存引用,這個引用指向對象或者數組在堆中的位置。
●在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候纔會被移除。
●堆,是GC ( Garbage Collection, 垃圾收集器)執行垃圾回收的重點區域。

image-20200523213852303

對應這樣的場景

image-20200523213906307

main方法結束後,s1,s2就被彈出棧,堆中的s1,s2實例就沒有被引用了,當GC進行判斷的時候,發現s1,s2沒有被引用,就判斷爲垃圾.如果變爲棧中的一走,堆中的對象就被回收,那麼GC的頻率將特別高

來看一下對象的創建:

image-20200523214532877

堆的戲份內容結構

image-20200523214652258

image-20200523214917451

image-20200523215640280

image-20200523215613959

加起來剛好10m,邏輯上是3部分,但是實際管轄的是這兩部分:新生區、老年區

image-20200523215656382

image-20200523220204078

image-20200523220258808

堆空間大小的設置和查看

●Java堆區用於存儲Java對象實例,那麼堆的大小在JVM啓動時就已經設定好了,大家可以通過選項"-XmX"和"-Xms"來進行設置。
➢“-Xms"用於表示堆區的起始內存,等價於-XX: Ini tialHeapSize
➢“-Xmx"則用於表示堆區的最大內存,等價於-XX : MaxHeapSize
●一旦堆區中的內存大小超過“-Xmx"所指定的最大內存時,將會拋出OutOfMemoryError異常。
通常會將-Xms 和-Xmx兩個參數配置相同的值,其目的是爲了能夠在java垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,從而提高性能。
●默認情況下,初始內存大小:物理電腦內存大小/ 64
最大內存大小:物理電腦內存大小/ 4

image-20200524094400141

image-20200524094557146

這裏出現一個問題爲初始與最大是一樣的值

開發者建議將出事堆內存和最大堆內存設置成相同堆值

因爲在不斷擴容和釋放的過程中會對系統造成額外壓力

image-20200524094936162

那麼這個數怎麼算出來的呢?我們先添加這一行,並打開命令行輸入jps(查看當前程序進程),jstat查看某進程在使用過程中內存使用情況

image-20200524095019676

image-20200524095242021

OC:總量 OU:你使用了多少

image-20200524105828009

image-20200524095859867

但是爲什麼運行時是575m呢

image-20200524105754582

3.另一種方式查看,並把Thread.sleep去掉

image-20200524100434564

image-20200524103121521

OOM的說明與舉例

並設置參數-Xms600m -Xmx600m

image-20200524105038912

image-20200524105133836

打開jvisualvm,再跑起來

image-20200524105213428

image-20200524105239794

查看抽樣器,可查看原因,原因爲byte[]太多

image-20200524105351397

新生代與老年代中相關參數的設置

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

image-20200524105658826

下面這參數開發中一般不會調:
image-20200524110122166

● 配置新生代與老年代在堆結構的佔比。
➢默認-XX:NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3
➢可以修改-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5

1.方式一

image-20200524110354979

image-20200524110501737

默認比例1:2

如果某些對象生命週期長的較多,則可以考慮把老年代調得大一些

2.方式二

通過jinfo -flag

image-20200524110937645

  • 在HotSpot中,Eden 空間和另外兩個Survivor空間缺省所佔的比例是8:1:1
  • 當然開發人員可以通過選項“-XX:SurvivorRatio”調整這個空間比例。比如-XX: SurvivorRatio=8
  • 幾乎所有的Java對象都是在Eden區被new出來的。
  • 絕大部分的Java對象的銷燬都在新生代進行了。
    ➢IBM公司的專門研究表明,新生代中80%的對象都是“朝生夕死”的。
  • 可以使用選項"-Xmn"設置新生代最大內存大小
    ➢這個參數一般使用默認值就可以了。

那麼是8:1:1嗎

那麼理論新生代是200m,則eden爲160m,打開jvisualvm看一下

image-20200524111531405

我們發現變成6:1:1了,我們從命令行看一下

image-20200524111713713

還是6:1:1

這是因爲其中還存在一個自適應的機制,默認想關閉可使用:這個’-’

-XX:-UseAdaptiveSizePolicy:關閉自適應的內存分配策略(暫時用不到)

image-20200524111949573

image-20200524112115834

但發現還是6:1:1

這時候就要用到-XX:SurvivorRatio

image-20200524112215387

image-20200524112228284

這樣就成功了

如果new一個對象非常大,Eden區放不下了,這個時候就放入老年代

image-20200524112607343

image-20200524112851080

圖解對象分配的一般過程

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

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

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

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

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

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

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

 -XX:MaxTenuringThreshold=<N>

進行設置。

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

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

image-20200524143438116

Eden區放着放滿了(此時用戶線程停止,觸發GC【判斷哪些是垃圾,紅色的是垃圾,然後釋放】)就往S0或S1【倖存者區】放。可以看到是一,我們爲每個對象分配了一個年齡計數器。此時Eden區裏面就沒有數據了,被清空了。

此時又放:

image-20200524143922880

伊甸園區滿了,又進行一次Minor GC,是放在S0還是S1呢?是放在S1當中。進入1之後,S1爲空的,就稱爲to【表示空】。當放入進S1,即這個1標註1號的柱子,此時S0中的1號柱子也要進行判斷,當發現這倆還被佔用,還不能被銷燬,就把S0區的放入S1區並增長2。此時S1就是from區,S0空了就是to區,以此類推。

image-20200524143454989

走着走着就進入特殊情況:
Eden又滿了,就繼續放入S0,S1進行判斷,1號還用就放入S0。我們發現S1中有兩個已經達到15了,就晉升至老年代,放入Old,此時就不進行判斷它了。只有當有柱子再進入老年代的時候才進行判斷。15稱爲閾值(臨界值,默認的)

Eden區滿出發YGC,S區滿了不會觸發。Eden觸發GC的時候會將Eden與S區(屬於被動)一起進行垃圾回收。

當S區滿了,沒有達到15的情況下,有可能進入Old,也有可能從Eden直接到達Old

總結

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

對象分配的特殊情況

image-20200524145910809

EdenGC後此時Eden是空的,假如Eden是10m,要放一個12m的對象,可直接放入老年代,則在老年代中分配內存空間。

如果老年代放不下,eg:老年代有20m,但是使用了10m,還剩10m放不下12m,此時進行FGC/major GC。若回收以後,還是不夠放12m(或是老年代本身就是10m不夠),此時就是OOM【如果不讓他自動調整內存空間的話】。

若從Eden過來的放到S區發現放不下,則直接進入老年代。

代碼舉例與JVisualVm演示對象分配過程

我們來模擬這個過程:
1.

image-20200524153209187

image-20200524153217281

進入YGC,放入S1

image-20200524153234816

第二次YGC,從S1放入S0,S1變to區

image-20200524153251994

繼續上面的步驟

image-20200524153505398

最終Old滿了,拋出OOM,因爲我們始終放在ArrayList中,沒有垃圾

可以看見在Eden峯底底時候執行了一次GC。執行完一次gc後,Eden區再不斷地去增長

MetaSpace就一直處於類數據的加載,比較平穩,沒有去加載一些額外的類。

常用優化工具概述與Jprofiler的演示

常用的調優工具

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

image-20200524170539641

Minor Gc、Major GC和Full GC的對比

JVM在進行GC時,並非每次都對上面三個內存區域(新生代、老年代;方法區)一起回收的,大部分時候回收的都是指新生代(80%)。
針對HotSpot VM的實現,它裏面的GC按照回收區域又分爲兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
部分收集:不是完整收集整個Java堆的垃圾收集。其中又分爲:
➢新生代收集(Minor GC / Young GC) :只是新生代(Eden/S0,S1)的垃圾收集
➢老年代收集(Major GC / Old GC) :只是老年代的垃圾收集。
1.目前,只有CMS GC會有單獨收集老年代的行爲。
2.注意,很多時候Major GC會和Full GC混淆使用,需要具體分辨是老年代
回收還是整堆回收
➢混合收集(Mixed GC): 收集整個新生代以及部分老年代的垃圾收集。
1.目前,只有G1 GC會有這種行爲
整堆收集(Ful1 GC):收集整個java堆和方法區的垃圾收集。

我們實際上希望出現GC情況少一些。GC也是垃圾回收的線程來做的,對應的另一個線程是用戶線程(我們真正要執行代碼所用到的線程)。GC的線程在判斷哪些是垃圾的時候,會讓用戶線程做一個暫停,用戶線程一暫停,程序執行的一個吞吐量就會差一些。減少GC就減少了這個STW的頻率,用戶就會被較少的干預到。重點關注Major GC和Full GC,因爲他們兩個所產生的暫停時間比Minor GC長

最簡單的分代GC策略的觸發條件:

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

image-20200527123619636

老年代GC (Major GC/Fu1l GC)觸發機制:
➢指發生在老年代的GC,對象從老年代消失時,我們說“Major GC”或“Full GC”
發生了。
➢出現了Major GC,經常會伴隨至少一次的Minor GC (但非絕對的,Parallel
Scavenge收集器的收集策略裏就有直接)。
1.也就是在老年代空間不足時,會先嚐試進行Major GC的策略選擇過程觸發 Minor GC。如果之後空間還不足,則觸發Major GC
➢Major GC的速度一般會比Minor GC慢10倍以上,STW的時間更長。
➢如果Major GC後,內存還不足,就報OOM了。

●Ful l 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是開發或調優中儘量要避免的。這樣暫時時間會短一些。

GC舉例與日誌分析

image-20200527154345824

image-20200527154358889

字符串常量池存在堆空間的,(以前在方法區)

不斷添加,導致了OOM

image-20200527154517442

GC年輕代,FullGC(老年代+整個堆區、方法區)

出現OOM一定會經歷Full GC,

image-20200527155034072

image-20200527155904978

image-20200527160910583

堆空間分代思想

爲什麼需要把Java堆分代?不分代就不能正常工作了嗎?
●經研究,不同對象的生命週期不同。70%-99%的對象是臨時對象。
➢新生代:有Eden、兩塊大小相同的Survivor(又稱爲from/to, s0/s1)構成,to總爲空。
➢老年代:存放新生代中經歷多次GC仍然存活的對象。

image-20200527161159646

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

image-20200527161311571

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

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

針對不同年齡段的對象分配原則如下所示:
●優先分配到Eden
●大對象直接分配到老年代
➢儘量避免程序中出現過多的大對象
●長期存活的對象分配到老年代
●動態對象年齡判斷
➢如果Survivor 區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
●空間分配擔保
➢-XX: HandlePromotionFailure

image-20200527164220614

爲對象分配內存:TLAB

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

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

image-20200527164854508

每個線程有一份,使用完了再用公共的

TLAB的再說明:
●儘管不是所有的對象實例都能夠在TLAB中成功分配內存,但JVM確實是TLAB作爲內存分配的首選。

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

image-20200527170603051

默認是開啓的

image-20200527170706159

小結堆空間的參數設置

官網說明

  • -XX:+PrintFlagsInitial :查看所有的參數的默認初始值

  • -XX: +PrintFlagsFinal : 查看所有的參數的最終值(可能會存在修改,
    不再是初始值)

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

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

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

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

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

  • -XX: MaxTenuri ngThreshold:設置新生代垃圾的最大年齡

  • -XX: +PrintGCDetails:輸出詳細的GC處理日誌
    ➢打印gc簡要信息: ①-XX: +PrintGC. ②-verbose:gc

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

/**
 * @author Aaron
 * @description-
 * 測試堆空間常用的JVM參數:
 * -XX:+PrintFlagsInitial :查看所有的參數的默認初始值
 * -XX:+PrintFlagsFinal : 查看所有的參數的最終值(可能會存在修改,不再是初始值)
 *              具體查看某個參數的指令:jps:查看當前運行中的進程
 *                                  jinfo -flag SurvivorRatio 進程id
 * -Xms: 初始堆空間內存 (默認爲物理內存的1/64)
 * -Xmx: 最大堆空間內存(默認爲物理內存的1/4)
 * -Xmn: 設置新生代的大小。(初始值及最大值)
 * -XX:NewRatio: 配置新生代與老年代在堆結構的佔比
 * -XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例
 * -XX:MaxTenuri ngThreshold:設置新生代垃圾的最大年齡
 * -XX:+PrintGCDetails:輸出詳細的GC處理日誌
 *   ➢打印gc簡要信息:  ①-XX: +PrintGC.    ②-verbose:gc
 * -XX:HandlePromotionFalilure:是否設置空間分配擔保
 *   配置新生代與老年代在堆結構的佔比
 * @date 2020/5/27 5:13 PM
 */
public class HeapArgsTest {
    public static void main(String[] args) {

    }
}

測試程序


1.PrintFlagInital

image-20200527212718272

image-20200527212802046

2.PrintFlagFinal

image-20200527213044211

image-20200527213134281

":="代表重新做的賦值

3.注意若SurvivorRatio設置Eden區過大,導致S區過小的話,理想狀態下GC過程中,Eden都被回收了,沒進入S區。不過不是理想情況下就會被放到Old區,導致minor GC失去意義。

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

堆是分配對象存儲的唯一選擇嗎?

在《深入理解Java虛擬機》中關於Java堆內存有這樣一段描述:
隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那麼“對”了。

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

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

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

image-20200527221831042

new 的這個V對象沒有發生逃逸,就放在棧上

image-20200527222004329

綠色方框中的代碼因爲StringBuffer()被返回了,跳出了該方法作用於,產生了逃逸,不能在棧中分配

參數設置:
●在JDK 6u23版本之後,HotSpot中默認就已經開啓了逃逸分析。
如果使用的是較早的版本,開發人員則可以通過:
➢選項“-XX: +DoEscapeAnalysis" 顯式開啓逃逸分析
➢通過選項“-XX: +PrintEscapeAnalysis"查看逃逸分析的篩選結果

結論

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

代碼優化之棧上分配

使用逃逸分析,編譯器可以對代碼做如下優化:
**一、棧上分配。**將堆分配轉化爲棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
**二、同步省略。**如果一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操作可以不考慮同步。
**三、分離對象或標量替換。**有的對象可能不需要作爲一-個連續的內存結構存在也可以被訪問到,那麼對象的部分(或全部)可以不存儲在內存,而是存儲在CPU寄存器中。

一、棧上分配

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

沒有開啓逃逸分析,則所有對象都是在堆中去分配空間

image-20200528131508123

image-20200528131519506

打開jvisualvm

image-20200528132354514

image-20200528132027310

現在將’-‘改爲’+’

image-20200528132057352

image-20200528132331881

執行速度更快一些

2.將堆空間大小變小,此時會進行GC。我們再在沒有開啓逃逸分析的情況下:

image-20200528132832045

2.1

image-20200528132807467

執行了GC

2.2 現開啓逃逸分析

image-20200528132857990

image-20200528132921701

發現不僅時間變短了,也根本沒有發生GC。因爲棧中沒有GC

二、同步省略

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

image-20200528133246945

image-20200528134535753

字節碼中仍然有,但是運行的時候會去掉

三、分離對象或標量替換

image-20200528134718963

image-20200528134905654
標量替換參數設置:
參數-XX: +EliminateAllocations:開啓了標量替換(默認打開),允許將對象打散分配在棧上。

1.先關閉

image-20200528135425822

image-20200528135744469

image-20200528135554838

28ms,進行了一些GC

2.開啓後

image-20200528135707976

明顯降低,也沒有GC

代碼優化及堆堆小結

image-20200528140017034

image-20200528140117076

  • 關於逃逸分析的論文在1999年就已經發表了,但直到JDK 1.6纔有實現,而且這項技術到如今也並不是十分成熟的。
  • 其根本原因就是無法保證逃逸分析的性能消耗一定能高於他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。
  • 一個極端的例子,就是經過逃逸分析之後,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
  • 雖然這項技術並不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段。
  • 注意到有一些觀點,認爲通過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,但是取決於JVM設計者的選擇。據所知,Oracle Hotspot JVM中並未這麼做,這一點在逃逸分析相關的文檔裏已經說明,所以可以明確所有的對象實例都是創建在堆上。
  • 目前很多書籍還是基於JDK 7以前的版本,JDK已經發生了很大變化,intern字符串的緩存和靜態變量曾經都被分配在永久代上,而永久代已經被元數據區取代。但是,intern字符串緩存和靜態變量並不是被轉移到元數據區,而是直接在堆上分配,所以這一點同樣符合前面一點的結論:對象實例都是分配在堆上。

小結

  • 年輕代是對象的誕生、成長、消亡的區域,一個對象在這裏產生、應用,
    最後被垃圾回收器收集、結束生命。
  • 老年代放置長生命週期的對象,通常都是從Survivor區域篩選拷貝過來的
    Java對象。當然,也有特殊情況,我們知道普通的對象會被分配在TLAB上;
    如果對象較大,JVM會 試圖直接分配在Eden其他位置上;如果對象太大,完全無法在新生代找到足夠長的連續空閒空間,JVM就會直接分配到老年代。
  • 當GC只發生在年輕代中,回收年輕代對象的行爲被稱爲MinorGC。當GC
    發生在老年代時則被稱爲MajorGC或者FullGC。 一般的,MinorGC的發生頻率要比MajorGC高很多,即老年代中垃圾回收發生的頻率將大大低於年輕代。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章