涉及到的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的效率,远大于普通原始类。