目錄
- 1、概述
- 2、堆空間細分:
- 3、堆空間大小設定
- 4、年輕代與老年代
- 5、物件分配
- 6、Minor GC、Major GC、Full GC
- 6.1、Minor GC
- 6.2、Major GC
- 6.3、Full GC
- 7、記憶體分配策略
- 8、TLAB
- 8.1、TLAB介紹
- 8.2、分配程序
- 9、堆空間的引數設定
- 10、堆是物件分配記憶體的唯一選擇?
- 10.1、逃逸分析
- 10.2、優化:
- 1. 堆疊上分配
- 2. 同步省略
- 3. 分離物件/標量替換
- 10.3、逃逸分析缺點
- 11、小結
1、概述
唯一性:
- 一個JVM實體只存在-一個堆記憶體,堆也是Java記憶體管理的核心區域,
核心概述
-
Java堆區在JVM啟動的時候即被創建,其空間大小也就確定了,是JVM管理的最大一塊記憶體空間,|
- 堆記憶體的大小是可以調節的
-
《Java虛擬機規范》規定,堆可以處于物理上不連續的記憶體空間中,但在邏輯上它應該被視為連續的,
-
所有的執行緒共享Java堆,在這里還可以劃分執行緒私有的緩沖區( Thread Local Allocation Buffer, TLAB) ,
-
《Java虛擬機規范》中對Java堆的描述是:所有的物件實體以及陣列都應當在運行時分配在堆上,(The heap is the run-time data area fro which memory for all class instances and arrays is allocated )
? 我要說的是:“幾乎”所有的物件實體都在這里分配記憶體,—從實際使用角度看的,陣列和物件可能永遠不會存盤在堆疊上,因為堆疊幀中保存參考,這個參考指向物件或者陣列在堆中的位置,
-
在方法結束后,堆中的物件不會馬上被移除,僅僅在垃圾收集的時候才會被移除,
-
堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域,
2、堆空間細分:


permanent generation --- 》metaspace
3、堆空間大小設定
- Java堆區用于存盤Java物件實體,那么堆的大小在JVM啟動時就已經設定好了,可以通過選項”-Xmx"和"-Xms"來進行設定,
"-Xms"用于表示堆區的起始記憶體,等價于-XX:InitialHeapSize
"-xmx"則用于表示堆區的最大記憶體,等價于-XX:MaxHeapSize - 一旦堆區中的記憶體大小超過“-Xmx”所指定的最大記憶體時,將會拋出OutOfMemoryError例外,
- 通常會將-Xms和-Xmx兩個引數配置相同的值,其目的是為了能夠在java垃圾回識訓制清理完堆區后不需要重新分隔計算堆區的大小,從而提高性能,
- 默認情況下,初始記憶體大小:物理電腦記憶體大小/64
最大記憶體大小:物理電腦記憶體大小/4
如何查看堆空間大小以及分配
-
開啟一個需要查看的行程--》在jdk的bin目錄下找到 jvisualvm.exe 檔案運行,安裝插件方便查看


- 設定jvm引數 -XX:PrintGCDetalis


-
命令列
先用jps顯示當前行程,再用jstat查看

4、年輕代與老年代
- 存盤在JVM中的Java物件可以被劃分為兩類:
- 一類是生命周期較短的瞬時物件,這類物件的創建和消亡都非常迅速
- 另外一類物件的生命周期卻非常長,在某些極端的情況下還能夠與JVM的生命周期保持一致,
- Java堆區進一步細分的話,可以劃分為年輕代(YoungGen)和老年代(OldGen),其中年輕代又可以劃分為Eden空間、Survivor空間和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”調整這個空間比例,比如-XX:SurvivorRatio=8
-
幾乎所有的Java物件都是在Eden區被new出來的,
-
絕大部分的Java物件的銷毀都在新生代進行了,
- IBM公司的專門研究表明,新生代中808的物件都是“朝生夕死”的,
-
可以使用選項”-Xmn”設定新生代最大記憶體大小,這個引數一般使用默認值就可以了,
5、物件分配
為新物件分配記憶體是一件非常嚴謹和復雜的任務,JVM的設計者們不僅需要考慮記憶體如何分配、在哪里分配等問題,并且由于記憶體分配演算法與記憶體回收演算法密切相關,所以還需要考 慮GC執行完記憶體回收后是否會在記憶體空間中產生記憶體碎片,
- new的物件先放伊甸園區,此區有大小限制,
- 當伊甸園的空間填滿時,程式又需要創建物件,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不再被其他物件所參考的物件進行銷毀,再加載新的物件放到伊甸園區
- 然后將伊甸園中的剩余物件移動到幸存者0區,
- 如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者0區的,如果沒有回收,就會放到幸存者1區,
- 如果再次經歷垃圾回收,此時會重新放回幸存者0區,接著再去幸存者1區,
- 啥時候能去養老區呢?可以設定次數,默認是15次,
- 可以設定引數:-XX:MaxTenuringThreshold=
進行設定, - 在養老區,相對悠閑,當養老區記憶體不足時,再次觸發GC:Major GC,進行養老區的記憶體清理,
- 若養老區執行了Major GC之后發現依然無法進行物件的保存,就會產生OOM例外
java.lang.OutofMemoryError:Java heap space
圖解:

總結:
- 針對幸存者so,s1區的總結:復制之后有交換,誰空誰是to.
- 關于垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不在永久區/元空間收集,

6、Minor GC、Major GC、Full GC
JVM在進行GC時,并非每次都對上面三個記憶體(新生代、老年代;方法區)區域一起回收的,大部分時候回收的都是指新生代,
針對Hotspot VM的實作,它里面的GC按斬訓收區域又分為兩大種型別:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
-
部分收集:不是完整收集整個Java堆的垃圾收集,其中又分為:
- 新生代收集(Minor GC/Young GC):只是新生代(Eden\S0,S1)的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集,
- 目前,只有CMS GC會有單獨收集老年代的行為,
- 注意,很多時候Major GC會和Full GC混淆使用,需要具 體分辨是老年代回識訓是整堆回收,
- 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集,
- 目前,只有G1,GC會有這種行為
-
整堆收集(Full GC):收集整個java堆和方法區的垃圾收集,
6.1、Minor GC
年輕代GC(Minor GC)觸發機制:
- 當年輕代空間不足時,就會觸發Minor GC,這里的年輕代滿指的是Eden代滿,Survivor滿不會引發GC,(每次Minor GC會清理年輕代的記憶體,)
- 因為Java物件大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快,這一定義既清晰又易于理解,
- Minor GC會引發STW,暫停其它用戶的執行緒,等垃圾回收結束,用戶線
程才恢復運行,
6.2、Major GC
老年代GC(Major GC/Full GC)觸發機制:
- 指發生在老年代的GC,物件從老年代消失時,我們說“Major GC”或“Full GC”發生了,
- 出現了Major GC經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇程序),
- 也就是在老年代空間不足時,會先嘗試觸發Minor GC,如果之后空間還不足,則觸發Major GC
- Major GC的速度一般會比Minor GC慢10倍以上,STW的時間更長,
- 如果Major GC后,記憶體還不足,就報OOM了,
6.3、Full GC
Full 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是開發或調優中盡量要避免的,這樣暫時時間會短一些,
7、記憶體分配策略
如果物件在Eden出生并經過第一次MinorGC后仍然存活,并且能被Survivor容納的話,將被移動到survivor空間中,并將物件年齡設為1,物件在Survivor 區中每熬過一次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代中,
物件晉升老年代的年齡閥值,可以通過選項-XX:MaxTenuringThreshold來設定,
針對不同年齡段的物件分配原則如下所示:
-
優先分配到Eden
-
大物件直接分配到老年代,比如eden放不下的物件
- 盡量避免程式中出現過多的大物件
-
長期存活的物件分配到老年代
-
動態物件年齡判斷
- 如果survivor區中相同年齡的所有物件大小的總和大于survivor空
間的一半,年齡大于或等于該年齡的物件可以直接進入老年代,無須等到
MaxTenuringThreshold中要求的年齡,
- 如果survivor區中相同年齡的所有物件大小的總和大于survivor空
-
空間分配擔保
- -XX:HandlePromotionFailure
8、TLAB
8.1、TLAB介紹
Thread Local Allocation Buffer
為什么需要TLAB?
-
堆區是執行緒共享區域,任何執行緒都可以訪問到堆區中的共享資料
-
由于物件實體的創建在JVM中非常頻繁,因此在并發環境下從堆區中劃分記憶體空間是執行緒不安全的
-
為避免多個執行緒操作同一地址,需要使用加鎖等機制,進而影響分配速度,
什么是TLAB?
- 從記憶體模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM為每個執行緒分配了一個私有快取區域,它包含在Eden空間內,
- 多執行緒同時分配記憶體時,使用TLAB可以避免一系列的非執行緒安全問題,同時還能夠提升記憶體分配的吞吐量,因此我們可以將這種記憶體分配方式稱之為快速分配策略,
- 據我所知所有openJDK衍生出來的JVM都提供了TLAB的設計,
TLAB的再說明:
- 盡管不是所有的物件實體都能夠在TLAB中成功分配記憶體,但JVM確實是將TLAB作為記憶體分配的首選,
- 在程式中,開發人員可以通過選項“-XX:UseTLAB”設定是否開啟TLAB空間,
- 默認情況下,TLAB空間的記憶體非常小,僅占有整個Eden空間的1號,可以通過選項
-XX:TLABWasteTargetPercent設定TLAB空間所占用Eden空間的百分比大小, - 一旦物件在TLAB空間分配記憶體失敗時,JVM就會嘗試著通過使用加鎖機制確保資料操
作的原子性,從而直接在Eden空間中分配記憶體,
8.2、分配程序

9、堆空間的引數設定
- -XX:+PrintFlagsInitial:查看所有的引數的默認初始值
- -XX:+PrintFlagsFinal:查看所有的引數的最終值(可能會存在修改,不再是初始值)
- -Xms:初始堆空間記憶體(默認為物理記憶體的1/64)
- -Xmx:最大堆空間記憶體(默認為物理記憶體的1/4)
- -Xmn:設定新生代的大小,(初始值及最大值)
- -XX:NewRatio:配置新生代與老年代在堆結構的占比
- -XX:SurvivorRatio:設定新生代中Eden和s0/s1空間的比例
- -XX:MaxTenuringThreshold:設定新生代垃圾的最大年齡
- -XX:+PrintGCDetails:輸出詳細的GC處理日志
列印gc簡要資訊:1. -XX:+PrintGC 2. -verbose:gc - -XX:HandlePromotionFailure:是否設定空間分配擔保
查看某個引數的值
- 命令列:
- jps查看行程id
- jinfo -flag 引數名稱(SurvivorRatio) 行程id
空間分配擔保
在發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大于新生代所有物件的總空間,
-
如果大于,則此次Minor GC是安全的
-
如果小于,則虛擬機會查看-XX:HandlePromotionFailure設定值是否允許擔保失敗,
- 如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代的物件的平均大小,
- 如果大于,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;
- 如果小于,則改為進行一次Full GC,
- 如果HandlePromotiorfFailure=false,則改為進行一次Full GC,
- 如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代的物件的平均大小,
在JDK6 Update24之后(JDK7),HandlePromotionFailure引數不會再影響到虛擬機的空間分配擔保策略,觀察OpenJDK中的原始碼變化,雖然原始碼中還定義了HandlePromotionFailure引數,但是在代碼中已經不會再使用它,JDK6 Update24之后的規則變為只要老年代的連續空間大于新生代物件總大小或者歷次晉升的平均大
小就會進行Minor GC,否則將進行Full GC,
10、堆是物件分配記憶體的唯一選擇?
在《深入理解Java虛擬機》中關于Java堆記憶體有這樣一段描述:隨著JIT編譯期的發展與逃逸分析技術逐漸成熟,堆疊上分配、標量替換優化技術將會導致一些微妙的變化,所有的物件都分配到堆上也漸漸變得不那么“絕對”了,
在Java虛擬機中,物件是在Java堆中分配記憶體的,這是一個普遍的常識,但是,有一種特殊情況,那就是如果經過逃逸分析(Escape Analysis)后發現,一個物件并沒有逃逸出方法的話,那么就可能被優化成堆疊上分配,這樣就無需在堆上分配記憶體,也無須進行垃圾回收了,這也是最常見的堆外存盤技術,
10.1、逃逸分析
- 如何將堆上的物件分配到堆疊,需要使用逃逸分析手段,
- 這是一種可以有效減少Java程式中同步負載和記憶體堆分配壓力的跨函式全域資料流分析演算法,
- 通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的物件的參考的使用范圍從而決定是否要將這個物件分配到堆上,
- 逃逸分析的基本行為就是分析物件動態作用域:
- 當一個物件在方法中被定義后,物件只在方法內部使用,則認為沒有發生逃逸,
- 當一個物件在方法中被定義后,它被外部方法所參考,則認為發生逃逸,例如作為呼叫引數傳遞到其他地方中,
引數設定:
在JDK 6u23版本之后,HotSpot中默認就已經開啟了逃逸分析,如果使用的是較早的版本,則可以通過:
-
選項“-XX:+DoEscapeAnalysis"顯式開啟逃逸分析
-
通過選項“-XX:+PrintEscapeAnalysis"查看逃逸分析的篩選結果,
結論:
能使用區域變數的,就不要在方法外部定義
10.2、優化:
1. 堆疊上分配
將堆分配轉化為堆疊分配,如果一個物件在子程式中被分配,要使指向該物件的指標永遠不會逃逸,物件可能是堆疊分配的候選,而不是堆分配,
2. 同步省略
如果一個物件被發現只能從一個執行緒被訪問到,那么對于這個物件的操作可以不考慮同步,
3. 分離物件/標量替換
有的物件可能不需要作為一個連續的記憶體結構在也可以被訪問到,那么物件的部分(或全部)可以不存盤在記憶體,而是存盤在CPU暫存器中,
標量(scalar)是指一個無法再分解成更小的資料的資料,Java中的原始資料型別就是相對的,那些還可以分解的資料叫做聚合量(Aggregate),Java中的物件就是聚合量,因為他可以分解成其他聚合量和標量,
在JIT階段,如果經過逃逸分析,發現一個物件不會被外界訪問的話,那么經過JIT優化,就會把這個物件拆解成若干個其中包含的若干個成員變數來代替,這個程序就是標量替換,
標量替換引數設定:
- 引數-XX:+EliminateAllocations:開啟了標量替換(默認打開),允許將物件打散分配在堆疊上,
10.3、逃逸分析缺點
無法保證逃逸分析的性能消耗一定能高于他的消耗,雖然經過逃逸分析可以做標量替換、堆疊上分配、和鎖消除,但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的程序,
一個極端的例子,就是經過逃逸分析之后,發現沒有一個物件是不逃逸的,那這個逃逸分析的程序就白白浪費掉了,
11、小結
- 年輕代是物件的誕生、成長、消亡的區域,一個物件在這里產生、應用,最后被垃圾回收器收集、結束生命,|
- 老年代放置長生命周期的物件,通常都是從Survivor區域篩選拷貝過來的Java物件,當然,也有特殊情況,我們知道普通的物件會被分配在TLAB上;如果物件較大,JVM會試圖直接分配在Eden其他位置上;如果物件太大,完全無法在新生代找到足夠長的連續空閑空間,JVM就會直接分配到老年代,
- 當GC只發生在年輕代中,回收年輕代物件的行為被稱為MinorGC,當GC發生在老年代時則被稱為MajorGC或者FullGC,一般的,MinorGC的發生頻率要比MajorGC高很多,即老年代中垃圾回收發生的頻率將大大低于年輕代,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/160765.html
標籤:Java
上一篇:JVM--虛擬機堆疊
下一篇:SpringBoot + Vue + ElementUI 實作后臺管理系統模板 -- 后端篇(二): 整合 Redis(常用工具類、快取)、整合郵件發送功能
