金藏源電商網(wǎng)站建設怎樣免費制作網(wǎng)頁
JVM系列整體欄目
內(nèi)容 | 鏈接地址 |
---|---|
【一】初識虛擬機與java虛擬機 | https://blog.csdn.net/zhenghuishengq/article/details/129544460 |
【二】jvm的類加載子系統(tǒng)以及jclasslib的基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/129610963 |
【三】運行時私有區(qū)域之虛擬機棧、程序計數(shù)器、本地方法棧 | https://blog.csdn.net/zhenghuishengq/article/details/129684076 |
【四】運行時數(shù)據(jù)區(qū)共享區(qū)域之堆、逃逸分析 | https://blog.csdn.net/zhenghuishengq/article/details/129796509 |
精通運行時數(shù)據(jù)區(qū)共享區(qū)域---堆
- 一,深入理解運行時數(shù)據(jù)區(qū)堆
- 1,堆空間概念
- 2,堆內(nèi)存空間
- 2.1,堆內(nèi)存的細分
- 2.2,堆大小的基本設置
- 2.3,堆大小的計算規(guī)則
- 3,堆對象
- 3.1,堆空間對象組成
- 3.2,堆對象分配的一般過程
- 3.3,堆對象分配的特殊過程
- 3.4,代碼示例
- 4,初識GC
- 4.1,初識Minor GC,Major GC,Full GC
- 4.1.1,新生代GC的觸發(fā)條件
- 4.1.2,老年代GC的觸發(fā)條件
- 4.1.3,Full GC的觸發(fā)機制
- 5,堆內(nèi)存分配
- 5.1,基本的內(nèi)存分配策略
- 5.2,TLAB
- 5.2.1,為什么要有TLAB
- 5.2.2,什么是TLAB
- 6,堆空間參數(shù)設置
- 7,逃逸分析(重點)
- 7.1,棧上分配
- 7.2,同步省略
- 7.3,標量替換
- 8,堆空間是分配對象的唯一選擇嗎(重點)
一,深入理解運行時數(shù)據(jù)區(qū)堆
1,堆空間概念
heap
堆屬于是運行時數(shù)據(jù)區(qū)的一塊空間,屬于一塊比較重要的一部分,并且該區(qū)域的數(shù)據(jù)屬于線程共享。
? 一個JVM實例只存在一個堆,堆也是java內(nèi)存管理的核心區(qū)域
? Java堆在啟動的時候被創(chuàng)建,其空間大小也被確定,并且是JVM管理的最大的一塊內(nèi)存空間(可調(diào)節(jié))
? 堆可以處于物理上不連續(xù)的內(nèi)存空間中,但是在邏輯上他應該是被視為連續(xù)的
? 所有的線程共享JVM堆,在這里還可以劃分為私有的緩沖區(qū)(Thread Local Allocation Buffer)
? 在java虛擬機規(guī)范中,所有的對象實例以及數(shù)組都應該分配在堆上
? 數(shù)組和對象可能永遠不會存儲在棧上,因為棧幀中保存引用,這個引用指向?qū)ο蠡蛘邤?shù)組在堆中的位置
? 在方法結(jié)束之后,堆中的對象不會被立馬移除,僅僅在垃圾收集的時候才會被移除
? 堆是GC(Garbage Collection,垃圾回收器)執(zhí)行垃圾回收的重點區(qū)域
2,堆內(nèi)存空間
2.1,堆內(nèi)存的細分
在JDK8之前,堆內(nèi)存的邏輯主要分為:新生代+老年代+永久區(qū)
在JDK8以及JDK8之后,堆內(nèi)存主要分為:新生代+老年代+永久區(qū)
jdk8的垃圾回收器如下,可以直接在jdk安裝目錄下,找到bin目錄,然后打開這個 jvisaulvm.exe 文件,然后工具的在插件中安裝一個 Visaul GC,就可以看到下面的界面,可知在jdk8中主要是分為Eden區(qū),old區(qū)和Metaspace這三個區(qū)域,分別對應著新生代,老年代和元空間
也可以直接新建一個application運行并設置其對應jvm的參數(shù),如設置初始大小和最大大小為10m,并在控制臺上將這個堆信息打印出來
-Xms10m -Xmx10m -XX:+PrintGCDetails
在運行項目后,可以發(fā)現(xiàn),PSYoungGen新生代,ParOldGen老年代,Metaspace元空間,其老年代所分配的內(nèi)存最大。
hello
Heap
PSYoungGen total 2560 K, used 1086 K[0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048 K, 28 % used[0x00000000ffd00000, 0x00000000ffd91b68, 0x00000000fff00000)
from space 512 K, 98 % used[0x00000000fff00000, 0x00000000fff7e030, 0x00000000fff80000)
to space 512 K, 0 % used[0x00000000fff80000, 0x00000000fff80000, 0x0000000100000000)
ParOldGen total 7168 K, used 371 K[0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168 K, 5 % used[0x00000000ff600000, 0x00000000ff65cf38, 0x00000000ffd00000)
Metaspace used 3520 K, capacity 4498 K, committed 4864 K, reserved 1056768 K
class space used 388 K, capacity 390 K, committed 512 K, reserved 1048576 K
2.2,堆大小的基本設置
Java堆區(qū)主要用于存儲Java對象的實例,那么堆的大小在JVM啟動的時候就已經(jīng)設定好了,可以直接通過 -Xmx和 -Xms來設定堆的初始內(nèi)存和最大內(nèi)存,當堆的最大內(nèi)存超過設置的 -Xms 最大內(nèi)存時,則會直接拋出內(nèi)存溢出異常。
-Xms : 設置堆空間(新生代+老年代)的初始內(nèi)存大小 -X表示的是Jvm的運行參數(shù)? ms表示的是memory start-Xmx:設置堆空間(新生代+老年代)的最大內(nèi)存大小
并且在默認的堆空間中,其最大內(nèi)存為物理電腦內(nèi)存 / 4,初始內(nèi)存為物理電腦內(nèi)存 / 64
在實際開發(fā)中,更加建議將初始堆內(nèi)存和最大的堆內(nèi)存設置成相同的值,主要是省去堆空間頻繁的擴容和釋放帶來的壓力,從而增加這種資源的利用率。
2.3,堆大小的計算規(guī)則
堆大小主要分為新生代區(qū)和老年代區(qū),新生代又分為eden區(qū)survive存活區(qū),survive區(qū)有分為survive0和survive1區(qū),對象只能存在期中的一個區(qū)域里面,每Minitor GC一次,就會從一個survive區(qū)到另外一個survive區(qū),如果沒有被清理掉,那么他的年齡就會+1,當年齡達到15時,就會從新生代中加入到老年代里面。
接下來在虛擬機中設置一個600m的堆大小,依舊是用剛剛那個程序
-Xms600m -Xmx600m
然后在main方法中運行之后,輸入以下命令
jps : 查看進程號
jstat -gc 進程號
可以發(fā)現(xiàn)新生代有S0和S1的survive區(qū)和Eden區(qū),老年代有old區(qū)。對應的容量分別是最大容量和已使用容量,如S0C表示的是survive0區(qū)的最大容量,S0U則表示的是survice0已使用的容量。后面也有對應的YGC和FGC發(fā)生的次數(shù)。
并且在計算總?cè)萘康臅r候,因為數(shù)據(jù)只能存在一個survive中,因此只計算一個survice的內(nèi)存大小,所以有時計算出來的內(nèi)容是小于實際設置的內(nèi)容,但結(jié)果是對的,只是jvm在統(tǒng)計時只統(tǒng)計了一個survice存活區(qū)的大小。
3,堆對象
3.1,堆空間對象組成
在JVM中,java對象可以分為兩類
一類是生命周期較短的瞬時對象,這類對象的創(chuàng)建和消亡都比較快
另一類周期長,某些極端情況下可以和JVM進程的生命周期保持一致
java堆細分可以分為新生代(YoungGen)和老年代(OldGen),年輕代又可以細分為Eden區(qū)和Survivor0空間和Survivor1空間,有時也被稱為(from區(qū)和to區(qū))
在這些對象中,也會對新生代和老年代的空間大小設置對應的比例,新生代和老年代的比例默認為1:2
當然也可以通過命令進行修改這個默認的比例,一般不會修改這個默認值,除非是知道這個老年代的對象偏多
-XX:NewRatio=2 : 表示新生代占1,老年代占2,新生代占1/3,默認比例
-XX:NewRatio=4 : 表示新生代占1,老年代占4,新生代占1/5
也可以直接通過命令查看,通過jps查出進程號之后,通過Jinfo去查看
jps
jinfo -flag NewRatio process(進程號)
除了老年代和新生代的對空間有比例之外,這個新生代中的Eden區(qū)和Survicor區(qū)也有對應的比例。如果是在Linux系統(tǒng)下,其比例為8:1 ;如果是在Linux系統(tǒng)下,其比例為6:1。
-XX:SurvivorRatio=8 : 表示eden區(qū)占8,Survicor區(qū)占1
-XX:SurvivorRatio=6 : 表示eden區(qū)占6,Survicor區(qū)占1
-XX:SurvivorRatio=4 : 表示eden區(qū)占4,Survicor區(qū)占1
這個比例也可以直接通過命令查看,通過jps查出進程號之后,通過Jinfo去查看
jps
jinfo -flag SurvivorRatio process(進程號)
因此可以得到一下結(jié)論
🎱 幾乎所有的Java對象都是在Eden區(qū)被new出來
🎱 絕大多數(shù)對象的銷毀在新生代就進行了
3.2,堆對象分配的一般過程
為新對象分配內(nèi)存時一件非常嚴謹和復雜的任務,JVM的設計者們不僅需要考慮內(nèi)存如何分配,在哪里分配等問題,并且由于內(nèi)存分配算法與內(nèi)存回收算法密切相關(guān),所以還需要考慮GC執(zhí)行完內(nèi)存回收后是否會在內(nèi)存空間中產(chǎn)生內(nèi)存碎片等問題。
🎱 在new一個對象時,會先將對象放在eden區(qū),并且該去有大小限制
🎱 當eden區(qū)的數(shù)據(jù)量滿了,程序還需要創(chuàng)建對象的時候,JVM的Minor Gc就會對Eden區(qū)的垃圾進行回收,當Eden區(qū)不再被其他對象引用的對象就進行銷毀,被引用的對象就加載到Survivor0區(qū)中,然后此時eden區(qū)為空,再將新數(shù)據(jù)加載到eden區(qū)中
🎱 只有eden去滿才會觸發(fā)這個Minor GC,但是在觸發(fā)這個垃圾回收時,不僅會回收Eden區(qū)的對象,也會回收這個Survivor區(qū)的對象
🎱 如果再次觸發(fā)垃圾回收,就會將eden區(qū)中的未被引用的對象銷毀,并且此時會檢查survicor0區(qū)中的的數(shù)據(jù),如果不存在有對該對象的引用,那么該對象也會被銷毀,然后將eden區(qū)未被銷毀的和survicor0未被銷毀的一起加入到survicor1區(qū),后面這兩個區(qū)的對象往返移動
🎱 每將對象移動一次survicor區(qū),其age年齡就會加1,初始值為1,閾值為默認為15,可以修改,當年齡達到閾值時,其再gc一次到達16,就會將對象加入到老年代的空間區(qū)域中
🎱 當老年代中的空間滿了時,就會觸發(fā)這個Full GC
總結(jié)
- 針對幸存者s0和s1的總結(jié):復制之后有交換,誰不空誰是from,誰空誰是to
- 針對垃圾回收總結(jié):頻繁回收在新生區(qū),很少在老年區(qū)回收,幾乎不在永久區(qū)/元空間回收
3.3,堆對象分配的特殊過程
🌲 前兩個步驟還是一樣,在new一個對象時,會先將對象放在eden區(qū),并且該去有大小限制
🌲 當eden區(qū)的數(shù)據(jù)量滿了,程序還需要創(chuàng)建對象的時候,JVM的Minor Gc就會對Eden區(qū)的垃圾進行回收,當Eden區(qū)不再被其他對象引用的對象就進行銷毀,被引用的對象就加載到Survivor0區(qū)中,然后此時eden區(qū)為空,再將新數(shù)據(jù)加載到eden區(qū)中
🌲 這里開始就不一樣了,如果遇到的是一個大對象,可以在eden區(qū)存的下,但是在survivor區(qū)存活不下,那么直接將這個對象晉升為老年代
🌲 如果遇到的是一個超大對象,在eden區(qū)放不下,那么也會直接晉升為老年代,將這個超大對象存入到老年區(qū)中,如果老年代內(nèi)存不夠,那么就會觸發(fā)Full GC,如果還是不夠或者內(nèi)存大小直接小于這個超大對象的大小,那么就會直接觸發(fā)這個OOM,內(nèi)存溢出錯誤。
3.4,代碼示例
給對應的jvm堆配置的大小如下:-Xms600m -Xmx600m
,然后其對應的代碼如下,就是寫一個死循環(huán),一直去創(chuàng)建Student對象,list作為一個對象的引用,只要list存在,里面的對象就不會被回收掉。
public class Student {//創(chuàng)建bu數(shù)組byte bu[] = new byte[new Random().nextInt(1024*200)];public static void main(String[] args) {List<Student> list = new ArrayList<>();while (true){list.add(new Student());try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}
}
接下來繼續(xù)通過這個jvisualvm這個jdk自帶的工具來查看堆內(nèi)信息,一段時間之后,就發(fā)現(xiàn)出現(xiàn)了這個OOM。
如下圖所示,Old區(qū)占400內(nèi)存,年輕代占200內(nèi)存,所以可知這個老年代區(qū)域:新生代區(qū)域為2:1;eden區(qū)大小為150,survicor為25,我這個是在windows系統(tǒng)下,因此也符合6:1,如果是linux系統(tǒng),那就是8:1了。
而這個eden區(qū)到達峰值時,就表示eden區(qū)的數(shù)據(jù)滿了,那么就會觸發(fā)一次Minor GC,那么就會有數(shù)據(jù)存儲在survivor中,再到達一次峰值,又會觸發(fā)一次Minor GC,因此可以看到數(shù)據(jù)在Survivor的兩個區(qū)中移動,而由于對象一直被引用著,那么不管是Full GC還是Minor GC都是不能將數(shù)據(jù)給移除的,因此這個young區(qū)的數(shù)據(jù)不能被清除,數(shù)據(jù)就會都存在old區(qū)中,那么old區(qū)的數(shù)據(jù)就是成直線上升的,當old區(qū)滿了之后,那么就會直接觸發(fā)這個OOM了。
4,初識GC
4.1,初識Minor GC,Major GC,Full GC
JVM在進行GC的時候,并非每次都對上面三個內(nèi)存(新生代,老年代和方法區(qū))區(qū)域一起回收,大部分的時候指的還是新生代。
而在HotSpot Vm中,GC又分為兩種類型:部分收集和整堆收集
-
部分收集,不是完整的收集整個Java堆的垃圾收集,期中又分為:
新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
老年代收集(Major GC / Old GC):老年代的收集器,很多時候Major GC和Full GC會混合使用,區(qū)要區(qū)分是整堆回收還是老年代回收
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集
-
整堆收集(Full GC),收集整個java堆和方法區(qū)的垃圾收集
4.1.1,新生代GC的觸發(fā)條件
🐣 當新生代空間不足時,就會觸發(fā)Minor GC,這里的新生代指的是Eden區(qū),Survivor區(qū)是不會觸發(fā)GC的
🐣 Java對象大多具有朝生夕滅的特性,所以Minor GC也非常頻繁,回收速度也比較快
🐣 Minor GC會觸發(fā)STW,就是暫停其他的用戶線程,等垃圾回收結(jié)束,才會恢復運行
4.1.2,老年代GC的觸發(fā)條件
🐣 指發(fā)生在老年代的GC,對象從老年代消失,即 Major GC
或者Full GC
發(fā)生了
🐣 出現(xiàn)了Major GC之后,經(jīng)常會伴隨著至少一次的Minor GC
🐣 Major GC的速度一般會比Minor GC慢10倍,其STW的時間甚至要更長
🐣 如果Major GC 之后,內(nèi)存還是不足,就會直接報OOM了
4.1.3,Full GC的觸發(fā)機制
🐣 調(diào)用System.gc()時,系統(tǒng)建議執(zhí)行Full GC時,但是不必然執(zhí)行
🐣 老年代或者方法區(qū)空間不足
🐣 Minor GC進入老年代的平均大小大于老年代的可用內(nèi)存
🐣 直接加入到老年代的對象的大小大于老年代可用內(nèi)存的大小
5,堆內(nèi)存分配
5.1,基本的內(nèi)存分配策略
🐋 將對象優(yōu)先分配到Eden區(qū)
🐋 大對象直接分配到老年代,盡量避免程序中出現(xiàn)過多的大對象,如大樹組,字符串等
🐋 長期存活的對象分配到老年代
🐋 動態(tài)對象年齡判斷
🐋 空間分配擔保
5.2,TLAB
Thread Local Allocation Buffer,即線程本地分配的一個緩沖區(qū)。
5.2.1,為什么要有TLAB
- 堆區(qū)是線程的共享區(qū)域,任何線程都可以訪問到堆區(qū)的共享數(shù)據(jù),好處是進程間的通信比較方便
- 由于對象的實例創(chuàng)建在JVM中非常頻繁,因此在并發(fā)環(huán)境下,堆區(qū)劃分的內(nèi)存空間是線程不安全的
- 為避免多個線程操作同一個地址,需要使用加鎖等機制,但是也會一定的效率
5.2.2,什么是TLAB
-
從內(nèi)存模型角度來看,對Eden區(qū)域繼續(xù)進行劃分,JVM為每個線程分配了一個私有的緩沖區(qū),存在Eden空間
-
多線程在分配內(nèi)存時,使用TLAB可以避免一系列的線程安全問題,并提升一定的吞吐量
-
JVM將這個TLAB作為內(nèi)存分配的首選,可以通過
-XX:UseTLAB
設置開啟TLAB空間 -
默認情況下的這個TLAB內(nèi)存只占1%,也可以通過
-XX:TLABWasteTargetPercent
設置其空間的百分比 -
一旦對象在創(chuàng)建這個TLAB空間分配失敗,JVM就會通過加鎖機制確保數(shù)據(jù)操作的原子性
因此在堆空間中,也不一定全部都是數(shù)據(jù)共享的,每個線程直接還存在著TLAB
6,堆空間參數(shù)設置
在堆空間的參數(shù)設置主要有如下
內(nèi)容 | 鏈接地址 |
---|---|
-Xms | 初始堆內(nèi)存 |
-Xmx | 最大堆內(nèi)存 |
-Xmn | 新生代比例 |
-XX:PrintFlagsInitial | 查看所有參數(shù)的默認值 |
-XX:PrintFlagsFinal | 查看所有參數(shù)的最終值 |
-XX:NewRatio | 設置新生代和老年代堆占比 |
-XX:SurvivorRatio | 設置s區(qū)在Eden區(qū)占比 |
-XX:MacTenuringThreshould | 設置新生代垃圾的最大年齡 |
-XX:PrintGCDetails | 輸出詳細GC日志 |
-XX:HandlePromotionFailure | 是否設置空間分配擔保 |
如設置堆空間的初始大小和最大大小:-Xms1024M -Xmx1024M
7,逃逸分析(重點)
在java虛擬機中,對象是在Java堆內(nèi)存中分配的,但是有一種特殊的情況,那就是經(jīng)過了逃逸分析后發(fā)現(xiàn),如果一個對象并沒有套溢出方法的話,那么就有可能被棧上分配,這樣就可能被優(yōu)化成棧上分配。這樣就無需進行堆上分配,也就無需進行垃圾回收了。
7.1,棧上分配
如何將堆上的對象分配到棧上,需要使用到逃逸分析的手段,這是一種可以有效的減少Java程序中同步負載和內(nèi)存壓力的跨函數(shù)全局數(shù)據(jù)流分析算法。
通過逃逸分析,虛擬機的編譯器可以分析一個新的對象的引用使用范圍從而決定是否要將這個對象分配到堆上,逃逸分析的基本行為就是分析對象的作用域:
🎈 當一個對象在方法中被定義之后,對象只在方法內(nèi)部被使用,則沒有發(fā)生逃逸
🎈 當一個對象在方法中被定義之后,他被外部所引用,則認為發(fā)生逃逸
//沒有發(fā)生逃逸分析
public Student test1(){//隨著入棧出棧,該對象被銷毀,對象分配在棧中Student stu = new Student();return null;
}//發(fā)生了逃逸分析
public Student test2(){//該對象被外部所引用,對象分配在堆中Student stu = new Student();return stu;
}
在實際開發(fā)中,能使用局部變量的,就不要使用在方法外定義,也不要考慮靜態(tài)等問題,這也是堆優(yōu)化的策略之一
開啟逃逸分析的命令:-XX:+DoEscapeAnalysis
,關(guān)閉則是-XX:-DoEscapeAnalysis
7.2,同步省略
除了這個棧上分配對象之外,JIT即時編譯器也可以借助逃逸分析來判斷同步塊所使用的鎖對象是否只能被一個線程訪問而沒有發(fā)布到其他線程,如果沒有,那么這個JIT編譯器就會取消堆這部分代碼的同步,這樣就能大大的提高并發(fā)性能,這個取消同步的過程就叫同步省略,又名鎖消除
public void test(){Student stu = new Student();//發(fā)現(xiàn)每次調(diào)用該方法鎖的根本不是同一個對象,因此會將這個鎖消除synchronized(stu){System.out.println("helloi stu");}
}
7.3,標量替換
逃逸分析也能實現(xiàn)這個標量替換,標量指的就是無法再分解的更小的數(shù)據(jù),如Java原始的數(shù)據(jù)類型就是標量,而還可以繼續(xù)分解的對象化就被稱為聚合量,java的對象就被稱為聚合量,他為他還可以分解成標量或者其他聚合量
//聚合量是由標量和聚合量組成
public class User{ //聚合量int age; //標量String name; //標量Account account; //聚合量
}public Account{ //聚合量Double money; //標量
}
而方法的局部變量是存在棧中的,在JIT編譯期間,如果發(fā)現(xiàn)一個對象不被外界訪問,那么結(jié)果這個JIT的優(yōu)化,就會將這個對象拆分成一個個標量,即對應的局部變量,存儲在棧幀里面。如這個Account對象,只有一個money這一個局部變量存儲在棧幀中,而這個對象不存在,這個行為就被稱為標量替換
標量替換的開啟如下:-XX:EliminateAllocations
,默認是打開的,允許將對象打散分配在棧上。
8,堆空間是分配對象的唯一選擇嗎(重點)
答案是:是的!在HotSpot虛擬機確實是這樣子
上面7中,重點是分析了一下這個逃逸分析的,里面是有棧上分配和標量替換的,棧上分配,不就是將對象分配在棧上面,隨著方法的入棧出棧而銷毀的嗎?
首先,逃逸分析這項技術(shù)到如今是一門不成熟的技術(shù),其根本原因就是無法保證逃逸分析的性能銷毀是否一定高于他的性能,雖然通過逃逸分析可以實現(xiàn)標量替換、棧上分配、鎖消除等,但是逃逸分析自身也是需要進行一定系列的復雜分析的,這其實也是一個相對耗時的過程。舉個極端的例子,就是在開啟逃逸分析,經(jīng)過一頓分析之后,發(fā)現(xiàn)沒有一個對象是不逃逸的,都被外面所引用著,那么這個逃逸分析的分析過程就被白白的浪費掉了。
其次,從逃逸分析的理論上來說,py,c++都是通過堆棧來存儲對象,所以逃逸分析確實是可以用的,并且在jvm內(nèi)部,逃逸分析也是一項主要的調(diào)優(yōu)手段。但是,棧上是否會分配內(nèi)存來保存那些未被逃逸的對象,這取決于JVM設計者的選擇,而在官方的HotSpot虛擬機規(guī)范中明確指出,該虛擬機中并沒有這么做,可以在逃逸分析的相關(guān)文檔中查看,因此也可以明確對象的實例都是創(chuàng)建在堆上的。
最后通過一段代碼來說明:
其jvm相關(guān)參數(shù)配置如下,主要是通過-XX:-DoEscapeAnalysis
的加減號控制是否開啟逃逸分析
//未開啟逃逸分析
-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
//開啟逃逸分析
-Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
其代碼如下
public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {createUser();}long end = System.currentTimeMillis();System.out.println("花費的時間為:" + (end - start) + "ms");try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}
}public static void createUser(){//對象未發(fā)生逃逸User user = new User();
}
可以發(fā)現(xiàn)不開啟逃逸分析創(chuàng)建10000000個對象需要77ms,而開啟這個逃逸分析可以發(fā)現(xiàn)只需要4ms。并且可以通過這個通過jvisaulVm發(fā)現(xiàn)在開啟逃逸分析之后,stackAllocation的個數(shù)是1000萬個,而開啟逃逸分析之后的個數(shù)只有幾百甚至幾十萬個,說明對象此時是隨著方法入棧出棧而銷毀了。那說明逃逸分析確實是可以將對象不分配在堆上面的,但是HotSpot又明確說了不會在棧上分配內(nèi)存,因此只剩下一種解釋:標量替換,如果將這一個個對象拆分成局部變量,然后通過局部變量存儲在棧幀上,從而代替這里的對象,然后隨著棧幀的創(chuàng)建而創(chuàng)建,隨著棧幀的銷毀而銷毀。
總而言之就一句話,逃逸分析理論確實可以存儲在棧上,但是在HotSpot虛擬機中還是只有堆分配對象,而HotSpot是通過逃逸分析以及標量替換來實現(xiàn)棧上分配的,但是棧上分配的并不是像堆一樣完整的對象,而是組成對象的各個標量