堆
堆的核心概述
堆與進程
(1)堆針對一個JVM進程來說是唯一的,也就是一個進程只有一個JVM實例,一個JVM示例也就只有只有一個運行時數據區,一個運行時數據區只有一個堆和一個方法區
(2)但是進程中包含多個線程,他們是共享一個堆空間的

(1)一個JVM實例只存在一個堆內存,堆也是Java內存管理的核心區域。
(2)Java堆區在JVM啟動的時候即被創建,其空間大小也就確定了,堆是JVM管理的最大一塊內存空間,并且堆內存的大小是可以調節的
(3)《Java虛擬機規范》規定,堆可以處于物理上不連續的內存空間中,但在邏輯上它應該被視為連續的
(4)所有的線程共享Java堆,在這里還可以劃分線程私有的緩沖區(Thread Local Allocation Buffer,TLAB)
(5)《Java虛擬機規范》中對Java堆的描述是:所有的對象實例以及數組都應當在運行時分配在堆上。
從實際使用角度看:“幾乎”所有的對象實例都在堆分配內存,但并非全部。因為還有一些對象是在棧上分配的(逃逸分析,標量替換)
(6)數組和對象可能永遠不會存儲在棧上(不一定),因為棧幀中保存引用,這個引用指向對象或者數組在堆中的位置
(7)在方法結束后,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除
也就是觸發了GC的時候,才會進行回收
如果堆中對象馬上被回收,那么用戶線程就會收到影響,因為有stop the word
(8)堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域
public class SimpleHeap { private int id;//屬性、成員變量 public SimpleHeap(int id) { this.id = id; } public void show() { System.out.println("My ID is " + id); } public static void main(String[] args) { SimpleHeap sl = new SimpleHeap(1); SimpleHeap s2 = new SimpleHeap(2); int[] arr = new int[10]; Object[] arr1 = new Object[10]; } }

堆內存細分
現代垃圾收集器大部分都基于分代收集理論設計,堆空間細分為:
(1)Java7 及之前堆內存邏輯上分為三部分:新生區+養老區+永久區
Young Generation Space 新生區 Young/New
又被劃分為Eden區和Survivor區
Old generation space 養老區 Old/Tenure
Permanent Space 永久區 Perm
(2)Java 8及之后堆內存邏輯上分為三部分:新生區+養老區+元空間
Young Generation Space 新生區,又被劃分為Eden區和Survivor區
Old generation space 養老區
Meta Space 元空間 Meta
約定:新生區 <–> 新生代 <–> 年輕代 、 養老區 <–> 老年區 <–> 老年代、 永久區 <–> 永久代

堆空間內部結構,JDK1.8之后從 永久代 替換成 元空間

JVisualVM可視化查看堆內存
運行下面代碼
public class HeapDemo { public static void main(String[] args) { System.out.println("start..."); try { TimeUnit.MINUTES.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end..."); } }
使用JVisualVM工具

設置堆內存大小與OOM
設置堆內存
(1)Java堆區用于存儲Java對象實例,那么堆的大小在JVM啟動時就已經設定好了,大家可以通過選項”-Xms”和”-Xmx”來進行設置。
-Xms 用于表示堆區的起始內存,等價于**-XX:InitialHeapSize**
-Xmx 用于表示堆區的最大內存,等價于**-XX:MaxHeapSize**
(2)一旦堆區的內存超過 -Xmx 所指定的最大內存時,就會拋出OutofMemoryError異常
(3)通常會將-Xms和-Xmx兩個參數配置相同的值
原因:假設兩個不一樣,初始內存小,最大內存大。在運行期間如果堆內存不夠用了,會一直擴容直到最大內存。如果內存夠用且多了,也會不斷的縮容釋放。頻繁的擴容和釋放造成不必要的壓力,避免在GC之后調整堆內存給服務器帶來壓力.
如果兩個設置一樣的就少了頻繁擴容和縮容的步驟。內存不夠了就直接報OOM
(4)默認情況下:
初始內存大小:物理內存大小/64
最大內存大小:物理內存大小/4
/** * 1. 設置堆空間大小的參數 * -Xms 用來設置堆空間(年輕代+老年代)的初始內存大小 * -X 是jvm的運行參數 * ms 是memory start * -Xmx 用來設置堆空間(年輕代+老年代)的最大內存大小 * * 2. 默認堆空間的大小 * 初始內存大小:物理電腦內存大小 / 64 * 最大內存大小:物理電腦內存大小 / 4 * 3. 手動設置:-Xms600m -Xmx600m * 開發中建議將初始堆內存和最大的堆內存設置成相同的值。 * * 4. 查看設置的參數:方式一: jps / jstat -gc 進程id * 方式二:-XX:+PrintGCDetails */ public class HeapSpaceInitial { public static void main(String[] args) { //返回Java虛擬機中的堆內存總量 long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; //返回Java虛擬機試圖使用的最大堆內存量 long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; System.out.println("-Xms : " + initialMemory + "M"); System.out.println("-Xmx : " + maxMemory + "M"); System.out.println("系統內存大小為:" + initialMemory * 64.0 / 1024 + "G"); System.out.println("系統內存大小為:" + maxMemory * 4.0 / 1024 + "G"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
輸出結果:
-Xms : 489M -Xmx : 7241M 系統內存大小為:30.5625G 系統內存大小為:28.28515625G
在內存32G的電腦,不足32G時操作系統自身占據
設置參數

public class HeapSpaceInitial { public static void main(String[] args) { //返回Java虛擬機中的堆內存總量 long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; //返回Java虛擬機試圖使用的最大堆內存量 long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; System.out.println("-Xms : " + initialMemory + "M"); System.out.println("-Xmx : " + maxMemory + "M"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
-Xms : 575M
-Xmx : 575M
為什么會少25M
方式一:jps / jstat -gc 進程id

jps:查看java進程;jstat:查看某進程內存使用情況
SOC: S0區總共容量
S1C: S1區總共容量
S0U: S0區使用的量
S1U: S1區使用的量
EC: 伊甸園區總共容量
EU: 伊甸園區使用的量
OC: 老年代總共容量
OU: 老年代使用的量
(1)25600+25600+153600+409600 = 614400K
614400 /1024 = 600M
(2)25600+153600+409600 = 588800K
588800 /1024 = 575M
(3)數據并非巧合,S0和S1區只有一個能使用,另一個用不了(后面會詳解)
方式二:添加參數 -XX:+PrintGCDetails

OOM
public class OOMTest { public static void main(String[] args) { ArrayList<Picture> list = new ArrayList<>(); while(true){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } list.add(new Picture(new Random().nextInt(1024 * 1024))); } } } class Picture{ private byte[] pixels; public Picture(int length) { this.pixels = new byte[length]; } }
(1)設置虛擬機參數
-Xms600m -Xmx600m
最終輸出結果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.example.demo.heaptest.Picture.<init>(OOMTest.java:24) at com.example.demo.heaptest.OOMTest.main(OOMTest.java:15)
(2)堆內存變化

(3)原因:大對象導致內存溢出

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


配置新生代與老年代在堆結構的占比:
默認**-XX:NewRatio**=2,表示新生代占1,老年代占2,新生代占整個堆的1/3;
可以修改**-XX:NewRatio**=4,表示新生代占1,老年代占4,新生代占整個堆的1/5
(1)在HotSpot中,Eden空間和另外兩個suvivor空間缺省所占的比例是8:1:1;
(2)當然開發人員可以通過**-XX:SurvivorRatio**調整這個空間比例,比如-XX:SurvivorRatio=8
(3)幾乎所有的Java對象都是在Eden區被new出來的
(4)絕大部分的Java對象的銷毀都在新生代進行了(有些大的對象在Eden區無法存儲時,將直接進入老年代),IBM公司的專門研究表明,新生代中80%的對象都是“朝生夕死”的
(5)可以使用選項”-Xmn“設置新生代最大內存大小,但是這個參數一般都是使用默認值了。

/** * -Xms600m -Xmx600m * * -XX:NewRatio : 設置新生代與老年代的比例。默認值是2. * -XX:SurvivorRatio :設置新生代中Eden區與Survivor區的比例。默認值是8 * -XX:-UseAdaptiveSizePolicy :關閉自適應的內存分配策略 (暫時用不到) * -Xmn:設置新生代的空間的大小。 (一般不設置) * * @author shkstart shkstart@126.com * @create 2020 17:23 */ public class EdenSurvivorTest { public static void main(String[] args) { System.out.println("我只是來打個醬油~"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
圖解對象分配過程
為新對象分配內存是一件非常嚴謹和復雜的任務,JVM的設計者不僅需要考慮內存如何分配,在哪里分配等問題,并且由于內存分配算法與內存回收算法密切相關,所以還需要考慮GC執行完內存回收后是否會在內存空間中產生內存碎片
具體過程
(1)new的對象先放入伊甸園區。此區的大小有限制
(2)當伊甸園區空間填滿時,程序又需要創建對象,JVM的垃圾回收器將會對伊甸園區進行垃圾回收(MinorGC),將伊甸園區中不再被其他對象所引用的對象進行銷毀,在加載新的對象到伊甸園區
(3)然后及那個伊甸園區剩余的對象移動到幸存者0區
(4)如果再次觸發垃圾回收,此時上次幸存下來放到幸存者0區的,如果沒有被回收,就會被放入幸存者1區
(5)如果再次經歷垃圾回收,此時會重新放入幸存者0區,接著再去幸存者1區
(6)啥時候能去養老區呢?可以設置次數,默認是16次。可以設置新生代進入養老區的年齡限制,設置JVM參數:**-XX:MaxTenuringThreshold**=N 進行設置
(7)在養老區,相對悠閑。當養老區內存不足時,再次觸發GC:Major GC,進行養老區的內存清理
(8)若養老區執行了Major GC之后,發現依然無法進行對象的保存,就會產生OOM異常
圖解對象分配(一般情況)
(1)我們創建的對象,一般都是存放在Eden區的,當我們Eden區滿了之后,就會觸發GC操作,一般被稱為YGC / Minor GC操作

(2)當我們進行一次垃圾回收后,紅色的對象就會被回收,而綠色的對象還被占用,就會存放人S0(suvivor From)區,同時我們給每個對象設置了一個年齡計數器,經過一次回收后還存在的對象,將其年齡加1
(3)同時Eden區繼續存放對象,當Eden區再次存放滿時,就會觸發一個MinorGC操作,此時GC將會把Eden和Survivo From中的對象進行一次垃圾回收,把存活的對象放到Survivor To(S1)區,同時讓存活的對象年齡加1
下一次再進行GC的時候
a. 這一次的s0區為空,所以稱為下一次GC的to區
b. 這一次的s1區則稱為下一次GC的from區
c. 也就是說s0區和s1區在互相轉換

(4)我們繼續不斷的進行對象生成和垃圾回收,當Survivor中的對象的年齡達到15時,將會觸發一次Promotion(晉升)的操作,也就是年輕代中的對象晉升到老年代中

關于垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不再永久代/元空間收集
特色情況說明
對象分配的特殊情況
(1)如果來了一個新對象,先看看Eden區是否放的下?
如果Eden區能放下,則直接放到Eden區
如果Eden區放不下,則會觸發YGC,執行垃圾回收,看看能不能放下?
(2)將對象放到老年區又有兩種情況:
如果Eden執行了YGC還是無法放下該對象,那就沒得辦法,只能說明這是超大對象,只能直接放到老年代
那萬一老年代都放不下,則會觸發FullGC,再看看能不能放下,放的下最好,但如果還是放不下,則只能報OOM
(3)如果Eden區滿了,將對象往幸存者區拷貝時,發現幸存者放不下,就只能直接放入老年代了

常用調優工具:
JDK命令行,Eclipse:Memory Analyzer Tool,Jconsole,Visual VM(實時監控,推薦),Jprofiler(IDEA插件),Java Flight Recorder(實時監控),GCViewer,GCEasy
GC分類
(1)我們都知道,JVM調優的一個環節,就是垃圾收集,我們要盡量避免垃圾回收,因為垃圾回收的過程中,容易出現STW(stop the world)的問題,而Major GC和Full GC出現STW的時間,是Minor GC的10倍以上
(2)JVM在進行GC時,并非每次都對上面三個內存區域一起回收的,大部分時候回收的都是新生代。針對Hotspot VM的實現,它里面的GC按照回收區域又分為兩大類型:一種是部分收集(Partial GC),一種是整堆收集(FullGC)
部分收集:不是完整收集整個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堆和方法區的垃圾收集
由于歷史原因,外界各種解讀,major GC和Full GC有些混淆
Young GC
年輕代GC(Minor GC)的觸發機制
(1)當年輕代空間不足時,就會觸發Minor GC,這里的年輕代滿指的是Eden代滿。Suvivor滿不會主動引發GC,在Eden區滿的時候,就順帶觸發s0區的GC,也就是被動觸發GC(每次Minor GC會清理年輕代的內存)
(2)因為Java對象大多具備朝生夕死的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
(3)Minor GC會引發STW(stop the world),暫停其他用戶線程,等垃圾回收結束,用戶線程才恢復運行

Major/Full GC
Major / Full GC有爭議,后續詳解兩者區別,暫時先看著
老年代GC(MajorGC)觸發機制
(1)指發生在老年代的GC,對象從老年代消失時,我們說“Major GC”或“Full GC”發生了
(2)出現了Major GC,經常會伴隨著至少一次的Minor GC。(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行MajorGC的策略選擇過程)
也就是在老年代空間不足時,會先嘗試觸發Minor GC(哈?我有點迷?),如果之后空間還不足,則觸發Major GC
(3)Major GC的速度一般會比Minor GC慢10倍以上,STW的時間更長
(4)如果Major GC后,內存還不足,就報OOM了
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是開發或調優中盡量要避免的。這樣STW時間會短一些
GC日志分析
public class GCTest { public static void main(String[] args) { int i = 0; try { List<String> list = new ArrayList<>(); String a = "hello world"; while (true){ list.add(a); a = a + a; i ++; } }catch (Throwable e){ e.printStackTrace(); System.out.println("遍歷次數為:" + i); } } }
[GC (Allocation Failure) [PSYoungGen: 2038K->508K(2560K)] 2038K->935K(9728K), 0.0017267 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2504K->512K(2560K)] 2932K->2408K(9728K), 0.0004281 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 1973K->512K(2560K)] 3870K->3168K(9728K), 0.0003883 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 1328K->0K(2560K)] [ParOldGen: 6880K->4927K(7168K)] 8209K->4927K(9728K), [Metaspace: 3273K->3273K(1056768K)], 0.0051197 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 4927K->4927K(9728K), 0.0001976 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 4927K->4909K(7168K)] 4927K->4909K(9728K), [Metaspace: 3273K->3273K(1056768K)], 0.0029506 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 遍歷次數為:16 Heap PSYoungGen total 2560K, used 123K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000) eden space 2048K, 6% used [0x00000000ffd00000,0x00000000ffd1ef00,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen total 7168K, used 4909K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000) object space 7168K, 68% used [0x00000000ff600000,0x00000000ffacb5c8,0x00000000ffd00000) Metaspace used 3343K, capacity 4496K, committed 4864K, reserved 1056768K class space used 364K, capacity 388K, committed 512K, reserved 1048576K java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3332) at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448) at java.lang.StringBuilder.append(StringBuilder.java:142) at com.example.demo.heaptest.GCTest.main(GCTest.java:16)
[PSYoungGen: 2038K->508K(2560K)] :年輕代總空間2560k,當前占用2038k,經過垃圾收集后剩余508k;
2038K->935K(9728K):堆內存總空間為9728k,當前占用空間2038k,經過垃圾收集后剩余935k;
堆空間分帶思想
為什么要把Java堆分代?不分帶就不能正常工作了嗎?經研究,不同對象的生命周期不同,70%-99%的對象是臨時對象。
新生代:有Eden,兩塊大小相同的survivor(又稱為from/to或s0/s1)構成,to總為空。
老年代:存放新生代中經歷多次GC仍然存活的對象

其實不分代完全可以,分代的唯一理由就是優化GC性能。
如果沒有分代,那所有的對象都在一起,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒有用,這樣就會對堆的所有區域都進行掃描(性能低)
而很多對象都是朝生夕死的,如果分代的話,把新創建的對象放在某一個地方,當GC的時候先把這塊存儲對象進行回收,這樣就會騰出很大的空間(多回收新生代,少回收老年代,性能會提高很多)

對象內存分配策略
(1)如果對象在Eden出生并經過第一次Minor GC后仍然存活,并且能被Survivot容納的話,將被移動到Survivor空間中,并將對象年齡設為1
(2)對象在Survivor區中每熬過一次MinorGC,年齡增加1歲,當它的年齡增加到一定的程度(默認15歲,每個JVM,每個GC都有所不同),就會被晉升到老年代
(3)對象晉升老年代的年齡閾值,可以通過選項**-XX:MaxTenuringThreshold**來設置
針對不同年齡段的對象分配原則如下:
(1)優先分配到eden:開發中比較長的字符串或數值,會直接存在老年代,但是因為新創建的對象都是朝生夕死的,所以這個大對象可能也很快被回收,但是因為老年代觸發Major GC的次數比 Minor GC要更少,因此可能回收起來就會比較慢
(2)大對象直接分配到老年代:盡量避免程序中出現過多的大對象
(3)長期存活的對象分配到老年代
(4)動態對象年齡判斷:如果Survivor區中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡
(5)空間分配擔保:-XX:HandlePromotionFailure
TLAB為對象分配內存(保證線程安全)
為什么有TLAB
(1)堆區是線程共享區域,任何線程都可以訪問到堆區中的共享數據
(2)由于對象實例的創建在JVM中非常頻繁,因此在并發環境下從堆區劃分內存空間是線程不安全的
(3)為避免多個線程操作同一地址,需要使用加鎖等機制,進而影響分配速度;
什么是TLAB
TLAB(Thread Local Allocation Buffer)
(1)從內存模型而不是垃圾收集的角度,對Eden區域進行劃分,JVM為每個線程分配了一個私有緩存區域,它包含在Eden空間內;
(2)多線程同時分配內存時,使用TLAB可以避免非線程安全問題,同時還能提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱之為快速分配策略
(3)所有OpenJDK衍生出來的JVM都提供了TLAB的設計

(4)每個線程都有一個TLAB空間
(5)當一個線程的TLAB滿時,可以使用公共區域(藍色)的
TLAB再說明
(1)盡管不是所有的對象實例都能夠在TLAB中成功分配內存,但JVM確實是將TLAB作為內存分配的首選
(2)程序中,開發人員可以通過選項“**-XX:UseTLAB**”設置是否開啟TLAB空間
(3)默認情況下,TLAB空間的內存非常小,僅占有整個Eden空間的1%,當然我們可以通過選項“**-XX:TLABWasteTargetPercent**”設置TLAB空間所占用Eden空間的百分比大小
(4)一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試著通過使用加鎖機制確保數據操作的原子性,從而直接在Eden空間中分配內存
TLAB分配過程

堆空間參數設置
常用參數設置
官方文檔:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
/** * 測試堆空間常用的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:MaxTenuringThreshold:設置新生代垃圾的最大年齡 * -XX:+PrintGCDetails:輸出詳細的GC處理日志 * 打印gc簡要信息:① -XX:+PrintGC ② -verbose:gc * -XX:HandlePromotionFailure:是否設置空間分配擔保 */
空間分配擔保
(1)在發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大于新生代所有對象的總空間
如果大于,則此次Minor GC是安全的
如果小于,則虛擬機會查看**-XX:HandlePromotionFailure**設置值是否允擔保失敗。
如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代的對象的平均大小
如果大于,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的
如果小于,則進行一次Full GC
如果HandlePromotionFailure=false,則進行一次Full GC
歷史版本
(1)在JDK6 Update 24之后,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察openJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,但是在代碼中已經不會再使用它
(2)JDK6 Update 24之后的規則變為只要老年代的連續空間大于新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。即 HandlePromotionFailure=true
堆是分配對象的唯一選擇么?
在《深入理解Java虛擬機》中關于Java堆內存有這樣一段描述
(1)隨著JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么“絕對”了
(2)在Java虛擬機中,對象是在Java堆中分配內存的,這是一個普遍的常識,但是有一種情況,就是如果經過逃逸分析后,一個對象并沒有逃逸出方法的話,那么就可能被優化成棧上分配了。這樣就無需在堆上分配內存,也無需進行垃圾回收了,這也是常見的對外存儲技術
(3)前面提到的基于OpenJDK深度定制的TaoBao VM,其中創新的GCIH(GC invisible heap)技術實現off-heap,將生命周期較長的Java對象從heap中移至heap外,并且GC不能管理GCIH內部的Java對象,以此達到降低GC的回收頻率和提升GC的回收效率的目的
逃逸分析:
(1)如何將堆上的對象分配到棧,需要使用逃逸分析手段
(2)一種可以有效減少Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法
(3)過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上
(4)逸分析的基本行為就是分析對象動態作用域
當一個對象在方法中被定義后,對象只在方法內部使用,則認為沒有發生逃逸;
當一個對象在方法中被定義后,它被外部方法所引用,則認為發生逃逸。例如作為調用參數傳遞到其他地方中
逃逸分析示例:
1. 沒有發生逃逸的對象,則可以分配到棧(無線程安全問題)上,隨著方法執行結果的結束,棧空間就被移除(也就無需GC)
public void my_method() { V v = new V(); // use v // .... v = null; }
2. 下面的代碼中StringBuffer sb 發生了逃逸,不能在棧上分配
public static StringBuffer createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; }
3. 如果想要StringBuffer sb不發生逃逸,可以這樣寫
public static String createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
/** * 逃逸分析 * * 如何快速的判斷是否發生了逃逸分析,大家就看new的對象實體是否有可能在方法外被調用。 */ public class EscapeAnalysis { public EscapeAnalysis obj; /* 方法返回EscapeAnalysis對象,發生逃逸 */ public EscapeAnalysis getInstance(){ return obj == null? new EscapeAnalysis() : obj; } /* 為成員屬性賦值,發生逃逸 */ public void setObj(){ this.obj = new EscapeAnalysis(); } //思考:如果當前的obj引用聲明為static的?仍然會發生逃逸。 /* 對象的作用域僅在當前方法中有效,沒有發生逃逸 */ public void useEscapeAnalysis(){ EscapeAnalysis e = new EscapeAnalysis(); } /* 引用成員變量的值,發生逃逸 */ public void useEscapeAnalysis1(){ EscapeAnalysis e = getInstance(); //getInstance().xxx()同樣會發生逃逸 } }
逃逸分析參數設置
(1)在JDK1.7版本之后,HotSpot中默認就開啟了逃逸分析
(2)如果使用的較早版本,開發人員則可以通過:
選項“-XX:+DoEscapeAnalysis”顯式開啟逃逸分析
通過選項“-XX:+PrintEscapeAnalysis”查看逃逸分析的篩選結果
總結:
開發中能使用局部變量的,就不要使用在方法外定義
代碼優化
使用逃逸分析,編譯器可以對代碼做如下優化:
(1)棧上分配:將堆分配轉化為棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會發生逃逸,對象可能是棧上分配的候選,而不是堆上分配
(2)同步省略:如果一個對象被發現只有一個線程被訪問到,那么對于這個對象的操作可以不考慮同步
(3)分離對象或標量替換:有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存,而是存儲在CPU寄存器中。
棧上分配
(1)JIT編譯器在編譯期間根據逃逸分析的結果,發現如果一個對象并沒有逃逸出方法的話,就可能被優化成棧上分配。分配完成后,繼續在調用棧內執行,最后線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收
(2)常見的棧上分配的場景:在逃逸分析中,已經說明了,分別是給成員變量賦值、方法返回值、實例引用傳遞
棧上分配舉例:
/** * 棧上分配測試 * -Xmx128m -Xms128m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails */ public class StackAllocation { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 10000000; i++) { alloc(); } // 查看執行時間 long end = System.currentTimeMillis(); System.out.println("花費的時間為: " + (end - start) + " ms"); // 為了方便查看堆內存中對象個數,線程sleep try { Thread.sleep(1000000); } catch (InterruptedException e1) { e1.printStackTrace(); } } private static void alloc() { User user = new User();//未發生逃逸 } static class User { } }
輸出結果:
[GC (Allocation Failure) [PSYoungGen: 33280K->1032K(38400K)] 33280K->1040K(125952K), 0.0005586 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 34312K->936K(38400K)] 34320K->944K(125952K), 0.0004612 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 34216K->904K(38400K)] 34224K->912K(125952K), 0.0004087 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 34184K->904K(38400K)] 34192K->912K(125952K), 0.0006141 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 花費的時間為: 23 ms
1. JVM參數設置
-Xmx128m -Xms128m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
2. 日志打印:發生了GC,耗時114ms
開啟逃逸分析的情況
輸出結果
花費的時間為: 3 ms
1. 參數設置
-Xmx128m -Xms128m -XX:+PrintGCDetails
2. 日志打印:并沒有發生GC,耗時3ms
同步省略(同步消除)
(1)線程同步的代價是相當高的,同步的后果是降低并發性和性能
(2)在動態編譯同步塊的時候,JIT編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發布到其他線程
(3)如果沒有,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高并發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除
例如下面的代碼
public void f() { Object hollis = new Object(); synchronized(hollis) { System.out.println(hollis); } }
代碼中對hollis這個對象加鎖,但是hollis對象的生命周期只在f()方法中,并不會被其他線程所訪問到,所以在JIT編譯階段就會被優化掉,優化成:
public void f() { Object hellis = new Object(); System.out.println(hellis); }
字節碼分析:
public class SynchronizedTest { public void f() { Object hollis = new Object(); synchronized(hollis) { System.out.println(hollis); } } }
0 new #2 <java/lang/Object> 3 dup 4 invokespecial #1 <java/lang/Object.<init>> 7 astore_1 8 aload_1 9 dup 10 astore_2 11 monitorenter 12 getstatic #3 <java/lang/System.out> 15 aload_1 16 invokevirtual #4 <java/io/PrintStream.println> 19 aload_2 20 monitorexit 21 goto 29 (+8) 24 astore_3 25 aload_2 26 monitorexit 27 aload_3 28 athrow 29 return
注意:字節碼文件中并沒有進行優化,可以看到加鎖和釋放鎖的操作依然存在,同步省略操作是在解釋運行時發生的
標量替換
分離對象或標量替換
(1)標量(scalar)是指一個無法再分解成更小的數據的數據。Java中的原始數據類型就是標量
(2)相對的,那些還可以分解的數據叫做聚合量(Aggregate),Java中的對象就是聚合量,因為他可以分解成其他聚合量和標量
(3)在JIT階段,如果經過逃逸分析,發現一個對象不會被外界訪問的話,那么經過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換
標量替換舉例
public static void main(String args[]) { alloc(); } private static void alloc() { Point point = new Point(1,2); System.out.println("point.x" + point.x + ";point.y" + point.y); } class Point { private int x; private int y; }
以上代碼,經過標量替換后,就會變成
private static void alloc() { int x = 1; int y = 2; System.out.println("point.x = " + x + "; point.y=" + y); }
(1)可以看到,Point這個聚合量經過逃逸分析后,發現他并沒有逃逸,就被替換成兩個聚合量了
(2)那么標量替換有什么好處呢?就是可以大大減少堆內存的占用。因為一旦不需要創建對象了,那么就不再需要分配堆內存了
(3)標量替換為棧上分配提供了很好的基礎
標量替換參數設置
參數 -XX:+ElimilnateAllocations:開啟了標量替換(默認打開),允許將對象打散分配在棧上。
代碼示例:
/** * 標量替換測試 * -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations * @author shkstart shkstart@126.com * @create 2020 12:01 */ public class ScalarReplace { public static class User { public int id; public String name; } public static void alloc() { User u = new User();//未發生逃逸 u.id = 5; u.name = "www.atguigu.com"; } public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 10000000; i++) { alloc(); } long end = System.currentTimeMillis(); System.out.println("花費的時間為: " + (end - start) + " ms"); } }
未開啟標量替換
(1)JVM參數
-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
(2)日志
[GC (Allocation Failure) 25600K->1040K(98304K), 0.0005690 secs] [GC (Allocation Failure) 26640K->952K(98304K), 0.0005886 secs] [GC (Allocation Failure) 26552K->920K(98304K), 0.0004698 secs] [GC (Allocation Failure) 26520K->928K(98304K), 0.0004406 secs] [GC (Allocation Failure) 26528K->1008K(98304K), 0.0004800 secs] [GC (Allocation Failure) 26608K->960K(100864K), 0.0004743 secs] [GC (Allocation Failure) 31680K->790K(100864K), 0.0005915 secs] [GC (Allocation Failure) 31510K->790K(100864K), 0.0002275 secs] 花費的時間為: 31 ms
開啟標量替換
(1)JVM參數
-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
(2)日志:時間減少很多,且無GC
花費的時間為: 2 ms
上述代碼在主函數中調用了一億次alloc()方法,進行對象創建由于User對象實例需要占16字節空間,因此累計分配空間達到將近1.5GB。如果堆空間小于這個值,就必然會發生GC,使用如下參數運行上述代碼:
-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
這里的參數如下:
參數-server:啟用Server模式,因為在server模式下,才可以啟用逃逸分析。
參數-XX:+DoEscapeAnalysis:啟用逃逸分析
參數-Xmx10m:指定堆空間最大為10MB
參數-XX:+PrintGC 打印GC日志
參數 -XX:+EliminateAllocations:開啟了標量替換(默認打開),允許將對象打散分配在棧上,比如對象擁有id和name兩個字段,那么這兩個字段將會被視為兩個獨立的局部變量進行分配。
逃逸分析的不足
(1)關于逃逸分析的論文在1999年就已經發表了,但直到JDK1.6才有實現,而且這項技術到如今也并不是十分成熟的
(2)其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的過程
(3)一個極端的例子,就是經過逃逸分析之后,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了
(4)雖然這項技術并不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段
(5)注意到有一些觀點,認為通過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,但是取決于JVM設計者的選擇。據我所知,Oracle Hotspot JVM中并未這么做(剛剛演示的效果,是因為HotSpot實現了標量替換),這一點在逃逸分析相關的文檔里已經說明,所以可以明確在HotSpot虛擬機上,所有的對象實例都是創建在堆上
(6)目前很多書籍還是基于JDK7以前的版本,JDK已經發生了很大變化,intern字符串的緩存和靜態變量曾經都被分配在永久代上,而永久代已經被元數據區取代。但是intern字符串緩存和靜態變量并不是被轉移到元數據區,而是直接在堆上分配,所以這一點同樣符合前面一點的結論:對象實例都是分配在堆上。
堆是分配對象的唯一選擇么?小結
(1)年輕代是對象的誕生,成長,消亡的區域,一個對象在這里產生,應用,最后被垃圾回收器收集,結束生命。
(2)老年代放置長生命周期的對象,通常都是從Survivo區域刷選拷貝過來的Java對象
(3)當然,也有特色的情況,我們知道普通的對象可能會被分配在TLAB上;
(4)如果對象較大,無法分配在TLAB上,則JVM會試圖直接分配在Eden其他位置上;
(5)如果對象太大,完全無法在新生代找到足夠長的連續空閑空間,JVM就會直接分配到老年代;
(6)當GC只發生在年輕代,回收年輕代對象的行為稱為Minor GC;
(7)當GC發生在老年代則被稱為Major GC或Full GC
(8)一般的,Minor GC的發生頻率要比Major GC高很多,即老年代中垃圾回收發生的頻率將大大低于年輕代
浙公網安備 33010602011771號