java 内联

重要要点

  • Valhalla项目正在开发内联类,以提高Java程序对现代硬件的亲和力
  • 内联类使开发人员能够编写行为更像Java内置基元类型的类型
  • 内联类的实例不具有对象标识,这带来了许多优化机会
  • 内联类的到来重新引发了有关Java泛型和类型擦除的争论
  • 尽管很有希望,但这仍在进行中,尚未投入生产

在本文中,我将介绍内联类。 此功能是以前称为“值类型”的演变。 这个功能的探索和研究仍在进行中,并且是Valhalla项目中的主要工作流程,InfoQ和Oracle Java杂志已经对此进行了报道 。

为什么要内联类?

内联类的目标是提高Java程序对现代硬件的亲和力。 这将通过重新审视Java平台的一个非常基本的部分来实现,即Java数据值的模型。

从最初的Java版本到今天,Java仅有两种类型的值:基本类型和对象引用。 该模型非常简单,开发人员易于理解,但可以在性能上进行取舍。 例如,处理对象数组涉及不可避免的间接访问,这可能导致处理器高速缓存未命中。

许多关心性能的程序员都希望能够处理更有效利用内存的数据。 更好的布局意味着更少的间接访问,这意味着更少的缓存丢失和更高的性能。

另一个感兴趣的主要领域是消除为每个数据组合需要一个完整的对象标头的开销— 拼合数据。

就目前而言,Java堆中的每个对象都具有元数据头以及实际的字段内容。 在热点,这头实际上是两个机器字- 标记克拉斯 。 首先是标记词,其中包含特定于此特定对象实例的元数据。

元数据的第二个单词称为klass单词,它是指向元数据(存储在内存的Metaspace区域中)的指针,该元数据与同一类的所有其他实例共享。 对于理解运行时如何实现某些语言功能(例如虚拟方法查找)的关键,该指针非常重要。

但是,对于内联类的讨论, 标记词中保存的数据特别重要,因为它与Java对象的标识概念固有地联系在一起。

内联类和对象标识

回想一下,在Java中,两个对象实例并不仅仅因为它们的所有字段都具有相同的值就被认为是相等的。 Java使用==运算符来确定两个引用是否指向相同的内存位置,如果对象分别存储在内存中,则它们不被视为相同。

注意:此身份概念与锁定Java对象的能力有关。 实际上, 标记词用于存储对象监视器(以及其他内容)。

但是,对于内联类,我们希望复合材料的语义本质上是原始类型的语义。 在那种情况下,唯一重要的是数据的位模式,而不是该模式在内存中出现的位置。

因此,通过删除对象标头,我们还删除了组合的唯一标识。 此更改释放了运行时,可以在布局,调用约定,编译和分配方面进行重大优化。

注意:删除还对内联类的设计有其他影响。 例如,它们无法同步(因为它们既没有唯一标识,也没有存储监视器的位置)。

重要的是要意识到Valhalla是一个贯穿语言和VM并最终达到最终目标的项目。 这意味着对于程序员来说,它可能看起来像一个新的构造( 内联类 ),但是功能依赖的层数太多。

注意:内联类与即将推出的记录功能不同。 Java记录只是一个常规类,使用简化的样例进行声明,并具有一些标准化的,由编译器生成的方法。 另一方面,内联类是JVM中一个根本上的新概念,它以根本方式改变了Java的内存模型。

当前的内联类原型(称为LW2)是可以运行的,但仍处于非常非常早期的阶段。 它的目标受众是高级开发人员,库作者和工具制造商。

使用LW2原型

让我们深入研究LW2当前状态下的内联类可以完成的一些示例。 我将能够使用低级技术(例如字节码和堆直方图)展示内联类的效果。 未来的原型将添加更多的用户可见的和更高层次的方面,但是它们尚未完成,因此我将不得不坚持低层次。

要获得支持LW2的OpenJDK构建,最简单的选择是从此处下载它-Linux,Windows和Mac构建可用。 另外,经验丰富的开源开发人员可以从头开始构建自己的二进制文件。

一旦下载并安装了原型,我们就可以使用它开发一些内联类。

要在LW2中创建内联类,请使用inline关键字标记类声明。

内联类的规则(目前,其中一些规则可能会在将来的原型中放宽或更改):

  • 接口,注释类型,枚举不能是内联类
  • 顶级内部,嵌套本地类可以是内联类
  • 内联类不可为空,而是具有默认值
  • 内联类可以声明内部,嵌套,本地类型
  • 内联类是隐式最终的,因此不能是抽象的
  • 内联类隐式扩展java.lang.Object (例如枚举,注释和接口)
  • 内联类可以显式实现常规接口
  • 内联类的所有实例字段都是隐式最终的
  • 内联类不能声明自己类型的实例字段
  • javac自动生成hashCode(),equals()和toString()
  • Javac不允许对内联类使用clone(),finalize(),wait()notify()

让我们看一下第一个内联类的示例,看看像Optional这样的类型的实现作为内联类的样子。 为了减少间接性并简化说明,我们将编写一个包含基本值的可选类型的版本,类似于标准JDK类库中的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";
}
}

应该使用当前的LW2版本的javac进行编译。 要查看新的内联类技术的效果,我们需要使用可像这样调用的javap工具查看字节码:

$ javap -c -p infoq/OptionalInt.class

拆卸OptionalInt类型后,我们在字节码中看到内联类的一些有趣方面:

public final value class infoq.OptionalInt {
private final boolean isPresent;
private final int v;

该类具有一个新的修饰符值,该值是从较早的原型(该功能仍称为值类型)中遗留下来的。 即使未在源代码中指定,该类和所有实例字段也都已定型。 接下来,让我们看一下对象构造方法:

public static infoq.OptionalInt empty();
Code:
0: defaultvalue  #1                  // class infoq/OptionalInt
3: areturn
public static infoq.OptionalInt of(int);
Code:
0: iload_0
1: invokestatic  #11                 // Method "<init>":(I)Qinfoq/OptionalInt;
4: areturn
private static infoq.OptionalInt infoq.OptionalInt(int);
Code:
0: defaultvalue  #1                  // class infoq/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

对于常规类,我们希望看到一个类似于以下简单工厂方法的已编译构造序列:

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

这两个字节码序列之间的区别很明显-内联类不使用新的操作码。 相反,我们遇到了两个专门用于内联类的全新字节码-defaultvaluewithfield

  • defaultvalue用于创建新的值实例
  • 使用withfield代替setfield

注意:这种设计的后果之一是,对于每个内联类, 默认值的结果必须是该类型的一致且可用的值。

值得注意的是, withfield的语义是用更新后的字段将修改后的值替换为堆栈顶部的值实例。 这与setfield (在堆栈上消耗对象引用)略有不同,因为内联类始终是不可变的,不一定总是表示为引用。

为了完成对字节码的初步了解,我们注意到,在该类的其他方法中,还有自动生成的hashCode()equals()的实现 ,它们使用invokedynamic作为一种机制。

public final int hashCode();
Code:
0: aload_0
1: invokedynamic #46,  0             // InvokeDynamic #0:hashCode:(Qinfoq/OptionalInt;)I
6: ireturn
public final boolean equals(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: invokedynamic #50,  0             // InvokeDynamic #0:equals:(Qinfoq/OptionalInt;Ljava/lang/Object;)Z
7: ireturn

在我们的例子中,我们显式提供了toString()的重写,但是通常也会为内联类自动生成此方法。

public java.lang.String toString();
Code:
0: aload_0
1: getfield      #7                  // Field isPresent:Z
4: ifeq          29
7: ldc           #28                 // String OptionalInt[%s]
9: iconst_1
10: anewarray     #30                 // class java/lang/Object
13: dup
14: iconst_0
15: aload_0
16: getfield      #3                  // Field v:I
19: invokestatic  #32                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: aastore
23: invokestatic  #38                 // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
26: goto          31
29: ldc           #44                 // String OptionalInt.empty
31: areturn

为了驱动我们的内联类,让我们看一下Main.java中包含的一个小型驱动程序

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);
}

没有显示Main的字节码,因为它没有任何意外。 实际上,它与(如果包名称除外)与Main使用java.util.OptionalInt而不是我们的内联类版本时生成的代码相同。

当然,这是重点的一部分-使内联类对主流Java程序员的影响最小,并在不增加认知负担的情况下提供其好处。

内联类的堆行为

注意到编译值类的字节码的功能之后,我们现在可以执行Main并快速查看运行时行为,从堆的内容开始。

$ java infoq.Main

注意,程序末尾的线程延迟只是为了让我们有时间从进程中生成堆直方图。

为此,我们在单独的窗口中运行另一个工具: jmap -histo:live <pid> ,它会产生如下结果:

num     #instances         #bytes  class name (module)
-------------------------------------------------------
1:             1      800000016  [Qinfoq.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)

这表明我们已经分配了一个单个的infoq.OptionalInt值数组,它大约占据了800M(每个1亿个元素的大小8)。

不出所料,我们的内联类没有独立的实例。

注意:熟悉Java类型描述符的内部语法的读者可能会注意到新的Q类型描述符的出现,以表示内联类的值。

为了对此进行比较,让我们使用java.util中OptionalInt的版本而不是内联类的版本重新编译Main。 现在,直方图看起来完全不同(来自Java 8的输出):

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

现在,我们有一个数组,其中包含1亿个大小为4的元素,这些元素是对对象类型java.util.OptionalInt引用。 我们还有5,000万个OptionalInt实例,再加上一个空值实例,这样,非内联类实例的总内存利用率约为1.6G。

这意味着在这种极端情况下,使用内联类可将内存开销减少约50%。 这是短语“像类一样的代码,像整数一样工作”的含义的一个很好的例子。

使用JMH进行基准测试

让我们来看看一个简单的JMH基准测试。 这样做的目的是让我们看到从减少程序运行时间的角度来看,删除间接寻址和高速缓存未命中的效果。

有关如何设置和运行JMH基准的详细信息,请参见OpenJDK网站 。

我们的基准测试将直接比较OptionalInt的内联实现和JDK中的版本。

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] = infoq.OptionalInt.of(i);
opts[++i] = infoq.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;
}
}

在现代的高规格MacBook Pro上进行单次运行可得出以下结果:

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

这表明在这种特定情况下,内联类要快得多。 但是,重要的是,不要过多地阅读此示例,这只是出于演示目的。

正如JMH框架本身警告的那样:“不要以为数字告诉您您想要他们说什么。”

例如,在这种情况下,基准的infoq.OptionalInt版本会分配大约50%的资源-分配的减少是否可以提高性能? 还是还有其他性能影响? 孤立地讲,该基准并没有告诉我们-它仅仅是一个数据点。

除了表明内联类在某些精心选择的情况下有可能显着提高速度外,不应将此粗略的基准当作认真的对待或用作其他任何东西。

例如,在LW2原型中,仅支持解释模式和C2(服务器)JIT编译器。 没有C1(客户端)编译器,没有分层编译,也没有Graal。 此外,解释器尚未优化,因为重点已放在JIT实现上。 预期所有这些功能都将在Java的发行版本中提供,并且如果没有它们,所有性能数字将完全不可靠。

实际上,与当前的LW2预览相比,不仅仅是性能还有很多工作要做。 基本问题仍然存在,例如:

  • 如何扩展泛型以允许对所有类型进行抽象,包括基元,值甚至void
  • 内联类的真正继承层次结构应该是什么样?
  • 关于类型擦除和向后兼容性该怎么办?
  • 如何使现有库(尤其是JDK)兼容地发展以充分利用内联类?
  • 目前或应该放宽多少当前LW2约束?

尽管其中大多数仍是未解决的问题,但LW2试图提供答案的一个领域是通过原型设计一种机制,以将内联类用作通用类型的类型参数(“有效负载”)。

内联类作为类型参数

在当前的LW2原型中,我们必须克服一个问题,因为Java的泛型模型隐式地假定了值的可空性,而内联类也不是可空的。

为了解决这个问题,LW2使用了一种称为间接投影的技术。 这就像是内联类的自动装箱形式,并允许我们编写Foo ?类型。 对于任何内联类型Foo

最终结果是,间接投影类型可以用作通用类型中的参数(而真正的内联类型则不能这样):

public static void main(String[] args) {
List<OptionalInt?> opts = new ArrayList<>();
for (int i=0; i < 5; i++) {
opts.add(OptionalInt.of(i));
opts.add(OptionalInt.empty());
opts.add(null);
}
int total = opts.stream()
.mapToInt(o -> {
if (o == null) return 0;
OptionalInt op = (OptionalInt)o;
return op.orElse(0);
})
.reduce(0, (x, y) -> x + y);
System.out.println("Total: "+ total);
}

内联类的实例始终可以强制转换为间接投影的实例,但反之,则需要进行空检查,如示例中的lambda正文所示。

注意:间接投影的使用仍处于实验阶段。 内联类的最终版本可能完全使用不同的设计。

在内联类准备好成为Java语言中的真正功能之前,仍有大量工作要做。 像LW2这样的原型对于感兴趣的开发人员来说是很有趣的尝试,但是应该始终记住,这些只是一种智力活动。 当前版本中的任何内容都无法保证该功能最终采用的最终形式。

翻译自: https://www.infoq.com/articles/inline-classes-java/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

java 内联

java 内联_Java内联类初探相关推荐

  1. java 匿名类型_Java之匿名类讲解

    匿名类,正如名字一样在java中没有名字标识的类,当然了编译后还是会安排一个名字的. 下面是一个关于匿名类的简单例子: public classClient {public static voidma ...

  2. java载入器材_JAVA之了解类载入器Classloader

    1.类的载入.连接和初始化 类初始化通常包含载入.连接.初始化三个步骤. (1)进程的结束 每当执行一个java程序时,将会启动一个java虚拟机进程,无论程序多么复杂.有多少线程.都在这个java虚 ...

  3. java zipfile用法_Java使用ZipFile类实现Zip文件解压

    java.util.zip.ZipFile类用于从 ZIP 文件读取条目. 即从给定的ZIP压缩文件中获取所有文件的信息,如:文件的名称.是否为目录等信息.可以使用这个类来实现将zip文件进行解压操作 ...

  4. java内嵌_Java内嵌类

    1.1. 内嵌类 1.1.1. 代码 /* * To change this template, choose Tools | Templates * and open the template in ...

  5. java 表达式写法_java内置核心4大函数式接口写法和lambda表达式

    java.util.function , Java 内置核心四大函数式接口标准写发和lambda表达式 消费型接口,有一个输入参数,没有返回值 public static void main(Stri ...

  6. java path 注解_Java内置系统注解和元注解

    第一节:注解(Annotation)的作用 Annotation(注解)是JDK5.0及以后版本引入的.它的作用是修饰程序元素.什么是程序元素呢?例如:包.类.构造方法.方法.成员变量等. 注解,就是 ...

  7. java 外循环_java内循环和外循环怎么区分

    关于for循环嵌套作如下解释: 首先内层循环属于外层循环循环体的一部分,当循环体执行完以后外层循环才进入第二次循环,此过程中内层循环需要执行符合条件的完整循环.(外循环控制行数,内循环控制每一行的个数 ...

  8. java局域网邮件_Java内网发送邮件

    最近为单位的系统增加了一个新的功能,为用户定期发送邮件,用了了Javaweb 发送邮件功能,所以对遇到的问题进行整理,为以后遇到同样问题的同志提供一些参考. Java发送邮件的方式有两种,一种是通过j ...

  9. java异常 子类_Java异常 Exception类及其子类(实例讲解)

    C语言时用if...else...来控制异常,Java语言所有的异常都可以用一个类来表示,不同类型的异常对应不同的子类异常,每个异常都对应一个异常类的对象. Java异常处理通过5个关键字try.ca ...

最新文章

  1. 机器学习算法基础——朴素贝叶斯算法
  2. python3精要(23)-递归与函数列表
  3. 【emWin】例程十六:窗口管理器
  4. Scrapy框架的学习(11.scrapy框架中的下载中间件的使用(DownloaderMiddlewares))
  5. 【APICloud系列|12】ios真机调试时如何添加新设备的udid?
  6. Android 自定义软键盘实现
  7. 0421 AutoLayout的实践/基本使用
  8. dedeCMS修改文案:页眉rss文字、导航栏“首页”、页脚copyright等
  9. linux如何添加默认路由表_linux 添加静态路由
  10. 初学者python笔记(json模块、pickle模块、xml模块、shelve模块)
  11. 软件概要设计_软件测试模型之 V模型
  12. 三菱GXWorks2 绘制梯形图
  13. 短信验证码和邮箱验证码
  14. Word 2013新建文档默认使用自己设置的样式
  15. FAT文件系统工作原理
  16. 计算机专业实践报告立题依据,开题报告立题依据 .doc
  17. 科学计算机可以用多久,科学家公布“寿命计算器” 算一下你能活多久?
  18. 通过微信公众号跳转H5页面领取现金红包
  19. 【Oracle】plsql连接64位的Oracle
  20. 【报错解决】telnet时报错:无法打开到主机的连接,在端口23连接失败

热门文章

  1. 2K和XP下的CMD命令
  2. dell服务器bios修改uefi,Dell PowerEdge BIOS 和 UEFI 参考指南
  3. linux压缩文件命令_24.gzip、unzip命令详解 - 钟桂耀
  4. 图片标签z-index设置不起作用
  5. 比 Elasticsearch 更快, RediSearch + RedisJSON = 王炸
  6. 黑帽实战 | 给大家讲讲一个二类电商的大佬的故事!
  7. 最简单的pentaho report desinger 5.01报表的制作
  8. 面试必备:高频算法题汇总「图文解析 + 教学视频 + 范例代码」必问之 排序 + 二叉树 部分!
  9. 公共基础知识:中国地形地貌
  10. AI 人工智能基础及应用