實戰 Java 16 值類型 Record - 1. Record 的默認方法使用以及基於預編譯生成相關字節碼的底層實現

在之前的 JEP 嚐鮮系列中,我們介紹了 Java Project Valhalla 以及 Java 值類型,經過 Java 14,15,16 的不斷開發優化反饋,終於 Java 16 我們迎來了 Java 值類型的最終版設計,可以正式在生產使用 Java 值類型相關 API 也就是 Record 這個類了。

相關資料:

但是,使用這個值類型 Record 替代原有的所有 Pojo 類,會遇到很多問題。這些問題包括:

  1. 由於值類型沒有原來普通 Object 的對象頭等信息,所以對於一些 Object 的特性是不兼容的。
  2. 我們目前使用 Java 開發不可能不使用很多三方 jar 包,各種庫。這些庫中使用的 Pojo 類型並沒有使用值類型。不過,不用太擔心,只要這些開源庫還比較活躍,那麼一定早晚會兼容值類型的。

Record 的產生背景

Record 要解決的問題最主要的一點就是,讓Java適應現代硬件:在 Java 語言發佈之初一次內存訪問和一次數字計算的消耗時間是差不多的,但是現在,一次內存訪問耗時大概是一次數值計算的 200 ~ 1000 倍。從語言設計上來說,也就是間接訪問帶來的通過指針獲取的需要操作的內存,對於整體性能影響很大。

Java 是基於對象的語言,也就是說,Java 是一種基於指針的間接引用的語言。這個基於指針的特性,給每個對象帶來了唯一標識性。例如判斷兩個 Object 的 ==,其實判斷的是兩個對象的內存相對映射地址是否相同,儘管兩個對象的 field 完全一樣,他們的內存地址也不同。同時這個特性也給對象帶來了多態性易變性還有的特性。但是,並不是所有對象都需要這種特性。

由於指針與間接訪問帶來了性能瓶頸,Java 準備對於不需要以上提到的特性的對象移除這些特性。於是乎, Record 出現了。

快速上手 Record 類

我們先舉一個簡單例子,聲明一個用戶 Record。

public record User(long id, String name, int age) {}

這樣編寫代碼之後,Record 類默認包含的元素和方法實現包括:

  1. record 頭指定的組成元素(int id, String name, int age),並且,這些元素都是 final 的。
  2. record 默認只有一個構造器,是包含所有元素的構造器。
  3. record 的每個元素都有一個對應的 getter(但這種 getter 並不是 getxxx(),而是直接用變量名命名,所以使用序列化框架,DAO 框架都要注意這一點)
  4. 實現好的 hashCode(),equals(),toString() 方法(通過自動在編譯階段生成關於 hashCode(),equals(),toString() 方法實現的字節碼實現)。

我們來使用下這個 Record :

User zhx = new User(1, "zhx", 29);
User ttj = new User(2, "ttj", 25);
System.out.println(zhx.id());//1
System.out.println(zhx.name());//zhx
System.out.println(zhx.age());//29
System.out.println(zhx.equals(ttj));//false
System.out.println(zhx.toString());//User[id=1, name=zhx, age=29]
System.out.println(zhx.hashCode());//3739156

Record 的結構是如何實現的

編譯後插入相關域與方法的字節碼

查看上面舉得例子的字節碼,有兩種方式,一是通過 javap -v User.class 命令查看文字版的字節碼,截取重要的字節碼如下所示:

//省略文件頭,文件常量池部分
{
  //public 構造器,全部屬性作爲參數,並給每個 Field 賦值
  public com.github.hashzhang.basetest.User(long, java.lang.String, int);
    descriptor: (JLjava/lang/String;I)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=5, args_size=4
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: aload_0
         5: lload_1
         6: putfield      #7                  // Field id:J
         9: aload_0
        10: aload_3
        11: putfield      #13                 // Field name:Ljava/lang/String;
        14: aload_0
        15: iload         4
        17: putfield      #17                 // Field age:I
        20: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      21     0  this   Lcom/github/hashzhang/basetest/User;
            0      21     1    id   J
            0      21     3  name   Ljava/lang/String;
            0      21     4   age   I
    MethodParameters:
      Name                           Flags
      id
      name
      age

  //public final 修飾的 toString 方法
  public final java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         //核心實現是這個 invokedynamic,我們後面會分析
         1: invokedynamic #21,  0             // InvokeDynamic #0:toString:(Lcom/github/hashzhang/basetest/User;)Ljava/lang/String;
         6: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/github/hashzhang/basetest/User;
  //public final 修飾的 hashCode 方法
  public final int hashCode();
    descriptor: ()I
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         //核心實現是這個 invokedynamic,我們後面會分析
         1: invokedynamic #25,  0             // InvokeDynamic #0:hashCode:(Lcom/github/hashzhang/basetest/User;)I
         6: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/github/hashzhang/basetest/User;
  //public final 修飾的 equals 方法
  public final boolean equals(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Z
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         //核心實現是這個 invokedynamic,我們後面會分析
         2: invokedynamic #29,  0             // InvokeDynamic #0:equals:(Lcom/github/hashzhang/basetest/User;Ljava/lang/Object;)Z
         7: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Lcom/github/hashzhang/basetest/User;
            0       8     1     o   Ljava/lang/Object;
  //public 修飾的 id 的 getter
  public long id();
    descriptor: ()J
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field id:J
         4: lreturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/github/hashzhang/basetest/User;
  //public 修飾的 name 的 getter
  public java.lang.String name();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #13                 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/github/hashzhang/basetest/User;
  //public 修飾的 age 的 getter
  public int age();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #17                 // Field age:I
         4: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/github/hashzhang/basetest/User;
}
SourceFile: "User.java"
Record:
  long id;
    descriptor: J

  java.lang.String name;
    descriptor: Ljava/lang/String;

  int age;
    descriptor: I

//以下是 invokedynamic 會調用的方法以及參數信息,我們後面會分析
BootstrapMethods:
  0: #50 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava
/lang/Object;
    Method arguments:
      #8 com/github/hashzhang/basetest/User
      #57 id;name;age
      #59 REF_getField com/github/hashzhang/basetest/User.id:J
      #60 REF_getField com/github/hashzhang/basetest/User.name:Ljava/lang/String;
      #61 REF_getField com/github/hashzhang/basetest/User.age:I
InnerClasses:
  public static final #67= #63 of #65;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

另一種是通過 IDE 的 jclasslib 插件查看,我推薦使用這種方法,查看效果如下:

自動生成的 private final field

自動生成的全屬性構造器

自動生成的 public getter 方法

自動生成的 hashCode(),equals(),toString() 方法

這些方法的核心就是 invokedynamic

看上去貌似是調用另外一個方法,這種間接調用難道沒有性能損耗問題麼?這一點 JVM 開發者已經想到了。我們先來來了解下 invokedynamic

invokedynamic 產生的背景

Java 最早是一種靜態類型語言,也就是說它的類型檢查的主體過程主要是在編譯期而不是運行期。爲了兼容動態類型語法,也是爲了 JVM 能夠兼容動態語言(JVM 設計初衷並不是只能運行 Java),在 Java 7 引入了字節碼指令 invokedynamic。這也是後來 Java 8 的拉姆達表達式以及 var 語法的實現基礎。

invokedynamic 與 MethodHandle

invokedynamic 離不開對 java.lang.invoke 包的使用。這個包的主要目的是在之前單純依靠符號引用來確定調用的目標方法這種方式以外,提供一種新的動態確定目標方法的機制,稱爲MethodHandle

通過 MethodHandle 可以動態獲取想調用的方法進行調用,和 Java Reflection 反射類似,但是爲了追求性能效率,需要用 MethodHandle,主要原因是: Reflection 僅僅是 Java 語言上補充針對反射的實現,並沒有考慮效率的問題,尤其是 JIT 基本無法針對這種反射調用進行有效的優化MethodHandle 更是像是對於字節碼的方法指令調用的模擬,適當使用的話 JIT 也能對於它進行優化,例如將 MethodHandle 相關方法引用聲明爲 static final 的:

private static final MutableCallSite callSite = new MutableCallSite(
        MethodType.methodType(int.class, int.class, int.class));
private static final MethodHandle invoker = callSite.dynamicInvoker();

自動生成的 toString(), hashcode(), equals() 的實現

通過字節碼可以看出 incokedynamic 實際調用的是 BoostrapMethods 中的 #0 方法:

0 aload_0
1 invokedynamic #24 <hashCode, BootstrapMethods #0>
6 ireturn

Bootstap 方法表包括:

BootstrapMethods:
  //調用的實際是 java.lang.runtime.ObjectMethods 的 boostrap 方法
  0: #50 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava
/lang/Object;
    Method arguments:
      #8 com/github/hashzhang/basetest/User
      #57 id;name;age
      #59 REF_getField com/github/hashzhang/basetest/User.id:J
      #60 REF_getField com/github/hashzhang/basetest/User.name:Ljava/lang/String;
      #61 REF_getField com/github/hashzhang/basetest/User.age:I
InnerClasses:
  //聲明 MethodHandles.Lookup 爲 final,加快調用性能,這樣調用 BootstrapMethods 裏面的方法可以實現近似於直接調用的性能 
  public static final #67= #63 of #65;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

從這裏,我們就能看出,實際上 toString() 調用的是 java.lang.runtime.ObjectMethodsbootstap() 方法。其核心代碼是: ObjectMethods.java

public static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type,
                                   Class<?> recordClass,
                                   String names,
                                   MethodHandle... getters) throws Throwable {
        MethodType methodType;
        if (type instanceof MethodType)
            methodType = (MethodType) type;
        else {
            methodType = null;
            if (!MethodHandle.class.equals(type))
                throw new IllegalArgumentException(type.toString());
        }
        List<MethodHandle> getterList = List.of(getters);
        MethodHandle handle;
        //根據 method 名稱,處理對應的邏輯,分別對應了 equals(),hashCode(),toString() 的實現
        switch (methodName) {
            case "equals":
                if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class)))
                    throw new IllegalArgumentException("Bad method type: " + methodType);
                handle = makeEquals(recordClass, getterList);
                return methodType != null ? new ConstantCallSite(handle) : handle;
            case "hashCode":
                if (methodType != null && !methodType.equals(MethodType.methodType(int.class, recordClass)))
                    throw new IllegalArgumentException("Bad method type: " + methodType);
                handle = makeHashCode(recordClass, getterList);
                return methodType != null ? new ConstantCallSite(handle) : handle;
            case "toString":
                if (methodType != null && !methodType.equals(MethodType.methodType(String.class, recordClass)))
                    throw new IllegalArgumentException("Bad method type: " + methodType);
                List<String> nameList = "".equals(names) ? List.of() : List.of(names.split(";"));
                if (nameList.size() != getterList.size())
                    throw new IllegalArgumentException("Name list and accessor list do not match");
                handle = makeToString(recordClass, getterList, nameList);
                return methodType != null ? new ConstantCallSite(handle) : handle;
            default:
                throw new IllegalArgumentException(methodName);
        }
    }

其中,toString() 方法 的核心實現邏輯,就要看case "toString" 這一分支了,核心邏輯是makeToString(recordClass, getterList, nameList)

private static MethodHandle makeToString(Class<?> receiverClass,
                                            //所有的 getter 方法
                                            List<MethodHandle> getters,
                                            //所有的 field 名稱
                                            List<String> names) {
    assert getters.size() == names.size();
    int[] invArgs = new int[getters.size()];
    Arrays.fill(invArgs, 0);
    MethodHandle[] filters = new MethodHandle[getters.size()];
    StringBuilder sb = new StringBuilder();
    //先拼接類名稱[
    sb.append(receiverClass.getSimpleName()).append("[");
    for (int i=0; i<getters.size(); i++) {
        MethodHandle getter = getters.get(i); // (R)T
        MethodHandle stringify = stringifier(getter.type().returnType()); // (T)String
        MethodHandle stringifyThisField = MethodHandles.filterArguments(stringify, 0, getter);    // (R)String
        filters[i] = stringifyThisField;
        //之後拼接 field 名稱=值
        sb.append(names.get(i)).append("=%s");
        if (i != getters.size() - 1)
            sb.append(", ");
    }
    sb.append(']');
    String formatString = sb.toString();
    MethodHandle formatter = MethodHandles.insertArguments(STRING_FORMAT, 0, formatString)
                                          .asCollector(String[].class, getters.size()); // (R*)String
    if (getters.size() == 0) {
        // Add back extra R
        formatter = MethodHandles.dropArguments(formatter, 0, receiverClass);
    }
    else {
        MethodHandle filtered = MethodHandles.filterArguments(formatter, 0, filters);
        formatter = MethodHandles.permuteArguments(filtered, MethodType.methodType(String.class, receiverClass), invArgs);
    }
    return formatter;
}

同理,hashcode() 實現是:

private static MethodHandle makeHashCode(Class<?> receiverClass,
                                            List<MethodHandle> getters) {
    MethodHandle accumulator = MethodHandles.dropArguments(ZERO, 0, receiverClass); // (R)I

    // 對於每一個 field,找到對應的 hashcode 方法,取 哈希值,最後組合在一起
    for (MethodHandle getter : getters) {
        MethodHandle hasher = hasher(getter.type().returnType()); // (T)I
        MethodHandle hashThisField = MethodHandles.filterArguments(hasher, 0, getter);    // (R)I
        MethodHandle combineHashes = MethodHandles.filterArguments(HASH_COMBINER, 0, accumulator, hashThisField); // (RR)I
        accumulator = MethodHandles.permuteArguments(combineHashes, accumulator.type(), 0, 0); // adapt (R)I to (RR)I
    }

    return accumulator;
}

同理,equals() 實現是:

private static MethodHandle makeEquals(Class<?> receiverClass,
                                          List<MethodHandle> getters) {
        MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass);
        MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class);
        MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z
        MethodHandle instanceTrue = MethodHandles.dropArguments(TRUE, 0, receiverClass, Object.class); // (RO)Z
        MethodHandle isSameObject = OBJECT_EQ.asType(ro); // (RO)Z
        MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z
        MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z
        //對比兩個對象的每個 field 的 getter 獲取的值是否一樣,對於引用類型通過 Objects.equals 方法,對於原始類型直接通過 == 
        for (MethodHandle getter : getters) {
            MethodHandle equalator = equalator(getter.type().returnType()); // (TT)Z
            MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z
            accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr));
        }

        return MethodHandles.guardWithTest(isSameObject,
                                           instanceTrue,
                                           MethodHandles.guardWithTest(isInstance, accumulator.asType(ro), instanceFalse));
    }

微信搜索“我的編程喵”關注公衆號,每日一刷,輕鬆提升技術,斬獲各種offer

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章