JVM学习笔记之-堆,年轻代与老年代,对象分配过程,Minor GC、Major GC、Full GC,堆内存大小与OOM,堆空间分代,内存分配策略,对象分配内存,小结堆空间,逃逸分析,常用调优工具
堆的核心概述
概述
一個JVM實例只存在一個堆內(nèi)存,堆也是Java內(nèi)存管理的核心區(qū)域。Java堆區(qū)在JVM 啟動的時候即被創(chuàng)建,其空間大小也就確定了。是JVM管理的最大一塊內(nèi)存空間。
堆內(nèi)存的大小是可以調(diào)節(jié)的。
《Java虛擬機規(guī)范》規(guī)定,堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的。
所有的線程共享Java堆,在這里還可以劃分線程私有的緩沖區(qū)( ThreadLocal Allocation Buffer,TLAB)。
《Java制擬機規(guī)范》中時Java堆的描述是:所有的對象實例以及數(shù)組都應(yīng)當在運行時分配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated )
我要說的是:“幾乎”所有的對象實例都在這里分配內(nèi)存。—從實際使用角度看的。
數(shù)組和對象可能永遠不會存儲在棧上,因為棧幀中保存引用,這個引用指向?qū)ο蠡蛘邤?shù)組在堆中的位置。
在方法結(jié)束后,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除。
堆,是GC( Garbage Collection,垃圾收集器)執(zhí)行垃圾回收的重點區(qū)域。
內(nèi)存細分
現(xiàn)代垃圾收集器大部分都基于分代收集理論設(shè)計,堆空間細分為:
Java 7及之前堆內(nèi)存邏輯上分為三部分:新生區(qū)+養(yǎng)老區(qū)+永久區(qū)
Young Generation Space 新生區(qū) Young /New又被劃分為Eden區(qū)和Survivor區(qū)Tenure generation space 養(yǎng)老區(qū) Old/TenurePermanent Space 永久區(qū) PermJava 8及之后堆內(nèi)存邏輯上分為三部分:新生區(qū)+養(yǎng)老區(qū)+元空間
Young Generation Space 新生區(qū) Young /New又被劃分為Eden區(qū)和Survivor區(qū) Tenure generation space 養(yǎng)老區(qū) old/Tenure Meta Space 元空間 Meta
邏輯上堆空間是包含 永久區(qū)或者元空間的,實際上是不歸屬于堆管轄的
JDK7 堆空間結(jié)構(gòu)
JDK8 堆空間結(jié)構(gòu)
JDK7與JDK8的內(nèi)存結(jié)構(gòu)變化
設(shè)置堆內(nèi)存大小與OOM
堆空間大小設(shè)置
Java堆區(qū)用于存儲Java對象實例,那么堆的大小在JVM啟動時就已經(jīng)設(shè)定好了,大家可以通過選項"-Xmx"和"-Xms"來進行設(shè)置。
“-Xms"用于表示堆區(qū)的起始內(nèi)存,等價于-XX:InitialHeapsize“-xmx"則用于表示堆區(qū)的最大內(nèi)存,等價于-XX:MaxHeapsize一旦堆區(qū)中的內(nèi)存大小超過“-Xmx"所指定的最大內(nèi)存時,將會拋出OutofMemoryError異常。
通常會將-Xms和一Xmx兩個參數(shù)配置相同的值,其目的是為了能夠在java垃圾回收機制清理完堆區(qū)后不需要重新分隔計算堆區(qū)的大小,從而提高性能。
默認情況下,
初始內(nèi)存大小:物理電腦內(nèi)存大小/ 64最大內(nèi)存大小:物理電腦內(nèi)存大小/ 4開發(fā)中建議將初始堆內(nèi)存和最大堆內(nèi)存設(shè)置成相同的值,因為釋放最大堆內(nèi)存會消耗系統(tǒng)線程
代碼演示
package com.fs.dui;/*** 1.如何設(shè)置堆空間大小參數(shù)* -xms用來設(shè)置堆空間(年輕代+老年代)的初始內(nèi)存大小* -X是jvm的運行參數(shù)* ms 是memory start* -Xmx用來設(shè)置堆空間(年輕代+老年代)的最大內(nèi)存大小** 2. 默認堆空間大小* 初始內(nèi)存大小:物理電腦內(nèi)存大小/ 64* 最大內(nèi)存大小:物理電腦內(nèi)存大小/ 4* 手動設(shè)置* -Xms600m -Xmx600m* 開發(fā)中建議將初始堆內(nèi)存和最大堆內(nèi)存設(shè)置成相同的值,因為釋放最大堆內(nèi)存會消耗系統(tǒng)線程*/ public class DuiDemo {public static void main(String[] args) {//返回Java虛擬機中的堆內(nèi)存總量long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;//返回Java虛擬機試圖使用的最大堆內(nèi)存量long maxMemory = Runtime.getRuntime( ).maxMemory() / 1024 / 1024;System.out.println( "-xms : " + initialMemory + "M");System.out.println("-xmx : " + maxMemory +"M");//核算可用內(nèi)存實際上是小于內(nèi)存的 // System.out.println("系統(tǒng)內(nèi)存大小為: " + initialMemory * 64.0 / 1024 +"G"); // System.out.println("系統(tǒng)內(nèi)存大小為: " + maxMemory * 4.0 / 1024 + "G");/*未設(shè)置 -Xms與-Xmx參數(shù)獲取到的值 -xms : 123M -xmx : 1812M 系統(tǒng)內(nèi)存大小為: 7.6875G 系統(tǒng)內(nèi)存大小為: 7.078125G*//*以設(shè)置 -Xms600m -Xmx600m -xms : 575M -xmx : 575M*/} }查看設(shè)置參數(shù)命令
查看設(shè)置的參數(shù):
方式一:jps/ jstat -gc進程id
方式二:-XX:+PrintGcDetails
方式一:jps/ jstat -gc進程id
計算一下
(25600+25600+153600+409600) /1024 = 600
為什么代碼中是575呢?
(25600+153600+409600) /1024 = 575
方式二:-XX:+PrintGcDetails
OutOfMemory舉例
代碼
package com.fs.dui;import java.util.ArrayList; import java.util.Random;/*** 模擬OOM* 設(shè)置上限 100m*/ public class OOMDemo {public static void main(String[] args) {ArrayList<Picture> pictures = new ArrayList<Picture>();while (true){try {Thread.sleep(20);}catch (Exception e){e.printStackTrace();}pictures.add(new Picture(new Random().nextInt(1024*1024)));}} }class Picture{private byte[] pixels;public Picture(int len) {this.pixels = new byte[len];} }
年輕代與老年代
存儲在JVM中的Java對象可以被劃分為兩類:
一類是生命周期較短的瞬時對象,這類對象的創(chuàng)建和消亡都非常迅速另外一類對象的生命周期卻非常長,在某些極端的情況下還能夠與JVM的生命周期保持一致。Java堆區(qū)進一步細分的話,可以劃分為年輕代(YoungGen)和老年代(oldGen)
其中年輕代又可以劃分為Eden空間、Survivor0( 幸存者0)空間和survivor1空間(有時也叫做from區(qū)、to區(qū)) 。
下面這參數(shù)開發(fā)中一般不會調(diào):
配置新生代與老年代在堆結(jié)構(gòu)的占比。
默認-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個堆的1/5在HotSpot中,Eden空間和另外兩個survivor空間缺省所占的比例是8:1:1當然開發(fā)人員可以通過選項
-xX : survivorRatio”調(diào)整這個空間比例。默認值為8比如: -xx:survivorRatio=8 設(shè)置新生代中Eden區(qū)與survivor區(qū)的比例為8:1:1-XX:-UseAdaptivesizePolicy :關(guān)閉自適應(yīng)內(nèi)存分配策略幾乎所有的Java對象都是在Eden區(qū)被new出來的。
絕大部分的Java對象的銷毀都在新生代進行了。
IBM公司的專門研究表明,新生代中 80% 的對象都是“朝生夕死”的。可以使用選項"-xmn"設(shè)置新生代最大內(nèi)存大小
-xmn 這個參數(shù)一般使用默認值就可以了。新生代對象分配過程
概述
為新對象分配內(nèi)存是一件非常嚴謹和復(fù)雜的任務(wù),JVM的設(shè)計者們不僅需要考慮內(nèi)存如何分配、在哪里分配等問題,并且由于內(nèi)存分配算法與內(nèi)存回收算法密切相關(guān),所以還需要考慮Gc執(zhí)行完內(nèi)存回收后是否會在內(nèi)存空間中產(chǎn)生內(nèi)存碎片。
1. new的對象先放伊甸園區(qū)。此區(qū)有大小限制。2.當伊甸園的空間填滿時,程序又需要創(chuàng)建對象,JVM的垃圾回收器將對伊甸園區(qū)進行垃圾回收(Minor GC),將伊甸園區(qū)中的不再被其他對象所引用的對象進行銷毀。再加載新的對象放到伊甸園區(qū)3.然后將伊甸園中的剩余對象移動到幸存者0區(qū)。4.如果再次觸發(fā)垃圾回收,此時上次幸存下來的放到幸存者0區(qū)的,如果沒有回收,就會放到幸存者1區(qū)。5.如果再次經(jīng)歷垃圾回收,此時會重新放回幸存者0區(qū),接著再去幸存者1區(qū)。6.啥時候能去養(yǎng)老區(qū)呢?可以設(shè)置次數(shù)。默認是15次。》可以設(shè)置參數(shù):-XX:MaxTenuringThreshold=<N>進行設(shè)置。7.在養(yǎng)老區(qū),相對悠閑。當養(yǎng)老區(qū)內(nèi)存不足時,再次觸發(fā)cC: Major Gc,進行養(yǎng)老區(qū)的內(nèi)存清理。8.若養(yǎng)老區(qū)執(zhí)行了Major Gc之后發(fā)現(xiàn)依然無法進行對象的保存,就會產(chǎn)生ooM異常》java.lang. outOfMemoryError: Java heap spaceYGC/Minor GC是當Eden滿的時候,就會觸發(fā)YGC/Minor GC
可以設(shè)置參數(shù):-XX:MaxTenuringThreshold=進行設(shè)置 進入老年代的閾值。
總結(jié):
針對幸存者se,s1區(qū)的總結(jié):復(fù)制之后有交換,誰空誰是to.
關(guān)于垃圾回收:頻繁在新生區(qū)收集,很少在養(yǎng)老區(qū)收集,幾乎不在永久區(qū)/元空間收集。
對象分配的特殊情況
圖解對象分配過程
代碼舉例與JVisualVM演示對象的分配過程
package com.fs.jvm.xlniandai; import java.util.ArrayList;/*** -Xms60om -Xmx600m*/ public class HeapInstanceDemo {public static void main(String[] args) {ArrayList<HeapInstanceDemo> list = new ArrayList< HeapInstanceDemo > ();while (true) {list.add(new HeapInstanceDemo());try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}} }將代碼運行起來,使用工具查看變化
當老年代對象存放滿后,就會報錯OOM
常用調(diào)優(yōu)工具
JDK命令行Eclipse: Memory Analyzer ToolJconsolevisualVMJprofilerJava Flight RecorderGcviewerGC Easy淺談 Minor GC、Major GC、Full GC
JVM在進行GC時,并非每次都對上面三個內(nèi)存(新生代,老年代,方法區(qū))區(qū)域一起回收的,大部分時候回收的都是指新生代。
針對HotSpot VM的實現(xiàn),它里面的Gc按照回收區(qū)域又分為兩大種類型:一種是部分收集(Partial Gc) ,一種是整堆收集( Fu1l GC)
部分收集:不是完整收集整個Java堆的垃圾收集。其中又分為:新生代收集(Minor Gc / Young cc):只是新生代(Eden/S1,S0)的垃圾收集老年代收集(Major Gc / old Gc):只是老年代的垃圾收集。√目前,只有cMS Gc會有單獨收集老年代的行為。√注意,很多時候Major GC會和Fu1l Gc混淆使用,需要具體分辨是老年代回收還是整堆回收。混合收集(Mixed Gc):收集整個新生代以及部分老年代的垃圾收集.√目前,只有G1 Gc會有這種行為整堆收集(Full Gc):收集整個java堆和方法區(qū)的垃圾收集。最簡單的分代式GC策略的觸發(fā)條件
年輕代GC(Minor GC)觸發(fā)機制:
當年輕代空間不足時,就會觸發(fā)Minor Gc,這里的年輕代滿指的是Eden代滿,Survivor滿不會引發(fā)Gc。(每次Minor Gc會清理年輕代的內(nèi)存。)
因為Java 對象大多都具備朝生夕滅的特性,所以 Minor GC非常頻繁,一般回收速度也比較快。這一定義既清晰又易于理解。
Minor GC會引發(fā)STW,暫停其它用戶的線程(STW),等垃圾回收結(jié)束,用戶線程才恢復(fù)運行。
老年代Gc (Major GC/Fu11 GC)觸發(fā)機制:
指發(fā)生在老年代的Gc,對象從老年代消失時,我們說“Major Gc”或“Full Gc發(fā)生了。
出現(xiàn)了Major Gc,經(jīng)常會伴隨至少一次的Minor Gc(但非絕對的,在Parallel scavenge收集器的收集策略里就有直接進行Major Gc的策略選擇過程)
√ 也就是在老年代空間不足時,會先嘗試觸發(fā)Minor Gc。如果之后空間還不足,則觸發(fā)Major GcMajor Gc的速度一般會比Minor Gc慢1e倍以上,STw的時間更長。
如果Major GC后,內(nèi)存還不足,就報oOM了。
Major Gc的速度一般會比Minor Gc慢1e倍以上。
Full Gc觸發(fā)機制: (后面筆記會詳細來說明)
觸發(fā)Fu1l GC執(zhí)行的情況有如下五種:
(1)調(diào)用system.gc()時,系統(tǒng)建議執(zhí)行Fu1l Gc,但是不必然執(zhí)行(2)老年代空間不足
(3)方法區(qū)空間不足
(4)通過Minor GC后進入老年代的平均大小大于老年代的可用內(nèi)存
(5)由Eden區(qū)、survivor spacee (From Space)區(qū)向survivor space1 (To Space)區(qū)復(fù)制時,對象大小大于To Space可用內(nèi)存,則把該對象轉(zhuǎn)存到老年代,且老年代的可用內(nèi)存小于該對象大小
說明: full gc是開發(fā)或調(diào)優(yōu)中盡量要避免的。這樣暫時時間會短一些。
堆空間分代思想
為什么需要把Java堆分代?不分代就不能正常工作了嗎?·
經(jīng)研究,不同對象的生命周期不同。70%-99%的對象是臨時對象。
新生代:有Eden、兩塊大小相同的survivor (又稱為from/to,s0/s1)構(gòu)成,to總為空。
老年代:存放新生代中經(jīng)歷多次Gc仍然存活的對象。
其實不分代完全可以,分代的唯一理由就是優(yōu)化GC性能。如果沒有分代,那所有的對象都在一塊,就如同把一個學(xué)校的人都關(guān)在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區(qū)域進行掃描。而很多對象都是朝生夕死的,如果分代的話,把新創(chuàng)建的對象放到某一地方,當GC 的時候先把這塊存儲“朝生夕死”對象的區(qū)域進行回收,這樣就會騰出很大的空間出來。
內(nèi)存分配策略(或?qū)ο筇嵘?Promotion)規(guī)則)
如果對象在Eden出生并經(jīng)過第一次MinorGc后仍然存活,并且能被survivor容納的話,將被移動到survivor空間中,并將對象年齡設(shè)為1 。對象在survivor區(qū)中每熬過一次MinorGc ,年齡就增加1 歲,當它的年齡增加到一定程度(默認為15 歲,其實每個JVM、每個Gc都有所不同)時,就會被晉升到老年代中。
對象晉升老年代的年齡閾值,可以通過選項-XX:MaxTenuringThreshold來設(shè)置。
針對不同年齡段的對象分配原則如下所示:
優(yōu)先分配到Eden大對象直接分配到老年代盡量避免程序中出現(xiàn)過多的大對象。更恐怖的是創(chuàng)建的大對象是朝生夕死的,就是只使用一次,那么直接放在老年代就會浪費內(nèi)存長期存活的對象分配到老年代動態(tài)對象年齡判斷如果survivor區(qū)中相同年齡的所有對象大小的總和大于survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代,無須等到MaxTenuringThreshold 中要求的年齡。空間分配擔保-XX:HandlePromotionFailure演示大對象直接分配到老年代的代碼
執(zhí)行后的堆日志說明大對象就直接存儲在老年代中,之前是沒有進行任何GC操作的,因為沒有GC日志打印
為對象分配內(nèi)存:TLAB
為什么有TLAB ( Thread Local Allocation Buffer ) ?
·堆區(qū)是線程共享區(qū)域,任何線程都可以訪問到堆區(qū)中的共享數(shù)據(jù)
·由于對象實例的創(chuàng)建在JVM中非常頻繁,因此在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線程不安全的
·為避免多個線程操作同一地址,需要使用加鎖等機制,進而影響分配速度。
什么是TLAB ?
·從內(nèi)存模型而不是垃圾收集的角度,對Eden區(qū)域繼續(xù)進行劃分,JVM為每個線程分配了一個私有緩存區(qū)域,它包含在Eden空間內(nèi)。
·多線程同時分配內(nèi)存時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升內(nèi)存分配的吞吐量,因此我們可以將這種內(nèi)存分配方式稱之為快速分配策略。
·據(jù)我所知所有openJDK衍生出來的JVM都提供了TLAB的設(shè)計。
TL.AB的再說明:
盡管不是所有的對象實例都能夠在TAB中成功分配內(nèi)存,但JVM確實是將TLAB作為內(nèi)存分配的首選。
在程序中,開發(fā)人員可以通過選項“-xX:UseTLAB”設(shè)置是否開啟TLAB空間。
默認情況下,TLAB空間的內(nèi)存非常小,僅占有整個Eden空間的1%,當然我們可以通過選項“-XX:TLABWasteTargetPercent”設(shè)置TLAB空間所占用Eden空間的百分比大小。
一旦對象在TAB空間分配內(nèi)存失敗時,JVM就會嘗試著通過使用加鎖機制確保數(shù)據(jù)操作的原子性,從而直接在Eden空間中分配內(nèi)存。
代碼測試TLAB參數(shù)是否開啟的情況
整體加上TLAB圖示
小結(jié)堆空間的參數(shù)設(shè)置
官網(wǎng)說明:官網(wǎng)說明
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-XX:+PrintFlagsInitial :查看所有的參數(shù)的默認初始值
-XX:+PrintFlagsFinal :查看所有的參數(shù)的最終值(可能會存在修改,不再是初始值)
-Xms:初始堆空間內(nèi)存(默認為物理內(nèi)存的1/64)
-Xmx:最大堆空間內(nèi)存(默認為物理內(nèi)存的1/4)
-Xmn:設(shè)置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代與老年代在堆結(jié)構(gòu)的占比
-XX:MaxTenuringThreshold:設(shè)置新生代垃圾的最大年齡
-XX:+PrintGcDetails:輸出詳細的GC處理日志
-XX:+PrintGc-verbose:gc 打印gc簡要信息
-XX:HandLePromotionFailure:是否設(shè)置空間分配擔保
參數(shù)解釋說明
堆是分配對象的唯一選擇嗎?
在《深入理解Java虛擬機》中關(guān)于Java堆內(nèi)存有這樣一段描述:
隨著JIT編譯期的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標量替換優(yōu)化技術(shù)將會導(dǎo)致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么“絕對”了。
在Java虛擬機中,對象是在Java堆中分配內(nèi)存的,這是一個普遍的常識。但是,有一種特殊情況,那就是**如果經(jīng)過逃逸分析(Escape Analysis)后發(fā)現(xiàn),一個對象并沒有逃逸出方法的話,那么就可能被優(yōu)化成棧上分配。**這樣就無需在堆上分配內(nèi)存,也無須進行垃圾回收了。這也是最常見的堆外存儲技術(shù)。
此外,前面提到的基于openJDK深度定制的TaoBaoVM,其中創(chuàng)新的GCIH ( GC invisible heap)技術(shù)實現(xiàn)off-heap,將生命周期較長的Java對象從heap中移至heap外,并且cc不能管理GcIH內(nèi)部的Java對象,以此達到降低cc的回收頻率和提升Gc的回收效率的目的。
逃逸分析概述
如何將堆上的對象分配到棧,需要使用逃逸分析手段。
這是一種可以有效減少Java程序中同步負載和內(nèi)存堆分配壓力的跨函數(shù)全局數(shù)據(jù)流分析算法。
通過逃逸分析,Java Hotspot編譯莽能夠才出一下新的N家的力u使用范圍從而決定是否要將這個對象分配到堆上。
逃逸分析的基本行為就是分析對象動態(tài)作用域:
當一個對象在方法中被定義后,對象只在方法內(nèi)部使用,則認為沒有發(fā)生逃逸。
當一個對象在方法中被定義后,它被外部方法所引用,則認為發(fā)生逃逸。例如作為調(diào)用參數(shù)傳遞到其他地方中。
發(fā)生逃逸的幾種情況
如何快速判斷是否發(fā)生了逃逸分析,就看**new的對象是否有可能在方法外被調(diào)用**
逃逸參數(shù)設(shè)置
在JDK 7 版本之后,HotSpot中默認就已經(jīng)開啟了逃逸分析。
如果使用的是較早的版本,開發(fā)人員則可以通過:
選項“-XX:+DoEscapeAnalysis"顯式開啟逃逸分析
通過選項“-XX: +PrintEscapeAnalysis"查看逃逸分析的篩選結(jié)果。
結(jié)論
開發(fā)中能使用局部變量的,就不要使用在方法外定義。
逃逸分析:代碼優(yōu)化
使用逃逸分析,編譯器可以對代碼做如下優(yōu)化:
一、棧上分配。將堆分配轉(zhuǎn)化為棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
二、同步省略。如果一個對象被發(fā)現(xiàn)只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。
三、分離對象或標量替換。有的對象可能不需要作為一個連續(xù)的內(nèi)存結(jié)構(gòu)存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內(nèi)存,而是存儲在cPU寄存器中。
代碼優(yōu)化:棧上分配
JIT編譯器在編譯期間根據(jù)逃逸分析的結(jié)果,發(fā)現(xiàn)如果一個對象并沒有逃逸出方法的話,就可能被優(yōu)化成棧上分配。分配完成后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行,最后線程結(jié)束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收了。
常見的棧上分配的場景
在逃逸分析中,已經(jīng)說明了。分別是給成員變量賦值、方法返回值、實例引用傳遞。
代碼演示
package com.fs.demo;/*** 棧上分配測試* -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails* -DoEscapeAnalysis關(guān)閉逃逸 +DoEscapeAnalysis開啟逃逸*/public class StackAllocation {public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {alloc();}//查看執(zhí)行時間long end = System.currentTimeMillis();System.out.println("花費的時間為:" + (end - start) + " ms");//為了方便查看堆內(nèi)存中對象個數(shù),線程sLeeptry {Thread.sleep(1000000);} catch (InterruptedException e1) {e1.printStackTrace();}}private static void alloc() {Demo demo = new Demo();}static class Demo {} }演示-DoEscapeAnalysis關(guān)閉逃逸分析
1:執(zhí)行時間長
2:內(nèi)存實例對象為遍歷添加次數(shù)
演示+DoEscapeAnalysis開啟逃逸分析
1:執(zhí)行時間短
2:內(nèi)存中添加的對象大量減少
將內(nèi)存參數(shù)從1G改為256,關(guān)閉逃逸分析情況
1:發(fā)生GC
2:執(zhí)行時間長
將內(nèi)存參數(shù)從1G改為256,打開逃逸分析情況
未發(fā)生GC,執(zhí)行時間短
代碼優(yōu)化:同步省略(消除)
線程同步的代價是相當高的,同步的后果是降低并發(fā)性和性能。
在動態(tài)編譯同步塊的時候,JIT編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發(fā)布到其他線程。如果沒有,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高并發(fā)性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除。
代碼演示
下面綠色代碼中,這個對象本身會發(fā)生逃逸分析,這個對象加鎖是毫無意義的
若這個對象本身是個局部變量,不會被多個線程共用,若打開逃逸分析,jvm會自動取消掉同步這一塊的代碼
代碼優(yōu)化:分離對象或標量替換
**標量(scalar)**是指一個無法再分解成更小的數(shù)據(jù)的數(shù)據(jù)。Java中的原始數(shù)據(jù)類型就是標量。
相對的,那些還可以分解的數(shù)據(jù)叫做聚合量(Aggregate) , Java中的對象就是聚合量,因為他可以分解成其他聚合量和標量。
在JIT階段,如果經(jīng)過逃逸分析,發(fā)現(xiàn)一個對象不會被外界訪問的話,那么經(jīng)過JIT優(yōu)化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換。
列子:
下面的代碼new Point(1,2)就是個聚合量
可以看到,Point這個聚合量經(jīng)過逃逸分析后,發(fā)現(xiàn)他并沒有逃逸,就被替換成兩個聚合量了。那么標量替換有什么好處呢?就是可以大大減少堆內(nèi)存的占用。因為一旦不需要創(chuàng)建對象了,那么就不再需要分配堆內(nèi)存了。
標量替換為棧上分配提供了很好的基礎(chǔ)。
標量替換參數(shù)設(shè)置︰
參數(shù)-XX:+EliminateAllocations:開啟了標量替換(默認打開),允許將對象打散分配在棧上。
逃逸分析總結(jié)
逃逸分析并不成熟
關(guān)于逃逸分析的論文在1999年就已經(jīng)發(fā)表了,但直到JDK 1.6才有實現(xiàn),而且這項技術(shù)到如今也并不是十分成熟的。
其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經(jīng)過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列復(fù)雜的分析的,這其實也是一個相對耗時的過程。
一個極端的例子,就是經(jīng)過逃逸分析之后,發(fā)現(xiàn)沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
雖然這項技術(shù)并不十分成熟,但是它也是即時編譯器優(yōu)化技術(shù)中一個十分重要的手段。注意到有一些觀點,認為通過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,但是取決于JVM設(shè)計者的選擇。據(jù)我所知,oracle HotspotJVM中并未這么做,這一點在逃逸分析相關(guān)的文檔里已經(jīng)說明,所以可以明確所有的對象實例都是創(chuàng)建在堆上。
目前很多書籍還是基于JDK 7以前的版本,JDK已經(jīng)發(fā)生了很大變化,intern字符串的緩存和靜態(tài)變量曾經(jīng)都被分配在永久代上,而永久代已經(jīng)被元數(shù)據(jù)區(qū)取代。但是,intern字符串緩存和靜態(tài)變量并不是被轉(zhuǎn)移到元數(shù)據(jù)區(qū),而是直接在堆上分配,所以這一點同樣符合前面一點的結(jié)論:對象實例都是分配在堆上。
本章小結(jié)
年輕代是對象的誕生、成長、消亡的區(qū)域,一個對象在這里產(chǎn)生、應(yīng)用,最后被垃圾回收器收集、結(jié)束生命。
老年代放置長生命周期的對象,通常都是從survivor區(qū)域篩選拷貝過來的Java對象。當然,也有特殊情況,我們知道普通的對象會被分配在TLAB上;如果對象較大,JVM會試圖直接分配在Eden其他位置上;如果對象太大,完全無法在新生代找到足夠長的連續(xù)空閑空間,JVM就會直接分配到老年代。
當GC只發(fā)生在年輕代中,回收年輕代對象的行為被稱為MinorGC。當GC發(fā)生在老年代時則被稱為MajorGC或者FullGC。一般的,MinorcC的發(fā)生頻率要比MajorGc高很多,即老年代中垃圾回收發(fā)生的頻率將大大低
于年輕代。
總結(jié)
以上是生活随笔為你收集整理的JVM学习笔记之-堆,年轻代与老年代,对象分配过程,Minor GC、Major GC、Full GC,堆内存大小与OOM,堆空间分代,内存分配策略,对象分配内存,小结堆空间,逃逸分析,常用调优工具的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM学习笔记之-运行时数据区概述及线程
- 下一篇: JVM学习笔记之-方法区,栈、堆、方法区