数组对象是否显式包含索引?
简短的回答:不。
更长的答案:通常不会,但理论上可以做到。
完整答案:
Java 语言规范和 Java 虚拟机规范都不保证数组在内部的实现方式。它所需要的只是数组元素由具有值 from 到 的索引号访问。实现如何实际获取或存储这些索引元素的值是实现私有的详细信息。int
0
length-1
完全符合的 JVM 可以使用哈希表来实现数组。在这种情况下,元素将是非连续的,分散在内存周围,并且需要记录元素的索引,以了解它们是什么。或者它可以向月球上的一个人发送消息,这个人将数组值写在标记的纸上,并将它们存储在许多小文件柜中。我不明白为什么JVM会想要做这些事情,但它可以。
在实践中会发生什么?典型的 JVM 会将数组元素的存储分配为平面、连续的内存块。查找特定元素是微不足道的:将每个元素的固定内存大小乘以所需元素的索引,并将其添加到数组开头的内存地址:。这意味着数组存储只包含原始元素值,按索引连续排序。将索引值与每个元素一起存储是没有目的的,因为元素在内存中的地址暗示了其索引,反之亦然。但是,我不认为您显示的图表试图说它显式存储了索引。该图只是标记图上的元素,以便您知道它们是什么。(index * elementSize) + startOfArray
使用连续存储并通过公式计算元素地址的技术简单且非常快速。它还具有非常小的内存开销,假设程序仅分配其数组与它们真正需要的大小一样大。程序依赖于并期望阵列的特定性能特征,因此对阵列存储执行奇怪操作的 JVM 可能性能不佳且不受欢迎。因此,实际的JVM将受到限制,以实现连续存储,或者类似地执行。
我只能想到该方案的几个变体,这些变体将永远有用:
堆栈分配或寄存器分配数组:在优化期间,JVM 可能通过转义分析确定数组仅在一个方法中使用,如果数组也是较小的固定大小,则它将成为直接在堆栈上分配的理想候选对象,计算相对于堆栈指针的元素地址。如果数组非常小(固定大小可能最多为4个元素),JVM可以走得更远,将元素直接存储在CPU寄存器中,所有元素访问都展开和硬编码。
打包的布尔数组:计算机上最小的直接可寻址内存单元通常为 8 位字节。这意味着如果 JVM 为每个布尔元素使用一个字节,那么布尔数组每 8 位就会浪费 7 个字节。如果布尔值打包在内存中,则每个元素仅使用 1 位。通常不进行此打包,因为提取单个字节位的速度较慢,并且需要特别考虑才能确保多线程的安全。但是,打包的布尔数组在某些内存受限的嵌入式设备中可能非常有意义。
尽管如此,这些变体都不需要每个元素存储自己的索引。
我想谈谈你提到的其他一些细节:
数组存储指定数量的相同类型的数据
正确。
数组的所有元素都是同一类型的这一事实很重要,因为这意味着所有元素在内存中的大小都相同。这就是允许通过简单地乘以它们的常见大小来定位元素的原因。
如果数组元素类型是引用类型,则从技术上讲这仍然是正确的。尽管在这种情况下,每个元素的值不是对象本身(大小可能不同),而只是引用对象的地址。此外,在这种情况下,数组的每个元素引用的对象的实际运行时类型可以是元素类型的任何子类。例如,
Object[] a = new Object[4]; // array whose element type is Object
// element 0 is a reference to a String (which is a subclass of Object)
a[0] = "foo";
// element 1 is a reference to a Double (which is a subclass of Object)
a[1] = 123.45;
// element 2 is the value null (no object! although null is still assignable to Object type)
a[2] = null;
// element 3 is a reference to another array (all arrays classes are subclasses of Object)
a[3] = new int[] { 2, 3, 5, 7, 11 };
数组是连续的内存位置
如上所述,这不一定是真的,尽管在实践中几乎肯定是正确的。
为了进一步说明,请注意,尽管JVM可能会从操作系统中分配连续的内存块,但这并不意味着它最终在物理RAM中是连续的。操作系统可以为程序提供一个虚拟地址空间,该空间的行为就像是连续的,但是单个内存页分散在各个位置,包括物理RAM,磁盘上的交换文件,或者如果其内容已知当前为空白,则根据需要重新生成。即使虚拟内存空间的页驻留在物理 RAM 中,它们也可以按任意顺序排列在物理 RAM 中,并具有复杂的页表,用于定义从虚拟地址到物理地址的映射。即使操作系统认为它正在处理“物理RAM”,它仍然可以在模拟器中运行。可以一层又一层地,这是我的观点,并且要了解它们的所有底部以找出真正发生的事情需要一段时间!
编程语言规范的部分目的是将明显的行为与实现细节分开。在编程时,您通常可以单独根据规范进行编程,而不必担心它在内部是如何发生的。但是,当您需要处理速度和内存有限的实际约束时,实现细节变得相关。
由于数组是一个对象,对象引用存储在堆栈上,并且实际对象位于堆中,因此对象引用指向实际对象
这是正确的,除了你所说的堆栈。对象引用可以存储在堆栈上(作为局部变量),但它们也可以存储为静态字段或实例字段,或作为数组元素,如上例所示。
此外,正如我前面提到的,聪明的实现有时可以直接在堆栈或CPU寄存器中分配对象作为优化,尽管这对程序的明显行为没有任何影响,而只是对其性能的影响。
编译器只需在运行时查看提供的数组索引号即可知道要去哪里。
在Java中,不是编译器来做这件事,而是虚拟机。数组是 JVM 本身的一个功能,因此编译器可以将仅使用数组的源代码转换为使用数组的字节码。然后,JVM的工作是决定如何实现数组,编译器既不知道也不关心它们是如何工作的。