Valhalla 不亚于 Java 语言的彻底改革,承诺纠正长期存在的性能问题。下面是对即将发生的事情的初步了解,从新的值类和原始类开始。

深入了解 Java 的史诗级重构-Valhalla 项目_泛型

在 Java 中,一切都是对象——除了像int. 事实证明,这个小小的警告对这门语言产生了很大的影响,而且多年来这种影响变得更加复杂。这个看似微不足道的设计决策会导致集合和泛型等关键领域出现问题。它还限制了某些性能优化。Java 语言重构项目 Valhalla 旨在纠正这些问题。Valhalla 项目负责人 Brian Goetz 表示,Valhalla 将“弥合基元与对象之间的裂痕”。

可以公平地说,Valhalla 项目是一次史诗般的重构,旨在解决自 Java 诞生以来平台中埋藏的技术债务。这种彻底的演变证明 Java 不仅是经典,而且仍然处于编程语言设计的最前沿。让我们看一下 Valhalla 项目的关键技术组件,以及为什么它们对 Java 的未来如此重要。

Java 中的性能问题

早在 90 年代 Java 首次推出时,就决定所有用户创建的类型都将是类。只有少数原始类型被视为特殊类型。这些不是作为基于指针的类结构处理的,而是直接映射到操作系统类型。八个基本类型是intbyteshortlongfloatdoublebooleanchar。 


将这些变量直接映射到操作系统可以更好地提高性能,因为当消除对象的引用开销时,数值运算的性能会更好。此外,所有数据最终都会解析为程序中的这八种基本类型。类只是一种结构和组织层,它提供了处理原始类型的更强大的方法。唯一的另一种结构是数组。基元、类和数组构成了 Java 表达能力的全部范围。

但基元是与类和数组不同的动物类别。作为程序员,我们已经学会了直观地处理差异。例如,基元是按值传递,而对象是按引用传递。其中的原因很深。这归结为身份问题。我们可以说原始值是可替换的: int x = 4是整数 4,无论它出现在哪里。我们在equals()vs中看到了这种区别==,前者测试对象的值等价性,后者测试同一性。如果两个引用共享内存中的相同空间,则它们满足==,这意味着它们是同一个对象。任何int设置为 4 的 s 也将满足==,而根本  int不支持。.equals()

Java虚拟机(JVM) 可以利用原语的处理方式来优化其存储、检索和操作原语的方式。特别是,如果平台确定变量未更改(即,它是常量或不可变),则可以对其进行优化。

相比之下,对象则无法抵抗这种优化,因为它们具有身份。作为类的实例,对象保存的数据可以是基元也可以是其他类。对象本身是通过指针句柄来寻址的。这创建了一个引用网络:对象图。每当某个值发生更改(或者即使可能发生更改)时,JVM 就被迫维护该对象的明确记录以供引用。引用对象的需要是某些性能优化的障碍。


性能困难还不止于此。对象作为引用桶的本质意味着它们以非常蓬松的方式存在于内存中。Fluffy是我的技术术语,用于描述 JVM 无法压缩对象以最大限度地减少其内存占用的事实。当一个对象在其构成中引用另一个对象时,JVM 被迫维护该指针关系。(在某些情况下,巧妙的优化可以帮助确定嵌套引用是特定实体的唯一句柄。)

在他的《瓦尔哈拉状态》博客文章中,Goetz 使用一系列要点来说明引用的非密集性质。我们可以使用一个类。例如,假设我们有一个Landmark带有名称和地理位置字段的类。这些意味着一种如下所示的内存结构:

深入了解 Java 的史诗级重构-Valhalla 项目_Java_02


国际数据集团

图 1. Java 对象的“蓬松”内存占用。

我们希望实现的是在适当的时候能够握住物体,如图 2 所示。

深入了解 Java 的史诗级重构-Valhalla 项目_原语_03


国际数据集团

图 2. 内存中的密集对象。

这是对早期设计决策给 Java 平台带来的性能挑战的概述。现在让我们考虑一下这些决策如何影响三个关键领域的绩效。

问题1:方法调用和传值

内存中对象的默认结构对于内存和缓存来说都是低效的。此外,还有机会在方法调用约定方面获益。能够将按值调用参数传递给具有类语法的方法(在适当的情况下)将产生巨大的性能优势。

问题 2:装箱和自动装箱

primitive除了效率低下之外,和之间的区别还class造成了语言层面的困难。Integer创建像and (以及自动装箱)这样的原始“盒子Long”是为了缓解这种区别引起的问题。然而,它并没有真正解决这些问题,而且给开发人员和机器带来了一定程度的开销。int作为开发人员,您必须了解并记住and Integer(和ArrayList<Integer>、、、int[]以及Integer[]缺少)之间的区别ArrayList<int>。与此同时,机器必须在两者之间进行转换。 

在某种程度上,拳击给了我们两全其美的好处。模糊这些实体如何工作的潜在细微差别使得更难访问类语法的强大功能原语的性能。

问题 3:泛型和流

所有这些考虑因素在仿制药中都达到了顶峰。泛型的目的是使跨功能的泛化变得更容易、更明确,但是这组非对象变量(原语)的挑剔存在只会导致它崩溃。 <int>不存在——它不可能存在,因为int根本不是一个类;它不是从 下降的Object

然后,这个问题在像集合和流这样的库中显现出来,其中通用库函数的理想被迫通过提供和其他非通用变体来处理intInteger、与 等的现实。longLongIntStream

Valhalla 的解决方案:值类和原始类

瓦尔哈拉计划从根本上解决这些问题。第一个也是最基本的概念是值类。这里的想法是,您可以定义一个类,该类具有类的所有优点,例如拥有方法并能够实现泛型,但没有标识。实际上,这意味着类是不可变的并且不能是布局多态的(其中超类可以通过抽象属性对子类进行操作)。 

值类为我们提供了一种清晰明确的方法来获取我们所追求的性能特征,同时仍然可以获得类语法和行为的好处。这意味着库构建者也可以使用它们,从而改进他们的 API 设计。 

更进一步是原始类,它就像一个更极端的值类。本质上,原始类是真正原始变量的薄包装,但具有类方法。这有点像定制的、流线型的原始盒子。改进在于使拳击系统更加明确和可扩展。此外,由基元类包装的基元值保留了基元的性能特征(没有幕后装箱和拆箱)。因此,原始类可以用在任何类可以存在的地方——Object[]例如在数组中。基本类型不能为空(它们不能设置为空)。 

一般来说,我们可以说 Valhalla 项目使原语和用户定义类型更加紧密地结合在一起。这为开发人员在纯基元和对象之间提供了更多选择,并使权衡变得明确。它还使这些操作总体上更加一致。特别是,新的基元系统将平滑基元和对象的工作方式、它们的装箱方式以及如何添加新对象。

Java 的语法将如何变化

Valhalla 已经看到了一些不同的语法提案,但现在该项目正在采取明确的形式和方向。两个新的关键字修改了class关键字:valueprimitive。使用该语法声明的类value class将放弃其身份,并在此过程中获得性能改进。除了可变性和多态性限制之外,您期望从类获得的大多数功能仍然适用,并且此类类可以完全参与通用代码(例如object[]ArrayList<T>)。值类默认为 null。

primitive class语法创建了一个从传统对象向传统基元更进一步的类。这些类默认为字段的基础值(0 表示int,0.0 表示double,等等),并且不能为 null。原始类在优化方面获得最多,但在功能方面牺牲最多。原始类不是32 位撕裂安全的。基元类最终将用于对平台中的所有基元进行建模,这意味着用户和库定义的基元添加将作为内置项参与同一系统。

身份对象和值对象

IdentityObjectValueObject是 Valhalla 项目中引入的两个新界面。这些将允许运行时确定您正在处理的类的类型。

对于经验丰富的 Java 开发人员来说,最根本的语法变化也许是添加了成员.ref。所有类型现在都将具有该V.ref()字段。该字段的操作类似于基元上的盒子,因此int.ref类似于用 包装intan  Integer。普通课程将解析.ref为他们的参考。总体效果是提供一种一致的方式来请求变量的引用,无论其类型如何。这还具有使所有 Java 数组“协变”的效果,也就是说,它们都源自Object[]. 因此,int[]now 源自任何Object[]需要的地方,并且可以在任何需要的地方使用。

结论

值类和原始类将对 Java 及其生态系统产生重大影响。当前的路线图计划首先引入值类,然后引入原始类。接下来将迁移现有的原始装箱类(如Integer)以使用新的原始类。有了这些功能,下一个功能(称为通用泛型)将允许原始类直接与泛型一起使用,从而消除 API 中重用的许多复杂性。最后,专门的泛型(允许 的所有表达能力T extends Foo)将与原始类集成。

Valhalla 项目和组成它的项目仍处于设计阶段,但我们已经越来越接近该项目,并且围绕该项目的活动表明不久之后价值类就会出现在 JDK 预览中。

除了所有有趣的技术工作之外,还有 Java 持续的活力。有意愿也有能力经历确定平台可以从根本上发展的过程,这证明了真正致力于保持 Java 的相关性。 Loom 项目是另一项让人们对 Java 未来持乐观态度的项目。