本文主要整理自煉術成金JVM教學資料和《深入理解Java虛擬機器》,部分資料整理自網路,已不明來源
一. JVM規範
1.1 位運算
1.1.1 整型int
- 原碼:第一位符號位,0為正,1為負
- 反碼:符號位不動,原碼取反
- 補碼
- 正數補碼:和原始碼相同
- 負數補碼:符號位不動,反碼加1
example
-6
原碼: 10000110
反碼: 11111001
補碼: 11111010
複製程式碼
- 為何使用補碼
- 可以無歧義地表示0
不使用補碼,將0看為
正數:0000 0000
負數:1000 0000
複製程式碼
則不一致
使用補碼:
負數:1000 0000
反碼:1111 111
補碼:0000 0000 = 正數
複製程式碼
正數和負數使用補碼做運算相當於用加法做運算
計算時都是使用補碼進行計算
1.1.2 單精度Float
- 表示方式
當指數位
- 全為0,尾數附加位為0
- 不全為0,則尾數附加位為1
如此,尾數位就湊足了24位
計算方式
S*M*2^(e-127)
eg: -5的單精度表示
1 10000001 01000000000000000000000
其符號位 S為1,表示負數 -1
指數位E:10000001 ,e =129
尾數附加位:指數位不全為0,則為1
尾數M: 1+2^-2
;(-2,尾數位由右往左數第二位)
結果:-1 * ( 1+2^-2) * 2^( 129 - 127) = -5
二.JVM執行機制
2.1 JVM啟動流程
2.2 JVM基本結構
方法區物理上存在於堆裡,而且是在堆的持久代裡面;但在邏輯上,方法區和堆是獨立的
方法區method area
只是JVM規範中定義的一個概念,用於儲存類資訊、常量池、靜態變數、JIT編譯後的程式碼等資料,具體放在哪裡,不同的實現可以放在不同的地方。而永久代是Hotspot虛擬機器特有的概念,是方法區的一種實現,別的JVM都沒有這個東西
java 8和java 7的某版本後,perm gen 被去除了,取而代之的是metaspace。
不同點在於:perm gen 含class metadata、class static variables和interned string
metaspace只含class metadata了,class static variables和interned string被移到java heap上去了(所以java heap使用肯定要大一點)
JVM主要管理兩種型別的記憶體:堆和非堆. 簡單來說堆就是Java程式碼可及的記憶體,是留給開發人員使用的;非堆就是JVM留給自己用的 所以方法區,JVM內部處理或優化所需的記憶體(如JIT編譯後的程式碼快取),每個類結構(如執行時常數池,欄位和方法資料)以及方法和構造方法的程式碼都在非堆記憶體中.
2.2.1 PC暫存器
- 每一個執行緒擁有一個PC暫存器
- 線上程建立時建立
- 指向下一條指令
- 執行本地方法時,PC值為undefined ?
2.2.2 方法區
- 儲存裝載的類資訊:欄位、方法資訊、方法位元組碼
- 通常和永久區(perm)關聯在一起
2.2.3 Java堆
- 物件儲存在堆中
- 所有執行緒共享java堆
- GC工作空間
2.2.4 Java棧
-
執行緒私有
-
棧由一系列幀組成(故也叫幀棧)
-
幀儲存每個方法的區域性變數表,運算元棧,常量池指標,程式計數器
-
每一次方法呼叫建立一個幀,並壓棧
-
幀中有區域性變數表
-
運算元棧
Java沒有暫存器,所有引數傳遞使用運算元棧
棧上分配空間 -
小物件(幾十bytes),在沒有逃逸的情況下,可以直接分配在棧上
-
直接分配在棧上,可以自動回收,減輕GC壓力
-
大物件或逃逸物件無法在棧上分配
逃逸物件:棧內物件被外部物件引用,其作用範圍脫離了當前方法棧
public class AppMain {
//執行時, jvm 把appmain的資訊都放入方法區
public static void main(String[] args) {
//main 方法本身放入方法區。
Sample test1 = new Sample( " 測試1 " );
//test1是引用,所以放到棧區裡, Sample是自定義物件應該放到堆裡面
Sample test2 = new Sample( " 測試2 " );
test1.printName();
test2.printName();
}
}
public class Sample {
//執行時, jvm 把appmain的資訊都放入方法區
private name;
//new Sample例項後, name 引用放入棧區裡, name 物件放入堆裡
public Sample(String name) {
this .name = name;
}
//print方法本身放入 方法區裡
public void printName() {
System.out.println(name);
}
}
複製程式碼
三.記憶體模型
每一個執行緒有一個工作記憶體和主存獨立 工作記憶體存放主存中變數的值和拷貝
對於普通變數,一個執行緒中更新的值,不能馬上反應在其他變數中 如果需要在其他執行緒中立即可見,需要使用 volatile 關鍵字3.1 記憶體模型特性
- 可見性:一個執行緒修改了變數,其他執行緒可以立即知道
- 保證可見性的方法:
- volatile
- synchronized(unlock之前,寫變數值回主存)
- final(一旦初始化完成,其他執行緒就可見)
- 有序性 在本執行緒內,操作是有序的 線上程外觀察,操作是無序的(指令重排 或 主記憶體與執行緒記憶體同步延期)
- 指令重排 為了提高程式執行效率,調整指令執行次序 與寫相鄰的指令不可重排:讀後寫,寫後讀,寫後寫 編譯器不考慮多執行緒間的語義
- 指令重排 – 破壞執行緒間的有序性
class OrderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
int i = a +1;
……
}
}
}
複製程式碼
執行緒A首先執行writer()
方法
執行緒B執行緒接著執行reader()
方法
執行緒B在int i=a+1
是不一定能看到a已經被賦值為1
- 指令重排 – 保證有序性的方法 對方法加上同步關鍵字synchronized
- 指令重排的基本原則
- 程式順序原則:一個執行緒內保證語義的序列性
- volatile規則:volatile變數的寫,先發生於讀
- 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
- 傳遞性:A先於B,B先於C 那麼A必然先於C
- 執行緒的start方法先於它的每一個動作和/方法
- 執行緒的所有操作先於執行緒的終結
Thread.join()
,最後才終結 - 執行緒的中斷
interrupt()
先於被中斷執行緒的程式碼,中斷立即停止 - 物件的建構函式執行結束先於
finalize()
方法
3.2 常用JVM引數配置
- Tract跟蹤引數
-XX:+TraceClassLoading
:監控類的載入
-XX:+PrintClassHistogram
: 按下Ctrl+Break後,列印類的資訊 - 堆的分配引數
XX:+HeapDumpOnOutOfMemoryError
:OOM時匯出堆到檔案
-XX:OnOutOfMemoryError
: 在OOM時,執行一個指令碼
官方推薦新生代佔堆的3/8
倖存代佔新生代的1/10 - 棧的分配引數
Xss- 通常只有幾百K
- 決定了函式呼叫的深度
- 每個執行緒都有獨立的棧空間
- 區域性變數、引數 分配在棧上
四.GC的演算法與種類
4.1 GC演算法
- 引用計數法:java中未使用
- 標記清除:老年代
- 標記壓縮:老年代
- 複製演算法:新生代
- 分代思想
- 依據物件的存活週期進行分類,短命物件歸為新生代,長命物件歸為老年代
- 根據不同代的特點,選取合適的收集演算法
- 少量物件存活,適合複製演算法
- 大量物件存活,適合標記清理或者標記壓縮
4.2 可觸及性
- 可觸及的
- 從根節點可以觸及到這個物件
- 根:(與方法區棧相關)
- 棧中引用的物件
- 方法區中靜態成員或者常量引用的物件(全域性物件)
- JNI方法棧中引用物件
- 可復活的
- 一旦所有引用被釋放,就是可復活狀態,即不可達
- 但在finalize()中可能復活該物件
- 不可觸及的
- 在finalize()後,可能會進入不可觸及狀態
- 不可觸及的物件不可能復活
- 可以回收
public class CanReliveObj {
public static CanReliveObj obj;
public static void main(String[] args) throws InterruptedException{
obj=new CanReliveObj();
obj=null; //可復活
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj 是 null");
}else{
System.out.println("obj 可用");
}
System.out.println("第二次gc");
obj=null; //不可復活
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj 是 null");
}else{
System.out.println("obj 可用");
}
}
@Override
//重寫析構方法
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanReliveObj finalize called");
obj=this;
}
@Override
public String toString(){
return "I am CanReliveObj";
}
}
複製程式碼
- 避免使用finalize方法
- 物件中只能呼叫一次,操作不慎可能導致錯誤
- 優先順序低,何時被呼叫不確定,何時發生GC不確定
- 可以使用try-catch-finally來替代
對於用可達性分析法搜尋不到的物件,GC並不一定會回收該物件。要完全回收一個物件,至少需要經過兩次標記的過程。
第一次標記:對於一個沒有其他引用的物件,篩選該物件是否有必要執行finalize()方法,如果沒有執行必要,則意味可直接回收。(篩選依據:是否複寫或執行過finalize()方法;因為finalize方法只能被執行一次)。
第二次標記:如果被篩選判定位有必要執行,則會放入FQueue佇列,並自動建立一個低優先順序的finalize執行緒來執行釋放操作。如果在一個物件釋放前被其他物件引用,則該物件會被移除FQueue佇列
4.3 Stop-The-World
Java中一種全域性暫停的現象
全域性停頓,所有Java程式碼停止,native程式碼可以執行,但不能和JVM互動
多半由於GC引起,也可以是Dump執行緒、死鎖檢查、堆Dump
4.4 序列蒐集器
- 最古老,最穩定
- 效率高
- 可能會產生較長的停頓
- 適用於資料量較小,對響應時間無要求的小型應用
- -XX:+UseSerialGC
- 新生代、老年代使用序列回收
- 新生代複製演算法
- 老年代標記-壓縮
4.5 並行收集器
適用於對吞吐量有較高要求, 多CPU、對應用響應時間無要求的中、大型應用。
舉例:後臺處理、科學計算
吞吐量:=執行使用者程式碼時間/(執行使用者程式碼時間+GC時間)
- 併發Concurrent:交替做不同事的能力,使用者程式可以不暫停,不一定並行,但可以交替執行
- 並行Parallel:同時做不同事的能力,垃圾回收執行緒並行工作,但應用程式等待暫停
4.5.1 ParNew
-XX:+UseParNewGC
- 新生代並行
- 老年代序列
- Serial收集器新生代的並行版本
- 複製演算法
- 多執行緒,需要多核支援
-XX:ParallelGCThreads
限制執行緒數量
0.834: [GC 0.834: [ParNew: 13184K->1600K(14784K), 0.0092203 secs] 13184K->1921K(63936K), 0.0093401 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
複製程式碼
4.5.2 Parallel收集器(可自定義的、靈活)
- 類似ParNew
- 新生代複製演算法
- 老年代 標記-壓縮
- 更加關注吞吐量
-XX:+UseAdaptiveSizePolicy
自適應調節策略是Parallel與ParNew的重要區別-XX:+UseParallelGC
- 新生代使用Parallel收集器+ 老年代序列
-XX:+UseParallelOldGC
- 新生代使用Parallel收集器+ 老年代並行
老年代不一樣而已
1.500: [Full GC [PSYounhttps://user-gold-cdn.xitu.io/2017/12/3/1601bd5a57d6924fen: 2682K->0K(19136K)] [ParOldGen: 28035K->30437K(43712K)] 30717K->30437K(62848K) [PSPermGen: 10943K->10928K(32768K)], 0.2902791 secs] [Times: user=1.44 sys=0.03, real=0.30 secs]
複製程式碼
- 特殊引數
-XX:MaxGCPauseMills
- 最大停頓時間,單位毫秒
- GC盡力保證回收時間不超過設定值
-XX:GCTimeRatio
- 0-100的取值範圍
- 垃圾收集時間佔總時間的比
- 預設99,即最大允許1%時間做GC
- 這兩個引數是矛盾的。因為停頓時間和吞吐量不可能同時調優
4.6 CMS併發收集器
適用於對響應時間有高要求,多CPU、對應用響應時間有較高要求的中、大型應用。
舉例:Web伺服器/應用伺服器、電信交換、整合開發環境
-
特性
Concurrent Mark Sweep 併發標記清除(與使用者執行緒一起執行 )
標記-清除演算法(不是標記壓縮)
併發階段會降低吞吐量(?)
只是針對老年代收集器(新生代使用ParNew/或序列)
-XX:+UseConcMarkSweepGC
-
執行過程
- 初始標記
根可以直接關聯到的物件
速度快
獨佔CPU,全域性停頓 - 併發標記(和使用者執行緒一起)
標記的主要過程,標記全部物件 - 重新標記
重新修正標記
獨佔CPU,全域性停頓 - 併發清除(和使用者執行緒一起)
基於標記結果,直接清除物件
- 初始標記
-
優:
儘可能降低停頓,在併發標記過程中並不需要全域性停頓 -
劣:
- 會影響系統整體吞吐量和效能
- 比如,在使用者執行緒執行過程中,分一半CPU去做GC,系統效能在GC階段,反應速度就下降一半
- 清理不徹底
- 在清理階段,使用者執行緒還在執行,會產生新的垃圾,無法清理,因為和使用者執行緒一起執行,不能在空間快滿時再清理
-XX:CMSInitiatingOccupancyFraction
設定觸發GC的閾值- 如果不幸記憶體預留空間不夠,就會引起concurrent mode failure,此時應該使用序列收集器作為後備,由於空間不足,此時一般停頓時間較長
- 碎片清理問題
CMS使用的是標記-清除演算法,在清除後堆記憶體有效物件地址不連續,有記憶體碎片存在,故可設定記憶體壓縮,整理記憶體碎片 即CMS為了效能考慮在老年代使用標記-清除演算法,但仍可以設定使用標記-壓縮演算法
-XX:+ UseCMSCompactAtFullCollection
Full GC後,進行一次整理- 整理過程是獨佔的,會引起停頓時間變長
-XX:+CMSFullGCsBeforeCompaction
- 設定進行幾次Full GC後,進行一次碎片整理
-XX:ParallelCMSThreads
- 設定CMS的執行緒數量,一般大約設成cpu核數,預設定義為(CPU數量+3)/4,即至少25%
4.7 GC引數整理
4.7.1 記憶體分配
引數名稱 | 含義 | 備註 |
---|---|---|
-Xms | 初始堆大小 | 預設空餘堆記憶體小於40%時,JVM就會增大堆直到-Xmx的最大限制 |
-Xmx | 最大堆大小 | 預設(MaxHeapFreeRatio引數可以調整)空餘堆記憶體大於70%時,JVM會減少堆直到 -Xms的最小限制 |
-Xmn | 年輕代大小 | eden+ 2 survivor space,增大年輕代後,將會減小年老代大小,Sun官方推薦配置為整個堆的3/8 |
-XX:PermSize | 設定持久代(perm gen)初始值 | 持久代是方法區的一種實現 |
-XX:MaxPermSize | 設定持久代最大值 | |
-Xss | 每個執行緒的棧大小 | JDK5.0以後每個執行緒堆疊大小為1M,棧越大,執行緒越少,棧深度越深 |
-XX:NewRatio | 年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代) | -XX:NewRatio=4表示年輕代與年老代所佔比值為1:4,年輕代佔整個堆疊的1/5,Xms=Xmx並且設定了Xmn的情況下,該引數不需要進行設定。 |
-XX:SurvivorRatio | Eden區與Survivor區的大小比值 | 設定為8,則兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區佔整個年輕代的1/10 |
-XX:MaxTenuringThreshold | 垃圾最大年齡 | 該引數只有在序列GC時才有效 |
-XX:PretenureSizeThreshold | 物件超過多大是直接在舊生代分配 | 單位位元組 新生代採用Parallel Scavenge GC時無效, 另一種直接在舊生代分配的情況是大的陣列物件,且陣列中無外部引用物件. |
4.7.2 並行收集器相關引數
引數名稱 | 含義 | 備註 |
---|---|---|
-XX:+UseParallelGC | 新生代使用Parallel收集器+ 老年代序列 | |
-XX:+UseParNewGC | 在新生代使用並行收集器 | |
-XX:ParallelGCThreads | 並行收集器的執行緒數 | 此值最好配置與處理器數目相等 也適用於CMS |
-XX:+UseParallelOldGC | 新生代使用Parallel收集器+ 老年代並行 | |
-XX:MaxGCPauseMillis | 每次年輕代垃圾回收的最長時間(最大暫停時間) | 如果無法滿足此時間,JVM會自動調整年輕代大小,以滿足此值 |
-XX:+UseAdaptiveSizePolicy | 自動選擇年輕代區大小和相應的Survivor區比例 | 設定此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直開啟. |
4.7.3 CMS併發相關引數
引數名稱 | 含義 | 備註 |
---|---|---|
-XX:+UseConcMarkSweepGC | 使用CMS記憶體收集 | 新生代使用並行收集器ParNew,老年代使用CMS+序列收集器 |
-XX:CMSFullGCsBeforeCompaction | 多少次後進行記憶體壓縮 | 由於併發收集器不對記憶體空間進行壓縮,整理,所以執行一段時間以後會產生"碎片",使得執行效率降低.此值設定執行多少次GC以後對記憶體空間進行壓縮,整理 |
-XX+UseCMSCompactAtFullCollection | 在FULL GC的時候, 對年老代的壓縮 | CMS是不會移動記憶體的, 因此, 這個非常容易產生碎片, 導致記憶體不夠用, 因此, 記憶體的壓縮這個時候就會被啟用。 增加這個引數是個好習慣。可能會影響效能,但是可以消除碎片 |
-XX:CMSInitiatingPermOccupancyFraction | 當永久區佔用率達到這一百分比時,啟動CMS回收 |
4.7.4 輔助資訊
引數名稱 | 含義 |
---|---|
-XX:+PrintGC | |
-XX:+PrintGCDetails | |
-XX:+PrintGCTimeStamps | |
-XX:+PrintGCApplicationStoppedTime | 列印垃圾回收期間程式暫停的時間.可與上面混合使用 |
-XX:+PrintHeapAtGC | 列印GC前後的詳細堆疊資訊 |
--Xloggc:filename | 把相關日誌資訊記錄到檔案以便分析 |
-XX:+HeapDumpOnOutOfMemoryError | |
-XX:HeapDumpPath | |
-XX:+PrintCommandLineFlags | 列印出已經被設定過的詳細的 XX 引數的名稱和值 |
4.8 調優總結
專案 | 響應時間優先 | 吞吐量優先 |
---|---|---|
年輕代 | -Xmn儘量大,直到接近系統的最低響應時間限制-XX:MaxGCPauseMillis,減少年輕代GC,減少到達老年代物件 | -Xmn儘量大 |
年輕代垃圾回收器 | 併發收集器 | 並行收集器 |
年老代 | 如果堆設定小了,可以會造成記憶體碎 片,高回收頻率以及應用暫停而使用傳統的標記清除方式;如果堆大了,則需要較長的收集時間 | |
要參照年輕代和年老代垃圾回收時間與次數 | -XX:NewRatio 年老代設定小一些,這樣可以儘可能回收掉大部分短期物件,減少中期的物件,而年老代盡存放長期存活物件 | |
年老代垃圾回收器 | 年老代使用併發收集器 | 因為對響應時間沒有要求,垃圾收集可以並行進行,也可以序列 |
典型配置
- 吞吐量優先的並行收集器
並行收集器主要以到達一定的吞吐量為目標,適用於科學技術和後臺處理等
年輕代都使用並行收集器,老年代沒要求
年輕代使用並行收集,而年老代仍舊使用序列收集
-Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
複製程式碼
年老代並行
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
複製程式碼
設定每次年輕代垃圾回收的最長時間,如果無法滿足此時間,JVM會自動調整年輕代大小,以滿足此值
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
複製程式碼
- 響應時間優先的併發收集器
併發收集器主要是保證系統的響應時間,減少垃圾收集時的停頓時間。適用於應用伺服器、電信領域等
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
複製程式碼
-XX:+UseConcMarkSweepGC
:設定年老代為併發收集
-XX:+UseParNewGC
:設定年輕代為並行收集
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection
複製程式碼
-XX:CMSFullGCsBeforeCompaction
:由於併發收集器不對記憶體空間進行壓縮、整理,所以執行一段時間以後會產生“碎片”,使得執行效率降低。此值設定執行多少次GC以後對記憶體空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection
:開啟對年老代的壓縮。可能會影響效能,但是可以消除碎片
4.9 GC日誌
5.617: [GC 5.617: [ParNew: 43296K->7006K(47808K), 0.0136826 secs] 44992K->8702K(252608K), 0.0137904 secs]
[Times: user=0.03 sys=0.00, real=0.02 secs]
複製程式碼
解釋
5.617(時間戳): [GC(Young GC) 5.617(時間戳):
[ParNew(使用ParNew作為年輕代的垃圾回收器): 43296K(年輕代垃圾回收前的大小)->7006K(年輕代垃圾回收以後的大小)(47808K)(年輕代的總大小), 0.0136826 secs(回收時間)]
44992K(堆區垃圾回收前的大小)->8702K(堆區垃圾回收後的大小)(252608K)(堆區總大小), 0.0137904 secs(回收時間)]
[Times: user=0.03(Young GC使用者耗時) sys=0.00(Young GC系統耗時), real=0.02 secs(Young GC實際耗時)]
複製程式碼
[GC [DefNew: 3468K->150K(9216K), 0.0028638 secs][Tenured:
1562K->1712K(10240K), 0.0084220 secs] 3468K->1712K(19456K),
[Perm : 377K->377K(12288K)],
0.0113816 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
複製程式碼
Tenured:持久代/老年代
序列收集器:
DefNew:使用-XX:+UseSerialGC
(新生代,老年代都使用序列回收收集器)。
並行收集器:
ParNew:是使用-XX:+UseParNewGC
(新生代使用並行收集器,老年代使用序列回收收集器)或者-XX:+UseConcMarkSweepGC
(新生代使用並行收集器,老年代使用CMS)。
PSYoungGen:是使用-XX:+UseParallelOldGC
(新生代,老年代都使用並行回收收集器)或者-XX:+UseParallelGC
(新生代使用並行回收收集器,老年代使用序列收集器)
garbage-first heap:是使用-XX:+UseG1GC
(G1收集器)
4.10 GC觸發條件
觸發條件就是某GC演算法對應區域滿了,或是預測快滿了(比如該區使用比例達到一定比例-對並行/併發,或不夠晉升)
4.10.1 GC分類
針對HotSpot VM的實現,它裡面的GC其實準確分類只有兩大種:
- Partial GC:並不收集整個GC堆的模式
Young GC:只收集young gen的GC
Old GC:只收集old gen的GC。只有CMS的concurrent collection是這個模式
Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式 - Full GC
收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。收集是整體收集的,無所謂先收集old還是young。marking是整體一起做的,然後compaction(壓縮)是old gen先來然後再young gen來
4.10.2 HotSpot VM的serial GC
Major GC通常是跟full GC是等價的,收集整個GC堆。最簡單的分代式GC策略,按HotSpot VM的serial GC的實現來看,觸發條件是:
- young GC:當young gen中的eden區分配滿的時候觸發。注意young GC中有部分存活物件會晉升到old gen,所以young GC後old gen的佔用量通常會有所升高。
- full GC
- 當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC);
- 如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;
- System.gc()、heap dump帶GC,預設也是觸發full GC。
4.10.3 HotSpot VM非併發GC(Parallel GC)
觸發條件複雜一些,不過大致的原理與序列GC一樣。
例外: Parallel Scavenge(-XX:+UseParallelGC
新生代使用Parallel收集器)框架下,預設是在要觸發full GC前先執行一次young GC,並且兩次GC之間能讓應用程式稍微執行一下,以期降低full GC的暫停時間(因為young GC會盡量清理了young gen的死物件,減少了full GC的工作量)。控制這個行為的VM引數是-XX:+ScavengeBeforeFullGC
4.10.4 HotSpot VM併發GC
併發GC的觸發條件就不太一樣。以CMS GC為例,主要是定時去檢查old gen的使用量,當使用量超過了觸發比例就會啟動一次CMS GC,對old gen做併發收集
-XX:CMSInitiatingOccupancyFraction=80 // old達到80%收集
複製程式碼
或者GC過程中,由於預留的記憶體無法滿足程式需要, 出現concurrent mode failure,臨時使用serial old進行Full GC
4.10.5 HotSpot VM G1收集
G1 GC的initial marking(初始標記)的觸發條件是Heap使用比率超過某值,收集時是按照回收價值的優先順序,不按照young old區
G1 GC:Young GC + mixed GC(新生代,再加上部分老生代)+ Full GC for G1 GC演算法(應對G1 GC演算法某些時候的不趕趟,開銷很大);
五. 類裝載器
5.1 Class裝載驗證流程
5.1.1 載入
轉為方法區資料結構 在Java堆中生成對應的java.lang.Class物件
- 類裝載器ClassLoader
- ClassLoader是一個抽象類
- ClassLoader的例項將讀入Java位元組碼將類裝載到JVM中
- ClassLoader可以定製,滿足不同的位元組碼流獲取方式(比如網路)
tomcat和OSGi有做更改
example:類從上往下載入
在工程目錄中新增A.java,自動編譯生成A.class
又指定根載入目錄path,-Xbootclasspath/a:path,重新放一個同名A.class
此時會載入指定根載入目錄下的class檔案
注意:以上是jdk預設的類載入模式,但tomcat和OSGi有自己的載入方式
Tomcat:Tomcat的WebappClassLoader 就會先載入自己的Class,找不到再委託parent
OSGi的ClassLoader形成網狀結構,根據需要自由載入Class
5.1.2 連結
- 驗證
目的:保證Class流的格式是正確的
- 檔案格式的驗證
- 是否以0xCAFEBABE開頭
- 版本號是否合理:class檔案由什麼版本jdk編譯生成,與執行class的jdk是否相容
- 後設資料驗證(基本資訊驗證)
- 是否有父類:class中指定了父類,檢查父類是否存在
- 繼承了final類?
- 非抽象類實現了所有的抽象方法
- 位元組碼驗證 (複雜)
- 執行檢查
- 棧資料型別和操作碼資料引數吻合
- 跳轉指令指定到合理的位置
- 符號引用驗證
- 常量池中描述類是否存在:引用的類必須存在
- 訪問的方法或欄位是否存在且有足夠的許可權:private…
- 檔案格式的驗證
- 準備
- 分配記憶體,併為類設定初始值 (方法區中)
- public static int v=1;
- 在準備階段中,v會被設定為0
- 在初始化的中才會被設定為1
- 對於static final型別,在準備階段就會被賦上正確的值—在初始化之前就賦值
- public static final int v=1;
- 分配記憶體,併為類設定初始值 (方法區中)
- 解析 符號引用替換為直接引用:即類名應用,直接替換為記憶體地址指標
5.1.3 初始化
- 執行類構造器
- static變數 賦值語句 : 注意,static final 在準備階段已經賦值了
- static{}語句
- 子類的呼叫前保證父類的被呼叫
- 是執行緒安全的,即單執行緒執行
六. 效能分析
6.1 Java自帶效能分析的工具
直接在控制檯輸入命令,引數具體使用可使用-help 命令
6.1.1 jps
一般是第一步,方便後續其他命令呼叫
列出java程式,類似於ps命令
引數-q可以指定jps只輸出程式ID ,不輸出類的短名稱
引數-m可以用於輸出傳遞給Java程式(主函式)的引數
引數-l可以用於輸出主函式的完整路徑
引數-v可以顯示傳遞給JVM的引數
6.1.2 jinfo
檢視程式引數
可以用來檢視正在執行的Java應用程式的擴充套件引數,甚至支援在執行時,修改部分引數
-flag 程式ID:列印指定JVM的引數值
-flag [+|-] 程式ID:設定指定JVM引數的布林值
-flag = 程式ID:設定指定JVM引數的值
6.1.3 jmap
生成Java應用程式的堆快照和物件的統計資訊
num #instances #bytes class name
----------------------------------------------
1: 370469 32727816 [C
2: 223476 26486384 <constMethodKlass>
3: 260199 20815920 java.lang.reflect.Method
…..
8067: 1 8 sun.reflect.GeneratedMethodAccessor35
Total 4431459 255496024
複製程式碼
6.1.4 jstack
列印執行緒dump
-l
列印鎖資訊
-m
列印java和native的幀資訊
-F
強制dump,當jstack沒有響應時使用
Jdk1.6版本只有 –l選項
6.1.5 JConsole
圖形化監控工具
可以檢視Java應用程式的執行概況,監控堆資訊、永久區使用情況、類載入情況等
6.1.6 Visual VM
Visual VM是一個功能強大的多合一故障診斷和效能監控的視覺化工具
6.1.7 MAT
6.2 Java堆分析
- 記憶體溢位OOM原因
Jvm記憶體區間:堆、永久區、執行緒棧、直接記憶體
堆+執行緒棧 +直接記憶體<= 作業系統可分配空間
- 堆溢位
佔用大量堆空間,直接溢位
public static void main(String args[]){
ArrayList<byte[]> list=new ArrayList<byte[]>();
for(int i=0;i<1024;i++){
list.add(new byte[1024*1024]);
}
}
複製程式碼
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at geym.jvm.ch8.oom.SimpleHeapOOM.main(SimpleHeapOOM.java:14)
複製程式碼
解決方法:增大堆空間,及時釋放記憶體,分批處理
- 永久區溢位
//生成大量的類
public static void main(String[] args) {
for(int i=0;i<100000;i++){
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
}
}
複製程式碼
Caused by: java.lang.OutOfMemoryError: 【PermGen space】
[Full GC[Tenured: 2523K->2523K(10944K), 0.0125610 secs] 2523K->2523K(15936K),
[Perm : 【4095K->4095K(4096K)】], 0.0125868 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Heap
def new generation total 4992K, used 89K [0x28280000, 0x287e0000, 0x2d7d0000)
eden space 4480K, 2% used [0x28280000, 0x282966d0, 0x286e0000)
from space 512K, 0% used [0x286e0000, 0x286e0000, 0x28760000)
to space 512K, 0% used [0x28760000, 0x28760000, 0x287e0000)
tenured generation total 10944K, used 2523K [0x2d7d0000, 0x2e280000, 0x38280000)
the space 10944K, 23% used [0x2d7d0000, 0x2da46cf0, 0x2da46e00, 0x2e280000)
compacting perm gen total 4096K, used 4095K [0x38280000, 0x38680000, 0x38680000)
the space 4096K, 【99%】 used [0x38280000, 0x3867fff0, 0x38680000, 0x38680000)
ro space 10240K, 44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)
rw space 12288K, 52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000)
複製程式碼
解決方法:避免動態生成class,增大Perm區,允許Class回收
- Java棧溢位
-Xmx1g -Xss1m
public static class SleepThread implements Runnable{
public void run(){
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]){
for(int i=0;i<1000;i++){
new Thread(new SleepThread(),"Thread"+i).start();
System.out.println("Thread"+i+" created");
}
}
複製程式碼
Exception in thread "main" java.lang.OutOfMemoryError:
unable to create new native thread
複製程式碼
這裡的棧溢位指,在建立執行緒的時候,需要為執行緒分配棧空間,這個棧空間是向作業系統請求的,如果作業系統無法給出足夠的空間,就會丟擲OOM
eg:堆空間1G,每個執行緒棧空間1m
注意:堆+執行緒棧+直接記憶體 <= 作業系統可分配空間
- 直接記憶體溢位
ByteBuffer.allocateDirect()
:申請堆外的直接記憶體
直接記憶體也可以被GC回收
-Xmx1g -XX:+PrintGCDetails
//會丟擲oom,但堆記憶體空間充足
for(int i=0;i<1024;i++){
ByteBuffer.allocateDirect(1024*1024);
System.out.println(i);
System.gc();
}
複製程式碼
七. 鎖
7.1 執行緒安全
public static List<Integer> numberList =new ArrayList<Integer>();
public static class AddToList implements Runnable{
int startnum=0;
public AddToList(int startnumber){
startnum=startnumber;
}
@Override
public void run() {
int count=0;
while(count<1000000){
numberList.add(startnum);
startnum+=2;
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new AddToList(0));
Thread t2=new Thread(new AddToList(1));
t1.start();
t2.start();
while(t1.isAlive() || t2.isAlive()){
Thread.sleep(1);
}
System.out.println(numberList.size());
}
複製程式碼
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 73
at java.util.ArrayList.add(Unknown Source)
at simpleTest.TestSome$AddToList.run(TestSome.java:27)
at java.lang.Thread.run(Unknown Source)
1000005
複製程式碼
ArrayList 不是執行緒安全的集合物件,在兩個執行緒新增元素的過程中,當陣列填滿,正在自動擴充套件時,另一個執行緒卻還是在新增元素,在ArrayList底層就是不可變長的陣列,則丟擲下表越界異常
7.2 物件頭Mark
HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為三塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。
7.3 偏向鎖
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)
大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。偏向鎖只能在單執行緒下起作用。
偏向鎖在鎖物件的物件頭中有個ThreadId欄位,這個欄位如果是空的,第一次獲取鎖的時候,就將自身的ThreadId寫入到鎖的ThreadId欄位內,將鎖頭內的是否偏向鎖的狀態位置1.,這樣下次獲取鎖的時候,直接檢查ThreadId是否和自身執行緒Id一致,如果一致,則認為當前執行緒已經獲取了鎖,因此不需再次獲取鎖,略過了輕量級鎖和重量級鎖的加鎖階段。提高了效率。
- 大部分情況是沒有競爭的,所以可以通過偏向來提高效能
- 所謂的偏向,就是偏心,即鎖會偏向於當前已經佔有鎖的執行緒
- 將物件頭Mark的標記設定為偏向,並將執行緒ID寫入物件頭Mark
- 只要沒有競爭,獲得偏向鎖的執行緒,在將來進入同步塊,不需要做同步
- 當其他執行緒請求相同的鎖時,偏向模式結束,在全域性安全點(在這個時間點上沒有位元組碼正在執行)撤銷偏向鎖,採用其他鎖
-XX:+UseBiasedLocking
- 預設啟用
- 在競爭激烈的場合,偏向鎖會增加系統負擔
開啟偏向鎖-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
系統啟動後,並不會立即開啟偏向鎖,而是會延遲,可以設定延遲時間為0
7.4 輕量級鎖
普通的鎖處理效能不夠理想,輕量級鎖是一種快速的鎖定方法
輕量級鎖是為了線上程交替執行同步塊時提高效能
-
如果物件沒有被鎖定
將物件頭的Mark指標儲存到鎖物件中
將物件頭設定為指向鎖的指標(線上程棧空間中)
即物件和鎖都互相儲存引用輕量級鎖加鎖
執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。
然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。輕量級鎖解鎖
輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到物件頭,如果成功,則表示沒有競爭發生。
如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖 -
lock位於執行緒棧中
由上可知,判斷一個執行緒是否持有輕量級鎖,只要判斷物件頭的指標,是否線上程的棧空間範圍內 -
特性
- 如果輕量級鎖失敗,表示存在競爭,升級為重量級鎖(常規鎖,作業系統,程式級)
- 在沒有鎖競爭的前提下,減少傳統鎖使用OS互斥量產生的效能損耗
- 在競爭激烈時,輕量級鎖會多做很多額外操作,導致效能下降 mark word中的lock record指向堆疊最近的一個執行緒的lock record,其實就是按照先來後到模式進行了輕量級的加鎖
7.5 自旋鎖 spin lock
- 儘量減少系統級別的執行緒掛起
- 當競爭存在時,如果執行緒可以很快獲得鎖,那麼可以不在OS層掛起執行緒,讓執行緒做幾個空操作(自旋)等待獲得鎖
- JDK1.6中
-XX:+UseSpinning
開啟 - JDK1.7中,去掉此引數,改為內建實現
- 如果同步塊很長,自旋失敗,會降低系統效能—空佔執行緒操作,最後還是要在OS層掛起,自旋鎖空耗資源
- 如果同步塊很短,自旋成功,節省執行緒掛起切換時間,提升系統效能
當發生爭用時,若Owner執行緒能在很短的時間內釋放鎖,則那些正在爭用執行緒(未阻塞)可以稍微等一等(自旋),在Owner執行緒釋放鎖後,爭用執行緒可能會立即得到鎖,從而避免執行緒阻塞
7.6 偏向鎖vs輕量級鎖vs自旋鎖
- 不是Java語言層面的鎖優化方法
- 內建於JVM中的獲取鎖的優化方法和獲取鎖的步驟
- 偏向鎖可用會先嚐試偏向鎖
- 輕量級鎖可用會先嚐試輕量級鎖
- 以上都失敗,嘗試自旋鎖
- 再失敗,嘗試普通鎖(重量級鎖),使用OS互斥量在作業系統層掛起
鎖 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 | 如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 | 適用於只有一個執行緒訪問同步塊場景。 |
輕量級鎖 | 競爭的執行緒不會阻塞,提高了程式的響應速度。 | 如果始終得不到鎖競爭的執行緒使用自旋會消耗CPU。有競爭時會比重量級鎖更慢 | 追求響應時間。同步塊執行速度非常快。 |
重量級鎖 | 執行緒競爭不使用自旋,不會消耗CPU。 | 執行緒阻塞,響應時間緩慢。 | 追求吞吐量。同步塊執行速度較長。 |
偏向鎖與輕量級鎖理念上的區別:
輕量級鎖:在無競爭的情況下使用CAS操作去消除同步使用的互斥量
偏向鎖:在無競爭的情況下把整個同步都消除掉
連CAS操作都不做了?
7.7 Java語言層面優化鎖
7.7.1 減少鎖持有時間
同步範圍減少
7.7.2 減小鎖粒度
將大物件拆成小物件,增加並行度,降低鎖競爭
偏向鎖和輕量級鎖成功率提高——粒度大,競爭激烈,偏向鎖,輕量級鎖失敗概率就高
- ConcurrentHashMap
若干個Segment :Segment<K,V>[] segments
Segment中維護HashEntry<K,V>
put操作時
先定位到Segment,鎖定一個Segment,執行put
在減小鎖粒度後, ConcurrentHashMap允許若干個執行緒同時進入
7.7.3 鎖分離
- 讀寫鎖ReadWriteLock
鎖型別 | 讀鎖 | 寫鎖 |
---|---|---|
讀鎖 | 可訪問 | 不可訪問 |
寫鎖 | 不可訪問 | 不可訪問 |
- LinkedBlockingQueue 只要操作互不影響,鎖就可以分離
7.7.4 鎖粗化
如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於效能的優化
- Example1:
public void demoMethod(){
synchronized(lock){
//do sth.
}
//做其他不需要的同步的工作,但能很快執行完畢
synchronized(lock){
//do sth.
}
}
複製程式碼
直接擴大範圍
public void demoMethod(){
//整合成一次鎖請求
synchronized(lock){
//do sth.
//做其他不需要的同步的工作,但能很快執行完畢
}
}
複製程式碼
- Example2
for(int i=0;i<CIRCLE;i++){
synchronized(lock){
}
}
//鎖粗化
synchronized(lock){
for(int i=0;i<CIRCLE;i++){
}
}
複製程式碼
7.7.5 鎖消除
在即時編譯器時,如果發現不可能被共享的物件,則可以消除這些物件的鎖操作
鎖不是由程式設計師引入的,JDK自帶的一些庫,可能內建鎖
棧上物件,不會被全域性訪問的,沒有必要加鎖
- Example
public static void main(String args[]) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < CIRCLE; i++) {
craeteStringBuffer("JVM", "Diagnosis");
}
long bufferCost = System.currentTimeMillis() - start;
System.out.println("craeteStringBuffer: " + bufferCost + " ms");
}
public static String craeteStringBuffer(String s1, String s2) {
//StringBuffer執行緒安全物件,內建鎖
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
複製程式碼
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
- 棧上物件(方法區域性變數),不會被全域性訪問的,沒有必要加鎖
7.7.6 無鎖
無鎖的一種實現方式
CAS(Compare And Swap)
非阻塞的同步
CAS(V,E,N):if V==E then V=N
複製程式碼
CAS演算法的過程: CAS(V,E,N)。V表示要更新的變數,E表示預期值,N表示新值。
僅當V值等於E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。
最後,CAS返回當前V的真實值。
CAS操作是抱著樂觀的態度進行的,它總是認為自己可以成功完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS操作即時沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並進行恰當的處理。
java.util.concurrent.atomic包使用無鎖實現,效能高於一般的有鎖操作
7.8 執行緒狀態及裝換
當多個執行緒同時請求某個物件監視器時,物件監視器會設定幾種狀態用來區分請求的執行緒:
- Contention List:所有請求鎖的執行緒將被首先放置到該競爭佇列
- Entry List:Contention List中那些有資格成為候選人的執行緒被移到Entry List
- Wait Set:那些呼叫wait方法被阻塞的執行緒被放置到Wait Set
- OnDeck:任何時刻最多隻能有一個執行緒正在競爭鎖,該執行緒稱為OnDeck
- Owner:獲得鎖的執行緒稱為Owner
- !Owner:釋放鎖的執行緒
那些處於ContetionList、EntryList、WaitSet中的執行緒均處於阻塞狀態,阻塞操作由作業系統完成(在Linxu下通過pthread_mutex_lock函式)。
執行緒被阻塞後便進入核心(Linux)排程狀態,這個會導致系統在使用者態與核心態之間來回切換,嚴重影響鎖的效能 - Synchronized加鎖
每一個執行緒在準備獲取共享資源時:
- 檢查MarkWord裡面是不是放的自己的ThreadId ,如果是,表示當前執行緒是處於 “偏向鎖”
- 如果MarkWord不是自己的ThreadId,鎖升級,這時候,用CAS來執行切換,新的執行緒根據MarkWord裡面現有的ThreadId,通知之前執行緒暫停,之前執行緒將Markword的內容置為空。
- 兩個執行緒都把物件的HashCode複製到自己新建的用於儲存鎖的記錄空間,接著開始通過CAS操作,把共享物件的MarKword的內容修改為自己新建的記錄空間的地址的方式競爭MarkWord,
- 第三步中成功執行CAS的獲得資源,失敗的則進入自旋
- 自旋的執行緒在自旋過程中,成功獲得資源(即之前獲的資源的執行緒執行完成並釋放了共享資源),則整個狀態依然處於 輕量級鎖的狀態,如果自旋失敗
- 進入重量級鎖的狀態,這個時候,自旋的執行緒進行阻塞,等待之前執行緒執行完成並喚醒自己
八.Class檔案結構
U4:無符號整型,4個位元組
型別 | 名稱 | 數量 | 備註 |
---|---|---|---|
u4 | magic | 1 | 0xCAFEBABE:表示java class檔案型別 |
u2 | minor_version | 1 | Jdk編譯版本 |
u2 | major_version | 1 | Jdk編譯版本 |
u2 | constant_pool_count | 1 | |
cp_info | constant_pool | constant_pool_count - 1 | 鏈式引用基本型別-被各處引用-要減1 |
u2 | access_flags | 1 | 訪問修飾符&class type |
u2 | this_class | 1 | 指向常量池的class |
u2 | super_class | 1 | 指向常量池的class |
u2 | interfaces_count | 1 | |
u2 | interfaces | interfaces_count | 每個介面指向常量池CONSTANT_Class索引 |
u2 | fields_count | 1 | |
field_info | fields | fields_count | access_flags,name_index ,descriptor_index ,attributes_count,attribute_info attributes[attributes_count] |
u2 | methods_count | 1 | |
method_info | methods | methods_count | |
u2 | attribute_count | 1 | |
attribute_info | attributes | attributes_count |
九. JVM位元組碼執行
9.1 javap
執行緒幀棧中的資料:
- 程式計數器:每個執行緒都有一個,用於指向當前執行緒執行的指令地
- 區域性變數表
- 運算元棧
9.2 JIT及其相關引數
- JIT Just-In-Time
位元組碼執行效能較差,所以可以對於熱點程式碼(Hot Spot Code)編譯成機器碼再執行,在執行時的編譯
當虛擬機器發現某個方法或程式碼塊執行特別頻繁時,就會把這些程式碼認定為“Hot Spot Code”(熱點程式碼),為了提高熱點程式碼的執行效率,在執行時,虛擬機器將會把這些程式碼編譯成與本地平臺相關的機器碼) - 辨別熱點程式碼
方法呼叫計數器:方法呼叫次數
回邊計數器:方法內迴圈次數,可以在棧上直接替換為機器碼 - 編譯設定
-XX:CompileThreshold=1000
:執行超過一千次即為熱點程式碼
-XX:+PrintCompilation
:列印編譯為機器碼的程式碼
-Xint
:解釋執行
-Xcomp
:全部編譯執行
-Xmixed
:預設,混合