1. 程式人生 > 實用技巧 >java動態代理——jvm指令集基本概念和方法位元組碼結構的進一步探究及proxy原始碼分析四

java動態代理——jvm指令集基本概念和方法位元組碼結構的進一步探究及proxy原始碼分析四

前文地址

https://www.cnblogs.com/tera/p/13336627.html

本系列文章主要是博主在學習spring aop的過程中瞭解到其使用了java動態代理,本著究根問底的態度,於是對java動態代理的本質原理做了一些研究,於是便有了這個系列的文章

上一篇文章詳細分析了class位元組碼結構中的field_info和method_info,以及對應的Proxy的原始碼。本文將會更詳細的分析method_info中的方法執行體部分

因為方法的位元組碼涉及到了jvm的操作指令,因此我們先做一個基礎性的瞭解

原文地址:https://dzone.com/articles/introduction-to-java-bytecode

jvm指令文件:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
文中開始介紹的堆、棧、方法區等概念這裡就不詳細描述了,主要看它後面對一些簡單方法的位元組碼的解析
首先我們定義一個簡單的類

public class Test {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
    }
}

編譯生成Test.class

javac Test.java

檢視位元組碼結構

javap -v Test.class

我們關注其中的main方法部分

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         
5: iload_2 6: iadd 7: istore_3 8: return LineNumberTable: line 3: 0 line 4: 2 line 5: 4 line 6: 8

其中的Code正是方法的執行體,下面按照順序圖解具體操作

iconst_1:將常量1壓入操作棧

istore_1:彈出棧頂的運算元,存入棧的本地變數陣列的索引1,也就是變數a

iconst_2:將常量2壓入操作棧

istore_2:彈出棧頂的運算元,存入棧的本地變數陣列的索引2,也就是變數b

iload_1:從本地變數索引1種讀取值,並壓入操作棧

iload_2:從本地變數索引2種讀取值,並壓入操作棧

iadd:彈出棧頂的2個運算元,相加後將結果壓入操作棧

istore_3:彈出棧頂的運算元,存入棧的本地變數陣列的索引3,也就是變數c

return:從方法返回

如果我們在類中定義一個方法

public class Test {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = calc(a, b);
    }
    static int calc(int a, int b) {
        return (int) Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
    }
}

得到的位元組碼如下,這次我把部分Constant pool也展示在下面

Constant pool:
   #1 = Methodref          #8.#19         // java/lang/Object."<init>":()V
   #2 = Methodref          #7.#20         // Test.calc:(II)I
   #3 = Double             2.0d
   #5 = Methodref          #21.#22        // java/lang/Math.pow:(DD)D
   #6 = Methodref          #21.#23        // java/lang/Math.sqrt:(D)D
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: invokestatic  #2                  // Method calc:(II)I
         9: istore_3
        10: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 4
        line 6: 10

  static int calc(int, int);
    descriptor: (II)I
    flags: ACC_STATIC
    Code:
      stack=6, locals=2, args_size=2
         0: iload_0
         1: i2d
         2: ldc2_w        #3                  // double 2.0d
         5: invokestatic  #5                  // Method java/lang/Math.pow:(DD)D
         8: iload_1
         9: i2d
        10: ldc2_w        #3                  // double 2.0d
        13: invokestatic  #5                  // Method java/lang/Math.pow:(DD)D
        16: dadd
        17: invokestatic  #6                  // Method java/lang/Math.sqrt:(D)D
        20: d2i
        21: ireturn
      LineNumberTable:
        line 8: 0

這裡我們主要看一下一些新出現的操作指令
在main方法中,編號6
invokestatic #2:呼叫靜態方法,方法在Constant Pool中索引為2,表示Test.calc方法(這裡特別注意,呼叫的方法目標必須是常量池中的一個有效索引)
在cacl方法中
i2d將int型別的轉換成double型別的
ldc2_w:將long型或者double型(思考一下為何是這2種類型放在同一個操作指令中)從靜態池中壓入棧
dadd:將double相加
d2i:將double型別轉換成int型別
ireturn:返回一個int

將上面的jvm指令結合java程式碼,就可以初步理解每一行java程式碼究竟是如何被jvm執行的了

接下去我們可以通過Proxy的程式碼結合實際來看看

方法還是generateClassFile()
在上一篇文章的第三部分位元組與方法位元組碼的寫入中,有提到

這裡的第一行,正是寫入構造器的位元組碼,這一部分因為涉及到jvm的執行指令,我們放到下篇文章再詳細看,所以這裡先跳過

this.methods.add(this.generateConstructor());

此時我們就可以詳細看下generateConstructor方法究竟幹了什麼

private ProxyGenerator.MethodInfo generateConstructor() throws IOException {
    ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
    DataOutputStream var2 = new DataOutputStream(var1.code);
    this.code_aload(0, var2);
    this.code_aload(1, var2);
    var2.writeByte(183);
    var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));
    var2.writeByte(177);
    var1.maxStack = 10;
    var1.maxLocals = 2;
    var1.declaredExceptions = new short[0];
    return var1;
}

接下一行一行分析

初始化MethodInfo物件,3個引數分別是,方法名、方法描述、access_flag,1表示public(參見Modifier.java)

因為是建構函式,所以方法名為<init>

方法的描述表示,該方法獲取一個java.lang.reflect.InvocationHandler型別的引數,返回值為V(表示void)

方法的access_flag為1,表示public

ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);

寫入aload_0和aload_1操作指令

this.code_aload(0, var2);
this.code_aload(1, var2);

寫入183號操作指令,查文件得:invokespecial

呼叫例項方法,特別用來處理父類的建構函式

var2.writeByte(183);

寫入需要呼叫的方法名和方法的引數

注意,這裡的方法是通過this.cp.getMethodRef方法得到的,也就是說,這裡寫入的最終資料,其實是一個符合該方法描述的常量池中的一個有效索引(這部分知識可以參看之前的3篇文章)

var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));

寫入177號指令,查文件得:return

返回void

var2.writeByte(177);

和上一篇文章中提到的一樣,最後還需要寫入棧深和本地變數數量,以及方法會丟擲的異常數量,因為建構函式不主動丟擲異常,所以異常數量直接為0

var1.maxStack = 10;
var1.maxLocals = 2;
var1.declaredExceptions = new short[0];

到此,一個建構函式的結構就完成了

此時我們總結一下該建構函式的結構

aload_0;
aload_1;
invokespecial  #x  //這裡x對應Constant pool中建構函式的編號
return;

驗證一下,我們建立一個類

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Test extends Proxy {
    protected TestClass(InvocationHandler h) {
        super(h);
    }
}

檢視其位元組碼

protected Test(java.lang.reflect.InvocationHandler);
    descriptor: (Ljava/lang/reflect/InvocationHandler;)V
    flags: ACC_PROTECTED
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #1                  // Method java/lang/reflect/Proxy."<init>":(Ljava/lang/reflect/InvocationHandler;)V
         5: return
      LineNumberTable:
        line 6: 0
        line 7: 5

正和我們之前總結的一模一樣

結合之前的一些jvm指令的基本描述,我們就可以對method_info的正題結構有了更深入的瞭解

本文中我們初步瞭解了方法執行體Code的結構,jvm指令的基本概念,那麼在下一篇文章中,我們將會繼續探究Proxy的最核心的部分,代理方法的實際實現