Java虛擬機器學習筆記整理

fengye發表於2017-12-27

本文主要整理自煉術成金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啟動流程

JVM啟動流程

2.2 JVM基本結構

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暫存器

  1. 每一個執行緒擁有一個PC暫存器
  2. 線上程建立時建立
  3. 指向下一條指令
  4. 執行本地方法時,PC值為undefined ?

2.2.2 方法區

  1. 儲存裝載的類資訊:欄位、方法資訊、方法位元組碼
  2. 通常和永久區(perm)關聯在一起

2.2.3 Java堆

  1. 物件儲存在堆中
  2. 所有執行緒共享java堆
  3. GC工作空間
    GC工作空間

2.2.4 Java棧

  1. 執行緒私有

  2. 棧由一系列幀組成(故也叫幀棧)

  3. 幀儲存每個方法的區域性變數表,運算元棧,常量池指標,程式計數器

  4. 每一次方法呼叫建立一個幀,並壓棧

  5. 幀中有區域性變數表

    static方法

    no static方法

  6. 運算元棧
    Java沒有暫存器,所有引數傳遞使用運算元棧

    運算元棧

    棧上分配空間

  7. 小物件(幾十bytes),在沒有逃逸的情況下,可以直接分配在棧上

  8. 直接分配在棧上,可以自動回收,減輕GC壓力

  9. 大物件或逃逸物件無法在棧上分配
    逃逸物件:棧內物件被外部物件引用,其作用範圍脫離了當前方法棧

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 記憶體模型特性

  • 可見性:一個執行緒修改了變數,其他執行緒可以立即知道
  • 保證可見性的方法:
  1. volatile
  2. synchronized(unlock之前,寫變數值回主存)
  3. 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

執行緒重排
在writer中,兩句話順序可能打亂

  • 指令重排 – 保證有序性的方法 對方法加上同步關鍵字synchronized
  • 指令重排的基本原則
    1. 程式順序原則:一個執行緒內保證語義的序列性
    2. volatile規則:volatile變數的寫,先發生於讀
    3. 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
    4. 傳遞性:A先於B,B先於C 那麼A必然先於C
    5. 執行緒的start方法先於它的每一個動作和/方法
    6. 執行緒的所有操作先於執行緒的終結Thread.join(),最後才終結
    7. 執行緒的中斷interrupt()先於被中斷執行緒的程式碼,中斷立即停止
    8. 物件的建構函式執行結束先於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演算法

  1. 引用計數法:java中未使用
  2. 標記清除:老年代
  3. 標記壓縮:老年代
  4. 複製演算法:新生代
  5. 分代思想
    • 依據物件的存活週期進行分類,短命物件歸為新生代,長命物件歸為老年代
    • 根據不同代的特點,選取合適的收集演算法
      • 少量物件存活,適合複製演算法
      • 大量物件存活,適合標記清理或者標記壓縮

4.2 可觸及性

  1. 可觸及的
  • 從根節點可以觸及到這個物件
  • 根:(與方法區棧相關)
    • 棧中引用的物件
    • 方法區中靜態成員或者常量引用的物件(全域性物件)
    • JNI方法棧中引用物件
  1. 可復活的
  • 一旦所有引用被釋放,就是可復活狀態,即不可達
  • 但在finalize()中可能復活該物件
  1. 不可觸及的
    • 在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";
    }
}
複製程式碼
  1. 避免使用finalize方法
  2. 物件中只能呼叫一次,操作不慎可能導致錯誤
  3. 優先順序低,何時被呼叫不確定,何時發生GC不確定
  4. 可以使用try-catch-finally來替代

對於用可達性分析法搜尋不到的物件,GC並不一定會回收該物件。要完全回收一個物件,至少需要經過兩次標記的過程。
第一次標記:對於一個沒有其他引用的物件,篩選該物件是否有必要執行finalize()方法,如果沒有執行必要,則意味可直接回收。(篩選依據:是否複寫或執行過finalize()方法;因為finalize方法只能被執行一次)。
第二次標記:如果被篩選判定位有必要執行,則會放入FQueue佇列,並自動建立一個低優先順序的finalize執行緒來執行釋放操作。如果在一個物件釋放前被其他物件引用,則該物件會被移除FQueue佇列

4.3 Stop-The-World

Java中一種全域性暫停的現象
全域性停頓,所有Java程式碼停止,native程式碼可以執行,但不能和JVM互動
多半由於GC引起,也可以是Dump執行緒、死鎖檢查、堆Dump

4.4 序列蒐集器

  1. 最古老,最穩定
  2. 效率高
  3. 可能會產生較長的停頓
  4. 適用於資料量較小,對響應時間無要求的小型應用
  5. -XX:+UseSerialGC
    • 新生代、老年代使用序列回收
    • 新生代複製演算法
    • 老年代標記-壓縮
      序列蒐集器

4.5 並行收集器

適用於對吞吐量有較高要求, 多CPU、對應用響應時間無要求的中、大型應用。
舉例:後臺處理、科學計算 吞吐量:=執行使用者程式碼時間/(執行使用者程式碼時間+GC時間)

  • 併發Concurrent:交替做不同事的能力,使用者程式可以不暫停,不一定並行,但可以交替執行
  • 並行Parallel:同時做不同事的能力,垃圾回收執行緒並行工作,但應用程式等待暫停

4.5.1 ParNew

  • -XX:+UseParNewGC
    • 新生代並行
    • 老年代序列
  • Serial收集器新生代的並行版本
  • 複製演算法
  • 多執行緒,需要多核支援
  • -XX:ParallelGCThreads 限制執行緒數量
    ParNew
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收集器(可自定義的、靈活)

  1. 類似ParNew
  2. 新生代複製演算法
  3. 老年代 標記-壓縮
  4. 更加關注吞吐量
  5. -XX:+UseAdaptiveSizePolicy自適應調節策略是Parallel與ParNew的重要區別
  6. -XX:+UseParallelGC
    • 新生代使用Parallel收集器+ 老年代序列
  7. -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]
複製程式碼
  1. 特殊引數
    • -XX:MaxGCPauseMills
      • 最大停頓時間,單位毫秒
      • GC盡力保證回收時間不超過設定值
    • -XX:GCTimeRatio
      • 0-100的取值範圍
      • 垃圾收集時間佔總時間的比
      • 預設99,即最大允許1%時間做GC
    • 這兩個引數是矛盾的。因為停頓時間和吞吐量不可能同時調優

4.6 CMS併發收集器

適用於對響應時間有高要求,多CPU、對應用響應時間有較高要求的中、大型應用。
舉例:Web伺服器/應用伺服器、電信交換、整合開發環境

  • 特性
    Concurrent Mark Sweep 併發標記清除(與使用者執行緒一起執行 )
    標記-清除演算法(不是標記壓縮)
    併發階段會降低吞吐量(?)
    只是針對老年代收集器(新生代使用ParNew/或序列)
    -XX:+UseConcMarkSweepGC

  • 執行過程

    1. 初始標記
      根可以直接關聯到的物件
      速度快
      獨佔CPU,全域性停頓
    2. 併發標記(和使用者執行緒一起)
      標記的主要過程,標記全部物件
    3. 重新標記
      重新修正標記
      獨佔CPU,全域性停頓
    4. 併發清除(和使用者執行緒一起)
      基於標記結果,直接清除物件
      CMS併發收集器
  • 優:
    儘可能降低停頓,在併發標記過程中並不需要全域性停頓

  • 劣:

  1. 會影響系統整體吞吐量和效能
    • 比如,在使用者執行緒執行過程中,分一半CPU去做GC,系統效能在GC階段,反應速度就下降一半
  2. 清理不徹底
    • 在清理階段,使用者執行緒還在執行,會產生新的垃圾,無法清理,因為和使用者執行緒一起執行,不能在空間快滿時再清理
    • -XX:CMSInitiatingOccupancyFraction設定觸發GC的閾值
    • 如果不幸記憶體預留空間不夠,就會引起concurrent mode failure,此時應該使用序列收集器作為後備,由於空間不足,此時一般停頓時間較長
  • 碎片清理問題
    CMS使用的是標記-清除演算法,在清除後堆記憶體有效物件地址不連續,有記憶體碎片存在,故可設定記憶體壓縮,整理記憶體碎片 即CMS為了效能考慮在老年代使用標記-清除演算法,但仍可以設定使用標記-壓縮演算法
  1. -XX:+ UseCMSCompactAtFullCollectionFull GC後,進行一次整理
    • 整理過程是獨佔的,會引起停頓時間變長
  2. -XX:+CMSFullGCsBeforeCompaction
    • 設定進行幾次Full GC後,進行一次碎片整理
  3. -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
    1. 當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC);
    2. 如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;
    3. 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可以定製,滿足不同的位元組碼流獲取方式(比如網路)

jdk預設類載入過程

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流的格式是正確的
    1. 檔案格式的驗證
      • 是否以0xCAFEBABE開頭
      • 版本號是否合理:class檔案由什麼版本jdk編譯生成,與執行class的jdk是否相容
    2. 後設資料驗證(基本資訊驗證)
      • 是否有父類:class中指定了父類,檢查父類是否存在
      • 繼承了final類?
      • 非抽象類實現了所有的抽象方法
    3. 位元組碼驗證 (複雜)
      • 執行檢查
      • 棧資料型別和操作碼資料引數吻合
      • 跳轉指令指定到合理的位置
    4. 符號引用驗證
      • 常量池中描述類是否存在:引用的類必須存在
      • 訪問的方法或欄位是否存在且有足夠的許可權:private…
  • 準備
    1. 分配記憶體,併為類設定初始值 (方法區中)
      • 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的引數

jps命令

6.1.2 jinfo

檢視程式引數 可以用來檢視正在執行的Java應用程式的擴充套件引數,甚至支援在執行時,修改部分引數
-flag 程式ID:列印指定JVM的引數值
-flag [+|-] 程式ID:設定指定JVM引數的布林值
-flag = 程式ID:設定指定JVM引數的值

jinfo

6.1.3 jmap

生成Java應用程式的堆快照和物件的統計資訊

jmap

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選項

jstack

6.1.5 JConsole

圖形化監控工具
可以檢視Java應用程式的執行概況,監控堆資訊、永久區使用情況、類載入情況等

JConsole

6.1.6 Visual VM

Visual VM是一個功能強大的多合一故障診斷和效能監控的視覺化工具

Visual VM

6.1.7 MAT

MAT

6.2 Java堆分析

  • 記憶體溢位OOM原因
    Jvm記憶體區間:堆、永久區、執行緒棧、直接記憶體
    堆+執行緒棧 +直接記憶體<= 作業系統可分配空間
  1. 堆溢位
    佔用大量堆空間,直接溢位
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)
複製程式碼

解決方法:增大堆空間,及時釋放記憶體,分批處理

  1. 永久區溢位
//生成大量的類
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回收

  1. 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

注意:堆+執行緒棧+直接記憶體 <= 作業系統可分配空間

  1. 直接記憶體溢位
    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)。

物件記憶體結構
Mark Word 32bit

7.3 偏向鎖

隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)
大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。偏向鎖只能在單執行緒下起作用
偏向鎖在鎖物件的物件頭中有個ThreadId欄位,這個欄位如果是空的,第一次獲取鎖的時候,就將自身的ThreadId寫入到鎖的ThreadId欄位內,將鎖頭內的是否偏向鎖的狀態位置1.,這樣下次獲取鎖的時候,直接檢查ThreadId是否和自身執行緒Id一致,如果一致,則認為當前執行緒已經獲取了鎖,因此不需再次獲取鎖,略過了輕量級鎖和重量級鎖的加鎖階段。提高了效率。

  1. 大部分情況是沒有競爭的,所以可以通過偏向來提高效能
  2. 所謂的偏向,就是偏心,即鎖會偏向於當前已經佔有鎖的執行緒
  3. 將物件頭Mark的標記設定為偏向,並將執行緒ID寫入物件頭Mark
  4. 只要沒有競爭,獲得偏向鎖的執行緒,在將來進入同步塊,不需要做同步
  5. 當其他執行緒請求相同的鎖時,偏向模式結束,在全域性安全點(在這個時間點上沒有位元組碼正在執行)撤銷偏向鎖,採用其他鎖
  6. -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加鎖
    每一個執行緒在準備獲取共享資源時:
  1. 檢查MarkWord裡面是不是放的自己的ThreadId ,如果是,表示當前執行緒是處於 “偏向鎖”
  2. 如果MarkWord不是自己的ThreadId,鎖升級,這時候,用CAS來執行切換,新的執行緒根據MarkWord裡面現有的ThreadId,通知之前執行緒暫停,之前執行緒將Markword的內容置為空。
  3. 兩個執行緒都把物件的HashCode複製到自己新建的用於儲存鎖的記錄空間,接著開始通過CAS操作,把共享物件的MarKword的內容修改為自己新建的記錄空間的地址的方式競爭MarkWord,
  4. 第三步中成功執行CAS的獲得資源,失敗的則進入自旋
  5. 自旋的執行緒在自旋過程中,成功獲得資源(即之前獲的資源的執行緒執行完成並釋放了共享資源),則整個狀態依然處於 輕量級鎖的狀態,如果自旋失敗
  6. 進入重量級鎖的狀態,這個時候,自旋的執行緒進行阻塞,等待之前執行緒執行完成並喚醒自己

八.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

Class檔案結構

九. 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:預設,混合

相關文章