涉及到的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.OptionalInt
的of
方法對應的字節碼:
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
這兩個字節碼操作。而是用defaultvalue
和withfield
代替。因爲字段都是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