1. 程式人生 > >Java虛擬機器(十六)--類方法解析

Java虛擬機器(十六)--類方法解析

Java方法的解析大體上分為3步:

  1. 在Java原始碼編譯期間,編譯器負責將Java類原始碼翻譯為對應的位元組碼指令,同時完成的工作還有Java方法的區域性變量表的計算,以及最大運算元棧的計算。
  2. 在jvm執行期間,jvm載入型別,呼叫classFileParser:parseClassFile()函式對Java class位元組碼檔案進行解析,這一步將會完成Java方法的解析、位元組碼指令存放、父類與介面類方法繼承與過載等一系列邏輯。
  3. 在呼叫系統類載入器SCL對應用程式的Java類進行載入過程中,完成方法符號連結、驗證,最重要的是完成vtable與itable的構建,從而支援在jvm執行期的方法動態繫結。

對於步驟2,解析方法時,先從位元組碼檔案中獲取方法總數。 然後遍歷每個方法,建立相應的mehodoop(也就是方法物件)。 建立方法物件的步驟主要有以下幾步:

  1. 讀取和驗證Java方法的訪問標識、名稱以及描述符
  2. 解析方法的屬性
  3. 建立methodoop
  4. 複製位元組碼

方法的訪問標識,名稱、描述符構成方法的簽名。這3個各佔2個位元組。

屬性常見的有Code,LVT(區域性變量表),具體內容可以看十三篇的附錄。

methodOop物件中儲存方法的訪問標識,虛方法表索引,方法大小,方法名等資訊,內部constMethodOop儲存了方法的位元組碼,區域性變量表,異常表,位元組碼與原始碼行對應值、最大運算元棧等資訊。

< init>方法是編譯器自動生成的構造方法。 在沒有明確定義構造方法時,init方法就是構造方法,所有類變數非靜態的都在這裡初始化。所有非靜態程式碼塊的內容也在這執行

其實在位元組碼層次上,所有的構造方法都是叫init,只是引數不同。不管是手工生成的,還是編譯器自動生成的。 如果定義了多個構造方法,那麼每個構造方法其實都會初始化類變數。 當然,一個構造方法明確呼叫另一個構造方法的話,只會在被呼叫的構造方法中做類變數的初始化工作。

< clint>方法也是編譯器自動生成的,執行所有靜態變數的初始化以及靜態程式碼塊。

vtable(虛擬函式表)是Java多型中的一個十分關鍵的技術。

在jvm載入類的過程中,會動態解析Java類的方法及其對父類方法的重寫,進而構建出一個vtable分配到instanceKlass記憶體區的尾部,從而支援執行期動態繫結。

計算vtable主要分為2步:

  1. 獲取父類vtable大小,並將當前類的vtable的設定為父類vtable的大小
  2. 迴圈遍歷當前Java類的每一個方法,並呼叫need_new_vtable_entry()函式判斷,如果是true,則vtable增加1

need_vtable_entry():

  1. 檢查方法有沒有被final,static修飾,或者是建構函式,或者類被final修飾。滿足一條都返回false 2.檢查方法是否被private修飾,如果是則返回true
  2. 檢查父類有沒有這個方法,父類方法是不是public或者protect的。如果是,也就是說子類重寫了父類的方法,返回false

如果Java類中聲明瞭一個非private,非final,非static的方法:如果該方法是對父類方法的重寫,那麼就直接更新從父類繼承來的vtable的方法指標; 如果不是對父類方法的重寫,那麼向這個類的vtable中新增一個位置,指向該方法的記憶體位置。

vtable分配在instanceKlass物件例項末尾,而instance在jdk8中固定佔用0x1b8大小,所以得到類對應的InstanceKlass例項地址,就可以得到虛方法表。 比如有下面2個類, Test1和p

public class Test1 extends p{
    public int a=0;
    static Integer si=6;
    String s="Hello world";

    public Test1(int d){

    }

     final void do1(){
        a++;
        System.out.println(a);

    }
    public static void main(String[] args) {
        Test1 test1=new Test1(4);
        test1.do1();
        test1.a=8;
        si=9;
        int d=si;
    }

    private void test(){
        this.a=a;
    }
}
class p{
     void do1(){}
}

在這裡插入圖片描述

是hsdb檢視記憶體,可以發現 Test1的虛方法表長度為7 在這裡插入圖片描述 父類p的虛方法表長度為6 在這裡插入圖片描述 檢視記憶體中相應的地址,可以看到從object類中繼承來的前5個方法都是一樣的,後面的方法不一樣。 在這裡插入圖片描述

檢視父類p中第6個方法,對應的名字,是常量池第11個元素,也就是do1 在這裡插入圖片描述 子類Test1中虛方法表中最後一個方法就是那個私有方法test()。

miranda方法 指那些該類所實現的介面(間接實現也算)中沒有在當前類中實現的方法。會被新增到vtable中,當然這個類一定是abtract的,不然必須要實現。 如果一個米蘭達方法已經在父類的vtable中了,那麼子類不需要新增,只需要修改就行了。

那麼vtable中會有哪些方法呢? 當前類中宣告的方法method,

如果method是被static修飾,那麼一定不在。 如果被final修飾,那要看父類的vtable中有沒有這個方法,有的話就把地址替換(因為子類會先複製父類的vtable)。沒有的話,那麼這個方法就不在vtable中,因為是這個類獨有的,不會被覆蓋,也就不需要轉發。

從修飾符的角度來說private修飾的方法,一定在vtable中,而且是新增的。