天天看點

JVM學習筆記十 之 位元組碼執行引擎

一、概述

jvm spec隻給出了執行引擎的概念模型,并沒有規定具體實作細節。執行引擎在執行時候可以解釋執行、編譯執行或直接由嵌入晶片的指令執行。引擎的行為使用指令集來定義。

java的目标是一次編寫到處運作,為了達到這個目标,jvm指令集就不能依賴于任何硬體平台的指令,jvm指令集中就隻有對棧的操作,沒有對特定于硬體平台的寄存器的操作。當然jvm運作期優化的時候,可以針對不同的硬體平台提供不同的優化實作,比如充分利用硬體平台的寄存器提高通路速度。既然jvm執行引擎隻有對棧的操作,那麼我們下邊就開始了解下棧的機構。

二、棧和棧幀

棧是線程私有的記憶體區域,每個線程都有一個棧,線程生則棧生,線程亡則棧滅(這裡有一些棧的描述)。棧又由棧幀組成,每個方法調用都生成一個棧幀,方法調用結束則彈出棧幀。

棧幀又由多個部分組成:

1、局部變量表。包含方法參數和方法内部聲明的局部變量,如果是執行個體方法,還有目前對象的this引用。局部變量表的大小在編譯期就已經确定了,Locals:2即是;局部變量表所有的值也确定了,Local variable table:即是。此處可以先看class檔案中方法的屬性中局部變量表資訊:

類的執行個體方法:

public class BigObejct {
	int[] value;
	private static final int M1 = 1024 * 1024;

	public BigObejct() {
		//4 * 1m = 4m
		this.value = new int[M1];
	}
	public void setValue(int[] value){
		this.value = value;
	}
}
           

 setValue的本地變量表資訊:

// Method descriptor #23 ([I)V
  // Stack: 2, Locals: 2
  public void setValue(int[] value);
    0  aload_0 [this]
    1  aload_1 [value]
    2  putfield com.yymt.jvm.BigObejct.value : int[] [16]
    5  return
      Line numbers:
        [pc: 0, line: 11]
        [pc: 5, line: 12]
      Local variable table:
        [pc: 0, pc: 6] local: this index: 0 type: com.yymt.jvm.BigObejct
        [pc: 0, pc: 6] local: value index: 1 type: int[]
           

   運作時本地變量表怎麼檢視?整個棧幀内容怎麼檢視?在eclipse中調試時候可以Variable視窗可以看到局部變量資訊,但是跟局部變量表并不是一一對應的,因為局部變量在運作期隻在start_pc之後才被建立并存活到超出作用域。

2、操作棧。出入棧操作就是對該操作數棧的操作,操作數棧的最大棧深在運作期也已經确定,1中Stack:2,表示最大棧深為2。如果考慮上運作期優化技術裡的标量替換和棧上配置設定對象,此處的棧是顯然不夠用的。後續jvm團隊會如何解決呢?

3、解析相關的資料,即指向常量池的指針。在方法運作過程中,可能會用到常量池中的表項,是以需要持有一個到常量池的引用。

4、方法調用傳回相關的資訊,記錄一些資訊恢複調用者的棧幀和計數器。需要記錄方法調用傳回後傳回到何處,調用者pc計數器指向哪條指令?方法傳回有兩種方式,正常的調用傳回和異常傳回。正常調用傳回如果有傳回值,則把傳回值壓入調用者棧中,把pc計數器指向調用者下一條指令,繼續調用者的執行。如果沒有傳回值則隻設定pc計數器。異常傳回則直接彈出棧幀,同樣恢複調用者的棧幀和計數器,調用者根據是否捕捉異常決定是彈出棧幀到上層還是捕捉異常處理。

5、異常相關資訊。棧幀中還必須儲存一個到異常表的引用,當方法抛出異常時候進行處理。

6、其他資訊,如調試相關資訊。

以上3、4、5、6一起也稱作幀資料區。

三、方法調用

分派是指根據參數和接受者?決定方法調用的版本。

1、靜态分派和動态分派

根據接受者類型和參數類型,在編譯器靜态決定調用哪個方法叫做靜态分派。根據接受者類型,在運作期動态決定調用哪個方法叫做動态分派。java中調用重載的方法屬于靜态分派,在編譯器根據類型資訊就決定了方法調用的版本。調用重寫的方法屬于動态分派,在運作期根據實際的類型資訊決定調用方法的版本。

package com.yymt.jvm.method.dispatch;
public class Dispatcher {

	static class Base {
		public void printMessage() {
			System.out.println("Base Message");
		}
	}

	static class Sub extends Base {
		public void printMessage() {
			System.out.println("Sub Message");
		}
	}

	public static void accept(Base base) {
		System.out.println("Accept Base");
	}

	public static void accept(Sub sub) {
		System.out.println("Accept Sub");
	}

	public static void staticDispatch() {
		Base base = new Base();
		Base sub = new Sub();
		accept(base);
		accept(sub);
	}

	public static void main(String[] args) {
		staticDispatch();
//		System.out.println("=========");
//		dynamicDispatch();
	}
	
	public static void dynamicDispatch() {
		Base base = new Base();
		Base b2s = new Sub();
		Sub sub = new Sub();
		base.printMessage();
		b2s.printMessage();
		sub.printMessage();
	}
}
           

    此處讀懂了靜态分派和動态分派的解釋,也就猜到結果了,即使之前沒見到過這種經典的筆試題:

Accept Base
Accept Base
           

  調用accept方法的時候都是調用的accept(Base)方法,編譯器已經根據參數的靜态類型決定了調用的方法,從位元組碼(紅色)就可以判定出來:

// Method descriptor #6 ()V
  // Stack: 2, Locals: 2
  public static void staticDispatch();
     0  new com.yymt.jvm.method.dispatch.Dispatcher$Base [38]
     3  dup
     4  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Base() [40]
     7  astore_0 [base]
     8  new com.yymt.jvm.method.dispatch.Dispatcher$Sub [41]
    11  dup
    12  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Sub() [43]
    15  astore_1 [sub]
    16  aload_0 [base]
    17  invokestatic com.yymt.jvm.method.dispatch.Dispatcher.accept(com.yymt.jvm.method.dispatch.Dispatcher$Base) : void [44]
    20  aload_1 [sub]
    21  invokestatic com.yymt.jvm.method.dispatch.Dispatcher.accept(com.yymt.jvm.method.dispatch.Dispatcher$Base) : void [44]
    24  return
      Line numbers:
        [pc: 0, line: 26]
        [pc: 8, line: 27]
        [pc: 16, line: 28]
        [pc: 20, line: 29]
        [pc: 24, line: 30]
      Local variable table:
        [pc: 8, pc: 25] local: base index: 0 type: com.yymt.jvm.method.dispatch.Dispatcher.Base
        [pc: 16, pc: 25] local: sub index: 1 type: com.yymt.jvm.method.dispatch.Dispatcher.Base
           

雖然兩處aload_1/aload_2分别從本地變量表中将base和sub壓入棧中,但是因為invokestatic指令是直接根據後邊常量池的CONSTANT_MethodRef_info表項指向的方法調用的,此方法的直接引用是指向對應類的方法區的位元組碼的指針,是以就一定調用的是accept(Base)方法。 我們再來看看動态調用:

public static void dynamicDispatch() {
	Base base = new Base();
	Base b2s = new Sub();
	Sub sub = new Sub();
	base.printMessage();
	b2s.printMessage();
	sub.printMessage();
} 
           

這個的輸出大概都能猜出:

Base Message
Sub Message
Sub Message 
           

調用printMessage的地方,都是運作期根據方法實際類型動态決定調用哪個類的執行個體方法的:

// Method descriptor #6 ()V
  // Stack: 2, Locals: 3
  public static void dynamicDispatch();
     0  new com.yymt.jvm.method.dispatch.Dispatcher$Base [38]
     3  dup
     4  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Base() [40]
     7  astore_0 [base]
     8  new com.yymt.jvm.method.dispatch.Dispatcher$Sub [41]
    11  dup
    12  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Sub() [43]
    15  astore_1 [b2s]
    16  new com.yymt.jvm.method.dispatch.Dispatcher$Sub [41]
    19  dup
    20  invokespecial com.yymt.jvm.method.dispatch.Dispatcher$Sub() [43]
    23  astore_2 [sub]
    24  aload_0 [base]
    25  invokevirtual com.yymt.jvm.method.dispatch.Dispatcher$Base.printMessage() : void [53]
    28  aload_1 [b2s]
    29  invokevirtual com.yymt.jvm.method.dispatch.Dispatcher$Base.printMessage() : void [53]
    32  aload_2 [sub]
    33  invokevirtual com.yymt.jvm.method.dispatch.Dispatcher$Sub.printMessage() : void [56]
    36  return
      Line numbers:
        [pc: 0, line: 39]
        [pc: 8, line: 40]
        [pc: 16, line: 41]
        [pc: 24, line: 42]
        [pc: 28, line: 43]
        [pc: 32, line: 44]
        [pc: 36, line: 45]
      Local variable table:
        [pc: 8, pc: 37] local: base index: 0 type: com.yymt.jvm.method.dispatch.Dispatcher.Base
        [pc: 16, pc: 37] local: b2s index: 1 type: com.yymt.jvm.method.dispatch.Dispatcher.Base
        [pc: 24, pc: 37] local: sub index: 2 type: com.yymt.jvm.method.dispatch.Dispatcher.Sub
           

從上邊的位元組碼出看兩處aload_0/aload_1/aload_2分别從本地變量表中将base、b2s和sub引用壓入棧中,invokevirtual指令會根據引用去引用指向的對象的類的方法表中查找具有相同名稱和描述符的方法,如果找到了則直接調用;如果沒有找到則去其父類的方法表中找,如果找到了則調用;如果沒有找到繼續向繼承關系上級去找,如果找不到就抛出java.lang.AbstractMethodError。問題是,invokevirtual指令後邊Constant_Methodref_info的直接引用是方法表的偏移量,在子類找和在父類查找的時候,怎麼確定同樣的偏移量指向的是相同簽名的方法?如b2s和sub執行個體都指向相同的方法入口。下邊講到虛拟機動态分派的時候會講到。

此處方法調用的直接引用是特定于hotspot vm的實作的。

2、單分派和多分派

先解釋個名詞,總量:方法的接受者和方法的參數一起被稱作宗量。分派時候根據影響方法調用的宗量個數不同,分派又分為單分派和多分派,如果方法調用隻受一個宗量影響的叫單分派,受多個宗量影響的叫多分派。來這裡了解更多。Java語言目前為止屬于靜态多分派,動态單分派,根據java語言的不斷發展也許以後會支援動态多分派的。java的靜态多分派是指,方法調用在編譯期根據方法接受者的靜态類型和參數的靜态類型共同決定;動态多分派是指,在運作期,究竟調用哪個方法隻由接受者的靜态類型決定。此處接受者在編譯期和運作期都決定了調用哪個方法,算是影響了兩次?

package com.yymt.jvm.method.dispatch;

public class SinMulDispatcher {

	public static class Car {
		public void printName() {
			System.out.println("I'm a Car");
		}
	}

	public static class BYDCar extends Car {
		public void printName() {
			System.out.println("I'm a BYD Car");
		}
	}

	public static class Father {
		public void chooseCar(Car car) {
			System.out.println("Father choose Car");

		}

		public void chooseCar(BYDCar car) {
			System.out.println("Father choose BYDCar");
		}
	}

	public static class Son extends Father {
		public void chooseCar(Car car) {
			System.out.println("Son choose Car");
		}

		public void chooseCar(BYDCar car) {
			System.out.println("Son choose BYDCar");
		}
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Car car = new Car();
		Car byd = new BYDCar();
		
		Father father = new Father();
		Father son = new Son();
		
		father.chooseCar(car);
		son.chooseCar(byd);
	}

}
           

  輸出為:

Father choose Car
Son choose Car
           

在編譯期,根據接受者靜态類型Father和參數靜态類型Car,一同決定了調用Father.chooseCar(Car),而不是Object.chooseCar:

// Method descriptor #15 ([Ljava/lang/String;)V
  // Stack: 2, Locals: 5
  public static void main(java.lang.String[] args);
     0  new com.yymt.jvm.method.dispatch.SinMulDispatcher$Car [16]
     3  dup
     4  invokespecial com.yymt.jvm.method.dispatch.SinMulDispatcher$Car() [18]
     7  astore_1 [car]
     8  new com.yymt.jvm.method.dispatch.SinMulDispatcher$BYDCar [19]
    11  dup
    12  invokespecial com.yymt.jvm.method.dispatch.SinMulDispatcher$BYDCar() [21]
    15  astore_2 [byd]
    16  new com.yymt.jvm.method.dispatch.SinMulDispatcher$Father [22]
    19  dup
    20  invokespecial com.yymt.jvm.method.dispatch.SinMulDispatcher$Father() [24]
    23  astore_3 [father]
    24  new com.yymt.jvm.method.dispatch.SinMulDispatcher$Son [25]
    27  dup
    28  invokespecial com.yymt.jvm.method.dispatch.SinMulDispatcher$Son() [27]
    31  astore 4 [son]
    33  aload_3 [father]
    34  aload_1 [car]
    35  invokevirtual com.yymt.jvm.method.dispatch.SinMulDispatcher$Father.chooseCar(com.yymt.jvm.method.dispatch.SinMulDispatcher$Car) : void [28]
    38  aload 4 [son]
    40  aload_2 [byd]
    41  invokevirtual com.yymt.jvm.method.dispatch.SinMulDispatcher$Father.chooseCar(com.yymt.jvm.method.dispatch.SinMulDispatcher$Car) : void [28]
    44  return
      Line numbers:
        [pc: 0, line: 42]
        [pc: 8, line: 43]
        [pc: 16, line: 45]
        [pc: 24, line: 46]
        [pc: 33, line: 48]
        [pc: 38, line: 49]
        [pc: 44, line: 50]
      Local variable table:
        [pc: 0, pc: 45] local: args index: 0 type: java.lang.String[]
        [pc: 8, pc: 45] local: car index: 1 type: com.yymt.jvm.method.dispatch.SinMulDispatcher.Car
        [pc: 16, pc: 45] local: byd index: 2 type: com.yymt.jvm.method.dispatch.SinMulDispatcher.Car
        [pc: 24, pc: 45] local: father index: 3 type: com.yymt.jvm.method.dispatch.SinMulDispatcher.Father
        [pc: 33, pc: 45] local: son index: 4 type: com.yymt.jvm.method.dispatch.SinMulDispatcher.Father
           

  在運作期,invokevirtual指令根據前邊壓入的調用者類型,動态決定分别調用了Father.chooseCar(Car)和Son.chooseCar(Car)。

3、虛拟機動态分派的實作

HotSpot VM虛拟機動态分派是通過方法表實作的。在jvm裝載完類型後連接配接階段的準備子階段,會在方法區為類變量配置設定記憶體,同時會為别的結構配置設定記憶體,如方法表。而對象在記憶體中會有一個指向方法區的指針,可以通過對象來找到對象的方法表,進而找到方法。方法表裡隻有虛方法,即非靜态、非私有、非初始化、非final的執行個體方法,也成為虛方法。常量池解析的時候,對于虛方法,直接引用會是方法表的偏移量。私有、靜态、初始化、final方法都指向方法區中方法的直接位址的,運作期這種非虛方法很容易優化,不需要動态派發。

每個類型的方法表,都會包含超類的方法。超類方法在方法表中的順序跟超類方法表順序一緻,這樣就可以實作子類方法表索引跟父類方法表索引相同時候,指向的方法也相同。

JVM學習筆記十 之 位元組碼執行引擎

四、位元組碼執行引擎

位元組碼執行是基于對操作數的出棧和入棧操作進行的,相對比較簡單,沒有寄存器。當然運作期優化時候把位元組碼編譯為本地代碼的時候,會充分利用機器的寄存器的。