java 内联_Java内联类初探

java 内联

重要要点

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

在本文中,我将介绍内联类。 此功能是以前称为“值类型”的演变。 这个功能的探索和研究仍在进行中,并且是Valhalla项目中的主要工作流程,InfoQOracle 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 内联

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: java 1.8.0_45是Java编程语言的一个版本。它是Java SE 8的更新版本之一。这个版本的发布在2015年3月,主要提供了一些新的功能与改进。 Java 1.8.0_45引入了许多新的特性,其中最重要的是函数式接口和Lambda表达式的支持。函数式接口允许我们使用函数式编程的概念,将方法作为参数进行传递和使用。这提供了更简洁和灵活的代码编写方式。 此外,Java 1.8.0_45还引入了默认方法、方法引用和Stream API等功能。默认方法允许在接口中添加具体的实现,而方法引用则提供了一种更简化的方法调用方式。Stream API提供了一套便捷的API来进行集合操作,使得处理数据集合变得更加方便和高效。 Java 1.8.0_45还通过引入新的Date-Time API、Repeating注解和新的安全增强等改进了Java的功能性。新的Date-Time API提供更强大、更易用的日期和时间处理能力。Repeating注解则允许我们在同一个元素上多次使用同一个注解,使得代码更加简洁。 此版本还对安全性进行了增强,包括强化了原有的安全机制、修复了一些漏洞,并提供了新的的加密和密钥管理工具。 总的来说,Java 1.8.0_45是Java SE 8的一个重要更新版本,通过引入函数式接口、Lambda表达式以及其他一些新功能和改进,使得Java编程更加方便、高效和灵活。 ### 回答2: Java 1.8.0_45是Oracle公司发布的一款Java开发工具。它是Java平台,标准版(JDK)的一个版本。这个版本提供了一系列的开发工具和库,用于创建和运行Java应用程序。 Java 1.8.0_45包含了一些重要的功能和改进。其中最显著的是Lambda表达式的引入。Lambda表达式是Java 8中引入的一种新的语法,它简化了函数式编程的实现。使用Lambda表达式,可以更加简洁和优雅地编写代码,并且可以更好地支持并行处理和函数组合。 此外,Java 1.8.0_45还引入了一些其他的功能和改进,比如新的日期和时间API(java.time包)、重复注解、型注解、默认方法、函数式接口等。这些功能都为开发者提供了更多的便利性和灵活性,使得Java语言更加现代化和强大。 Java 1.8.0_45也对性能进行了优化和改进。它引入了一些新的编译器优化技术,例如Lambda表达式的内联优化和StringSwitch的编译优化。这些优化可以提高程序的运行效率和性能。 总结来说,Java 1.8.0_45是一款功能强大、性能优越的Java开发工具。它引入了许多重要的新特性和改进,使得Java语言更加现代化和灵活,并且提高了开发效率和程序性能。对于Java开发者来说,升级到Java 1.8.0_45是一个不错的选择。 ### 回答3: Java 1.8.0_45是 Oracle 公司于2015年3月发布的Java SE 8 update 45版本。该版本是Java SE 8系列的一部分,它引入了许多新的功能和改进,并修复了一些之前版本中的bug和安全漏洞。 Java SE 8 update 45的最重要的功能是引入了Lambda表达式和新的函数式接口。Lambda表达式使得在Java中使用函数式编程变得更加容易和简洁,减少了代码的冗余和复杂性。此外,还引入了新的Stream API,使得对集合和数组的操作更加方便和灵活。 Java SE 8 update 45还提供了更好的日期和时间处理支持,引入了新的Date-Time API,以取代Java旧有的Date和Calendar。新的API提供了更加简洁和易于使用的日期和时间操作方法。 此外,Java SE 8 update 45改善了对接口的支持,允许在接口中定义默认方法和静态方法。默认方法使得在接口中添加新的方法不会破坏已有的实现,而静态方法可以直接通过接口调用,不需要通过实例对象。 Java SE 8 update 45还提供了一些其他的改进,包括改进了型推断、增强了注解处理、优化了JVM的性能等。 总的来说,Java SE 8 update 45为Java开发者带来了许多强大的新功能和改进,使得Java的开发更加高效和方便。它是一个重要的Java版本,在业界得到了广泛的认可和使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值