Valhalla项目一直是Java社区的“流行语”,但很少有关于这个重要项目的公开文章。对于一部分人来说,Valhalla项目意味着创建值类型的能力,对于另外一部分人来说,Valhalla项目意味着具体化通用运行时类型。
Valhalla项目有一个非常明确的目标:停止Java开发人员在性能和抽象之间进行选择的要求。这篇文章将介绍什么是Valhalla项目,以及它带来了什么问题。
什么是Valhalla项目?
Valhalla项目是一个始于2014年的OpenJDK项目,由Brian Goetz领导,目的是为Java Development Kit(JDK)10或未来的Java版本引入基于价值的优化。该项目主要侧重于允许开发人员创建和使用值类型,或像原语一样不引用值。用Goetz说:“Codes like a class, works like an int“。
相比于引用类型(对象),值类型的主要好处是无论在内存还是计算中,都删除了引用类型的开销。例如,尽管与对象关联的实际大小和开销特定于Java虚拟机(JVM)实现,但所有JVM都包含用于存储有关对象信息的字节,包括多态信息、标识信息、同步信息和垃圾收集元数据(如引用计数器)。另外,当必须访问与引用类型关联的对象时,引用必须是被间接引用的,赋予一个间接层。在某些情况下,这种开销是多余的、是不需要的。
尽管值类型在某些重要的特征中偏离了对象,但它们保持了与类相似的抽象层次。例如,值类型可能仍然有方法和字段,都带有便于封装的可见性修饰符。虽然,在当前的Java版本(编写本文时的JDK 9)中,主要差异之一是值类型(如基本类型)不能被用作泛型类型参数;例如,List 在当前版本的Java中不是有效的类型。
尽管像int这样的原始类型可以被自动结合到引用类型(比如Integer),但主要的缺点是:它会重新引入对象的开销。尽管自JDK 5引,这一直是Java泛型的一个障碍,但在Valhalla项目中,开发人员可以创建新的值类型。为了解决这个问题,Valhalla还负责提供一种机制,允许将值类型作为有效的泛型参数提供,同时仍然保持Java的当前类型被删除的通用语义。
什么是值类型?
值类型是一组直接存储在内存中的数据组,而不是对数据的引用(或指针)。因此,值类型可以被认为是只消耗内存来存储包含在其字段中的数据聚合,而没有额外的开销。从概念上讲,原语是值类型的一个可重新定义的例子,其中int只消耗32位字节来存储元数据。
将数据直接放在内存中(而不是参考)被称为扁平化,其优点在数组中更为明显。在对象数组的情况下,数组中的每个元素都存储对与该元素关联的对象的引用,要求在访问对象之前执行取消引用。在值类型的数组中,值直接放置在数组中,并保证在连续的内存中。这个想法如下图所示:
使用值类型比参考类型有一些明显的好处,包括:
* 减少内存使用情况:没有额外的内存用于存储对象元数据,例如便于同步、标识和垃圾回收的标志。对于像Integer这样的小对象,对象的开销可以匹配甚至超过数据本身的大小。
* 减少间接性:由于对象是以Java的引用类型存储的,每次访问对象时,都必须先解除引用,从而导致执行额外的指令。与值类型相关联的扁平化数据会立即出现在需要它们的位置,因此不需要取消引用。
* 增加局部性:扁平化值对象删除了间接存储在内存中的可能性,增加值相邻地存储在内存中的可能性,特别是对于数组或其他连续的内存结构。
引用类型和值类型之间的主要区别之一是标识的对应定义:引用类型的标识内在地绑定到对象,而值类型的标识绑定到其当前状态。
例如,日期可以被认为是一个值类型,其中一个值实例解析为2018年1月1日,等于另一个解析为2018年1月1日的值实例,不管这两个实例是不是相同。只要一个实例的数据与另一个实例的数据匹配字段,这两个实例是可互换的(即它们可以被识别为相同的值)。在实践中,日期通常被存储为自某个时期以来的时间单位(例如毫秒)。因此,如果两个日期值类型从相同时期开始具有相同的毫秒数,则它们是相等的。
虽然值类型有许多的好处,但有好必然就有坏,特别是在现有的Java语言中。在JDK版本的每次迭代中,最重要的问题之一是向后兼容性,主要是现有二进制字节码表示的兼容性。尽管可以将值类型与Java目前使用的原始值模型相匹配,但仍然存在一些主要缺陷,其中包括:
* 原语目前不支持限定的方法调用或字段访问(即使用点运算符);
* 用户定义的值类型不存在文字;
* 所有值类型都不支持内置运算符(如+);
* 很难为所有用户定义类型的默认值;
除了现有原始模型中的这些缺点之外,还有其他一些关于用户定义的值类型必须解决的问题,包括:
* 值类型可以实现接口吗?
* 一个值可以是另一个值子类化?
* 值类型是否隐式地派生了一个基类?
* 值类型在必要时可以被当作对象吗?
什么是通用专业化?
自从JDK 5中包含了泛型以来,Java泛型的一个主要缺陷就是缺少对原始类型参数的支持。例如,尽管List 是一个有效的Java类型,但是List 不是。这个缺点是由泛型被引入到Java,并不改变类文件的二进制运行时的特性要求引起的。这导致泛型类的类型参数被删除,泛型类的所有通用用法都有相同的运行时类型。例如,虽然在编译期间会处理List 和List ,以确保每个类型参数都是类型安全的,但两个类型参数都将被删除,并在运行时解析为相同类型。
在运行时解决以下几行问题:
这允许现有代码继续使用泛型类的原始类型而不进行更改,从而保留了现有Java代码的向后兼容性。例如,现有代码仍然可以使用原始类型列表(没有任何泛型类型参数),因为原始类型和泛型类型的运行时类型将通过删除来解析相同类型:List。此外,由于泛型类型的参数不再存在于泛型类型,因此所有泛型参数都被引用类型对象替换。例如,给定以下类定义:
等效的运行时类型将是:
然后,编译器负责生成正确的强制转换和桥接方法,以确保所有使用的均匀转换的运行类型所有用法都是类型安全的。
会有效地变成:
由于类型消除,运行时泛型参数必须被解析为某种类型。之所以选择对象,是因为它包含了可以存储任何用户定义类型的最高类型。由于原始值(例如int,double,boolean等)没有从Object类继承,所以它们被禁止用作泛型类型参数。实际上,随着泛型的引入,Java的运行时性质不能随着泛型的引入而改变,导致原始类型不能用作泛型类型参数。
如何将值类型用作类型参数?
一般来说,在面向对象的编程语言中支持泛型的技术有两种。第一种技术,即同类转换,将泛型类型解析为单一类型,而不考虑其类型参数。这是Java使用的技术,List 和List 都解析为运行时类型列表。第二种技术,异构翻译(或泛型专门化),在运行时,解析为不同类型的单个泛型类型的不同类型参数。例如,List 和List 将导致类似于List_String和List_Integer的运行时类型(与单一齐型List相反)。使用专业化有许多“副作用“,包括泛型类型层次的分离。
随着用户定义的值类型的引入,非引用类型被用作泛型类型参数的需求变得更大。正如我们已经看到的,同质转换不足以提供非引用类型的泛型参数方法。为了解决这种困境,在保持与现有向后兼容性的同时,设计了一种混合解决方案,将同质的作为参考类型,而异构的用于值类型。
为了确保现有的泛型类仍然可以运行,一个新的关键字any与泛型类型参数声明一起使用,表示它将使用增强的泛型(允许将值类型和引用类型作为泛型类型参数)。例如,增强的泛型Box类将如下所示:
尽管在需要增强泛型时包含any关键字似乎很麻烦,但是还有一些重要的情况是必须维护现有的泛型语义。例如,ArrayList类具有以下方法:
最初,remove(int)方法的设计是假定泛型参数是引用类型。如果这个限制被放宽,原始泛型类型参数被允许,则会出现方法冲突,如果通用参数T解析为类型int,则上述两个方法将解析为相同的签名。这可以通过创建两个具有不同名称的方法来解决,例如removeByPosition和removeElement,但是这仍然需要对ArrayList的接口进行更改,因此不能假定所有的泛型类都可以自动接受值类型的泛型参数。
在Valhalla项目的这一点上,关于泛型专业化的问题仍然存在很多问题,但是可以得出这样的结论:如果将值类型添加到Java中,那么接受这些值类型的泛型很可能会与它们一起出现。虽然还有大量的工作做,包括具有值型的泛型语义,但是还是有一些重大的决定,即使在它的初期也是如此。
这是否意味着Java将会具体化?
作为Java使用同类转换的必然结果,通用类型的泛型参数不通用,或者是在运行时可用的。这对于无数的应用程序来说都是一个大问题,但为了权衡向后兼容性,Java应用程序引入了泛型。随着Valhalla项目中泛型类型的重新审视,不禁要问:Java是否有具体化的类型?
这个问题的答案很可能是,是的,但只有在价值型泛型的情况下。尽管对于值类型参数的通用特化的实现还没有被固化,但实现可能包括泛化类型。然而,为了保持向后兼容性,引用类型泛型参数不太可能会被视为具体化。虽然普遍认可的泛型有很多好处,但也有一些主要缺点:
* 与现有的参考类型不兼容
* 假定泛型在运行时被擦除的优化失效
* 字节码的复杂性来维护运行时类型信息
尽管目前还没有在stone中设置任何东西,但通用的通用具体化将会进入项目Valhalla,即使有值类型的泛型会看到某种程度的部分具体化
结论
随着Jigsaw项目和功能性编程语义的引入,对于Java语言来说,这是一个进步,虽然Java不是完美的语言,但正在采取措施使其进一步成熟,提供了新的功能和语义,以便在没有适当抽象的情况下获得更好的性能和问题映射。