JEP解讀與嚐鮮系列1 - Java Valhalla與Java Inline class

涉及到的JEP:

Valhalla項目背景

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

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

由於指針與間接訪問帶來了性能瓶頸,Java準備對於不需要這種特性的對象移除這種特性。於是乎,Value type出現了。

Value Type

Value type用於表示純數據集合。所有不需要的指針特性被移除。其實就是,Java的對象頭被移除了。

來看一個例子:

final class Point {
  final int x;
  final int y;
}

這個在內存中的結構是:

在這裏插入圖片描述

對於Value type

value class Point {
  int x;
  int y
}

這個在內存中的結構是:

在這裏插入圖片描述
我們再來對比下數組的存儲:

對於CommonObj[],只有引用是連續存儲的,實際的值:

在這裏插入圖片描述
對於Value types,數組存儲可以扁平化,不用分散存儲,採取真正的:

在這裏插入圖片描述

這樣,JVM不用再跑到堆上分配內存來存儲這種對象,而是可以直接在棧上面分配。這樣,Value types的表現,就和Java的原始類型int等就很像了。與原始類型不同的是,Value types可以有方法和fileds。

同時我們還希望能讓它作爲接口泛型。我們希望能有更廣泛的接口泛型,無論是對象,還是Value types,還是原始類型(剛纔已經說明了,利用原始類型性能更好),而不是封裝的原始類型。這就引出了,Valhalla的另一個重要更新(針對泛型):Specialized Generics

Specialized Generics

從字面上理解,其實就是指泛型不止針對對象,也需要包含Value types,還有最重要的是原始類型例如int這些。

目前(JDK14之前的),泛型必須是一個對象類。針對原始類型,也必須使用原始類型的封裝類,例如Integer之於int。這就違反了之前說的減少對象封裝,使用原始類型。所以這個優化對於Value Types的實現也是必須的。

順便一提,目前JDK框架的Java源碼也有很多使用原始類型從而提高性能的地方,例如IntStream涉及到的所有int的操作函數,傳參都是int,而不是Integer:

@FunctionalInterface
public interface IntUnaryOperator {
    int applyAsInt(int operand);
}

JDK 14 引入的關鍵字 inline:Inline Classes

這個Inline Classes實際上就是一種Value Types的實現。

我們首先回顧下,普通類對象的存儲結構:

public static void main(String args) {
    CommonObj a = new CommonObj();
}

這段代碼,會在棧上新建一個引用變量a, 在堆上面申請一塊內存用於存儲新建的CommonObj這個對象,對象包括,

  • 標記字
  • 指向class原數據的指針
  • 具體數據字段
    如圖所示:
    在這裏插入圖片描述

Inline class 嚐鮮

由於目前JDK 14 還沒發佈,我們只能通過目前開發版的OpenJDK進行嚐鮮。可以通過這裏下載全平臺的OpenJDK project Valhalla嚐鮮版:http://jdk.java.net/valhalla/

由於目前還沒開發完,我們只能通過字節碼去解讀與原始類的不同。

目前,inline class的限制是:

  • 接口,註解和枚舉不能成爲inline class
  • 公共類,內部類,靜態內部類,本地類可以作爲inline class
  • inline class不能接受空值,需要有默認值
  • 可以聲明內部類型,靜態內部類性和本地類型
  • inline class 默認隱式final的,所以不能是abstract的
  • inline class 默認隱式繼承java.lang.Object(就和enum, annotation還有interface一樣)
  • inline class 可以實現普通的interface
  • inline class 的實例的所有field默認都是final的
  • nline class不能聲明類型是自己這種類型的field
  • javac 編譯的時候,自動給inline class 生成 hashCode(), equals(), and toString()方法
  • javac 編譯的時候,會檢查並禁止是否有對於inline class的 clone(), finalize(), wait(), 或者 notify()的調用

我們來聲明一個類似於java.util.OptionalInt的類:

public inline class OptionalInt {
   private boolean isPresent;
   private int v;

   private OptionalInt(int val) {
       v = val;
       isPresent = true;
   }

   public static OptionalInt empty() {
       // New semantics for inline classes
       return OptionalInt.default;
   }

   public static OptionalInt of(int val) {
       return new OptionalInt(val);
   }

   public int getAsInt() {
       if (!isPresent)
           throw new NoSuchElementException("No value present");
       return v;
   }

   public boolean isPresent() {
       return isPresent;
   }

   public void ifPresent(IntConsumer consumer) {
       if (isPresent)
           consumer.accept(v);
   }

   public int orElse(int other) {
       return isPresent ? v : other;
   }

   @Override
   public String toString() {
       return isPresent
               ? String.format("OptionalInt[%s]", v)
               : "OptionalInt.empty";
   }
}

編譯後,我們反編譯一下代碼,查看下,發現:

public final value class OptionalInt {
 private final boolean isPresent;

 private final int v;

class 變成 value class修飾,同時,按照之前的約束,這裏多了final修飾符。同時,所有的field也多了final修飾。

然後是構造器部分:

public static OptionalInt empty();
    Code:
       0: defaultvalue  #1                  // class OptionalInt
       3: areturn

  public static OptionalInt of(int);
    Code:
       0: iload_0
       1: invokestatic  #11                 // Method "<init>":(I)OptionalInt;
       4: areturn

  private static OptionalInt OptionalInt(int);
    Code:
       0: defaultvalue  #1                  // class OptionalInt
       3: astore_1
       4: iload_0
       5: aload_1
       6: swap
       7: withfield     #3                  // Field v:I
      10: astore_1
      11: iconst_1
      12: aload_1
      13: swap
      14: withfield     #7                  // Field isPresent:Z
      17: astore_1
      18: aload_1
      19: areturn

我們來看java.util.OptionalIntof方法對應的字節碼:

public static OptionalInt of(int);
    Code:
       0: new           #5  // class OptionalInt
       3: dup
       4: iload_0
       5: invokespecial #6  // Method "<init>":(I)V
       8 setfield
       9: areturn

我們發現,對於inline class,沒有new也沒有serfield這兩個字節碼操作。而是用defaultvaluewithfield代替。因爲字段都是final的,沒必要保留引用,所以用withfield

Inline class 和原始類堆棧內存佔用大小對比

首先編寫測試代碼,下面的OptionalInt在兩次測試中,分別是剛剛自定義的Inline class,還有java.util.OptionalInt

public static void main(String[] args) {
        int MAX = 100_000_000;
        OptionalInt[] opts = new OptionalInt[MAX];
        for (int i=0; i < MAX; i++) {
            opts[i] = OptionalInt.of(i);
            opts[++i] = OptionalInt.empty();
        }
        long total = 0;
        for (int i=0; i < MAX; i++) {
            OptionalInt oi = opts[i];
            total += oi.orElse(0);
        }
        try {
            Thread.sleep(60_000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("Total: "+ total);
    }

運用jmap命令查看:

jmap -histo:live

對於Inline class:

num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:             1      800000016  [OptionalInt;
   2:          1687          97048  [B (java.base@14-internal)
   3:           543          70448  java.lang.Class (java.base@14-internal)
   4:          1619          51808  java.util.HashMap$Node (java.base@14-internal)
   5:           452          44600  [Ljava.lang.Object; (java.base@14-internal)
   6:          1603          38472  java.lang.String (java.base@14-internal)
   7:             9          33632  [C (java.base@14-internal)

大概佔用了8*100_000_000這麼多字節的內存,剩下的16字節是數組頭,這也符合之前提到的Value Type的特性。

對於java.util.OptionalInt:

 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:      50000001     1200000024  java.util.OptionalInt
   2:             1      400000016  [Ljava.util.OptionalInt;
   3:          1719          98600  [B
   4:           540          65400  java.lang.Class
   5:          1634          52288  java.util.HashMap$Node
   6:           446          42840  [Ljava.lang.Object;
   7:          1636          39264  java.lang.String

大概多了400MB的空間,並且多了50000000個對象。並且根據之前的描述,內存分配並不是在一起連續的,發生垃圾回收的時候,降低了掃描效率。

利用JMH測試下性能

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class MyBenchmark {

    @Benchmark
    public long timeInlineOptionalInt() {
        int MAX = 100_000_000;
        infoq.OptionalInt[] opts = new infoq
.
OptionalInt[MAX];
        for (int i=0; i < MAX; i++) {
            opts[i] = OptionalInt.of(i);
            opts[++i] = OptionalInt.empty();
        }
        long total = 0;
        for (int i=0; i < MAX; i++) {
            infoq.OptionalInt oi = opts[i];
            total += oi.orElse(0);
        }

        return total;
    }

    @Benchmark
    public long timeJavaUtilOptionalInt() {
        int MAX = 100_000_000;
        java.util.OptionalInt[] opts = new java
.
util
.
OptionalInt[MAX];
        for (int i=0; i < MAX; i++) {
            opts[i] = java.util.OptionalInt.of(i);
            opts[++i] = java.util.OptionalInt.empty();
        }
        long total = 0;
        for (int i=0; i < MAX; i++) {
            java.util.OptionalInt oi = opts[i];
            total += oi.orElse(0);
        }

        return total;
    }
}

結果:

Benchmark                             Mode  Cnt  Score   Error  Units
MyBenchmark.timeInlineOptionalInt    thrpt   25  5.155 ± 0.057  ops/s
MyBenchmark.timeJavaUtilOptionalInt  thrpt   25  0.589 ± 0.029  ops/s

可以看出,Inline class的效率,遠大於普通原始類。

參考:
https://www.infoq.com/articles/inline-classes-java/?itm_campaign=rightbar_v2&itm_source=infoq&itm_medium=articles_link&itm_content=link_text

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