堆的核心概述
- 所有的對象執行個體以及數組都應當在運作時配置設定在堆上
- 從實際實用角度看 --"幾乎所有的對象執行個體都在堆中配置設定記憶體"
- 數組和對象可能永遠不會存儲在棧上,因為棧幀中儲存引用,這個引用指向對象或者數組在堆中的位置
- 在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除
- 堆,是GC執行垃圾回收的重點區域
記憶體細分
# 1. 堆空間的大小設定
-Xms 用來設定堆空間(年輕代+老年代)的初始記憶體大小
-X 是jvm的運作參數
ms 是memory start
-Xmx 用來設定堆空間(年輕代+老年代) 的最大記憶體大小
# 2. 預設堆空間的大小
初始記憶體大小:實體電腦記憶體大小/64
最大記憶體大小:實體電腦記憶體大小/4
# 3. 手動設定: -Xms600m -Xmx600m
開發中建議将初始堆記憶體和最大的堆記憶體設定成相同的值.
- 原因:值不同,當達到堆的初始記憶體大小後,堆會不斷擴容,當空閑時又會釋放記憶體,頻繁的擴容與釋放會造成不必要的系統壓力.
# 4. 檢視設定的參數:
方式一:在指令行中jps 檢視程序号
jstat -gc 程序id
方式二: 增加參數 -XX:+PrintGCDetails
新生代與老年代
- 存儲在JVM中的Java對象可以被劃分為兩類
- 一類是生命周期較短的瞬時對象,這類對象的建立和消亡都非常迅速
- 另一類對象的生命周期卻非常長,在某些極端情況下還能與JVM的生命周期保持一緻
- Java堆區進一步細分的話,可以劃分為年輕代(YoungGen)和老年代(OldGen)
- 幾乎所有的Java對象都是在Eden區被new出來的.
- 絕大部分的Java對象的銷毀都在新生代進行了
- 其中年輕代又可以劃分為Eden空間,Survivor0空間和Survivor1空間(有時也叫做from區,to區)
# 新生代與老年代相關參數設定
> 配置新生代與老年代在堆結構的占比
> 預設:-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3
>可以修改為:-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個堆的1/5
- 如果程式很多對象生命周期比較長,可以考慮将老年代占比調大
> 在HotSpot中,Eden空間和另外兩個Survivor空間預設所占的比例是8:1:1
>可以通過選項 -XX:SurvivorRatio=x 調整這個空間比例
> 但是會有個問題,實際的比例并不足8:1:1,實際是6:1:1
可以通過 -XX:-UseAdaptiveSizePolicy 關閉自适應的記憶體配置設定政策(實測無用)
最好還是直接設定-XX:SurvivorRatio=8
對象配置設定的一般過程
- new的對象先放在伊甸園區,此區有大小限制
- 當伊甸園的空間填滿時,程式有需要建立對象,JVM的垃圾回收器将對伊甸園區進行垃圾回收(Minor GC),将伊甸園區中的不再被其他對象所引用的對象進行銷毀,再加載新的對象放到伊甸園區
- 然後将伊甸園中的剩餘對象移動到幸存者0區
- 如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者0區的,如果沒有回收,就會放到幸存者1區
- 如果再次經曆垃圾回收,此時會重新放回幸存者0區,接着再去幸存者1區
- 直到這個對象存活了15次,這個對象就可以去養老區了
- 可以通過設定參數:
進行設定-XX:MaxTenuringThreshold=<N>
- 可以通過設定參數:
具體流程圖:
常用調優工具
- JDK指令行 (jsp,jstat)
- Jconsole
- JVisualVM
- Jprofiler
- Java Flight Recorder
- GCViewer
- GC Easy
Minor GC,Major GC,Full GC
JVM再進行GC時,并非每次都對上面三個記憶體(新生代,老年代,方法區)區域一起回收的,大部分時候回收的都是指新生代
正如HotSpot VM的實作,它裡面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
- 部分收集:不是完整收集整個Java對的垃圾收集.其中又分為:
- 新生代收集(Young GC):隻是新生代(Eden,S0,S1)的垃圾收集
- 老年代收集(Major GC/Old GC):隻是老年代的垃圾收集
- 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集
- 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集.
年輕代GC(Minor GC)觸發機制:
- 當年輕代空間不足時,就會觸發Minor GC 這裡的年輕代滿指的時Eden代滿,Survivor滿不會引發GC.
- 因為Java對象大多都具備朝生夕滅的特性,是以MinorGC非常頻繁,一般回收速度也比較快
- Minor GC會引發STW(Stop The World),暫停其他使用者的線程,等垃圾回收結束,使用者線程才恢複運作
老年代GC(Major GC/Full GC)觸發機制:
- 發生在老年代的GC,對象從老年代消失時,我們說"Major GC"或"Full GC"就發生了.
- 出現了Major GC,進場會伴随至少一次的Minor GC
- 也就是在老年代空間不足時.會先嘗試觸發Minor GC,如果之後空間還不足,則出發Major GC
- Major GC的速度一般會比Minor GC慢10倍以上,STW的時間更長
- 如果Major GC後,記憶體還不足,就會出現OOM.
Full GC觸發機制
- 調用System.gc()時,系統建議執行Full GC,但是不必然執行
- 老年代空間不足
- 方法區空間不足
- 通過Minor GC進入老年代的平均大小大于老年代的可用記憶體
- 由Eden區,Survivor0區向Survivor1區複制時,對象大小大于Survivor1區可用記憶體,則把該對象轉存到老年代,且老年代的可用記憶體小于該對象大小
為什麼需要Java堆分代?
分代的唯一理由就是優化GC性能,如果沒有分代,那所有的對象都在一塊,Gc的時候要找到哪些對象沒用,這裡就會對堆的所有區域進行掃描.而很多對象生命周期都很短,如果分代的話,把新建立的對象放到一塊,當GC的時候先把這塊存儲"朝生夕死"對象的區域進行回收,這樣就會騰出很大的空間出來
記憶體配置設定政策
如果對象再Eden出生并經過第一次MinorGC後仍然存活,并且能被Survivor容納的話,将被移動到Survivor空間中,并将對象年齡設為1.對象在Survivor區中每熬過一次MinorGC,年齡就增加1歲,當他的年齡增加到一定程度(預設為15歲)時,就會被晉升到老年代中
對象晉升老年代的年齡門檻值可以通過設定參數:
-XX:MaxTenuringThreshold=<N>
進行設定
針對不同年齡段的對象配置設定原則如下所示:
- 優先配置設定到Eden
- 大對象直接配置設定到老年代
- 盡量避免程式中出現過多的大對象
- 長期存活的對象配置設定到老年代
- 動态對象年齡判斷
- 如果Survivor區中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡
- 空間配置設定擔保
-
-XX:HandlePromotionFailure
-
TLAB(Thread Local Allocation Buffer)
為什麼有TLAB?
- 堆區是線程共享區域,任何線程都可以通路到堆區中的共享資料
- 由于對象執行個體的建立在JVM中非常頻繁,是以在并發環境下從堆區中劃分記憶體空間是線程不安全的
- 為避免多個線程操作同一位址,需要使用加鎖等機制,進而影響配置設定速度
什麼是TLAB?
- 從記憶體模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM為每個線程配置設定了一個私有緩存區域,它包含在Eden空間内
- 多線程同時配置設定記憶體時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升記憶體配置設定的吞吐量,是以我們可以将這種記憶體配置設定方式稱之為 快速配置設定政策
堆空間的常用參數設定總結
** -XX:+PrintFlagsInitial:檢視所有的參數的預設初始值
-xx:+PrintFlagsFinal:檢視所有的參數的最終值
具體檢視某個參數的指令:
jps:檢視目前運作中的程序
jinfo -flag SurvivorRatio 程序id
-Xms:初始堆空間記憶體(預設為實體記憶體的1/64)
-Xmx:最大堆空間記憶體(預設為實體記憶體的1/4)
-Xmn:設定新生代的大小(初始值及最大值)
-XX:NewRatio:配置新生代與老年代在堆結構的占比
-XX:SurvivorRatio:設定新生代中Eden和S0/S1空間的比例
-XX:MaxTenuringThreshold:設定新生代垃圾的最大年齡
-XX:PrintGCDetails:輸出詳細的GC處理日志
-XX:PrintGC:列印GC簡要資訊
-XX:HandlePromotionFailure:是否設定空間配置設定擔保
堆是配置設定對象存儲的唯一選擇麼?
在《深入了解Java虛拟機》中關于Java堆記憶體有這樣一段描述:
随着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上配置設定,标量替換優化技術将會導緻一些微妙的變化,所有的對象都配置設定到堆上也漸漸變得不那麼"絕對"了.
- 在Java虛拟機中,對象是在Java堆中配置設定記憶體的,這是一個普遍的常識.但是,有一種特殊情況,那就是如果經過逃逸分析後發現,一個對象并沒有逃逸出方法的話,那麼就可能被優化成棧上配置設定.這樣就無需在堆上配置設定記憶體,也無需進行垃圾回收了,這也是最常見的堆外存儲技術
- 此外,TaoBaoVM,其中創新的GCIH(GC invisible heap)技術實作off-heap,将生命周期較長的Java對象從heap中移至heap外,并且GC不能管理GCIH内部的對象,以此達到降低GC的回收頻率和提升GC的回收效率的目的
舉個栗子:
pubilc static StringBuffer createStringBuffer(String s1,String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
//這個方法内建立的對象sb可能會被其他方法調用,即這個變量可能會逃逸,是以不能采用棧上配置設定的政策
return sb;
}
pubilc static String createStringBuffer(String s1,String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
//此時StringBuffer沒有逃逸,但是toString()方法建立的String對象逃逸了.....
return sb.toString();
}
開發中能使用局部變量,就不要使用在方法外定義
逃逸分析:代碼優化
使用逃逸分析,編譯器可以對代碼做如下優化:
- 棧上配置設定.将堆配置設定轉換為棧配置設定.如果一個對象在子程式中被配置設定,要使指向該對象的指針永遠不會逃逸,對象可能是棧配置設定的候選,而不是堆配置設定
- 同步省略.如果一個對象被發現隻能從一個線程被通路到,那麼對于這個對象的操作可以不考慮同步
- 分離對象或标量替換.有的對象可能不需要作為一個連續的記憶體結構存在也可以本通路到,那麼對象的部分或全部可以不存儲在記憶體,而是存儲在CPU寄存器中
逃逸分析并不成熟
- 關于逃逸分析的論文在1999年就已經發表了,但直到JDK 1.6才有實作,而且這項技術到如今也并不是十分成熟的。
- 其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經過逃逸分析可以做标量替換、棧上配置設定、和鎖消除。但是逃逸分析自身也是需要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。
- 一個極端的例子,就是經過逃逸分析之後,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了.
- 有一些觀點,認為通過逃逸分析,JVM會在棧上配置設定那些不會逃逸的對象,這在理論上是可行的,但是取決于JVM設計者的選擇,Oracle HotspotJVM中并未這麼做,是以可以明确所有的對象執行個體都是建立在堆上.
總結
- 年輕代是對象的誕生,成長,消亡的區域,一個對象在這裡産生,應用,最後被垃圾回收器收集,結束生命
- 老年代放置長生命周期的對象,通常都是從Survivor區域篩選拷貝過來的Java對象,也有例外:如果對象太大,完全無法在新生代找到足夠長的連續空閑空間,JVM就會直接配置設定到老年代.
- 當GC隻發生在年輕代中,回收年輕代對象的行為被稱為MinorGC,當GC發生在老年代時則被稱為MajorGC或者FullGC.一般的,MinorGC的發生頻率要比MajorGC高很多