数组对象是否显式包含索引?

2022-09-01 12:37:43

从学习Java的第一天起,各种网站和许多老师就告诉我,数组是连续的内存位置,可以存储相同类型的指定数量的数据。

由于数组是一个对象,对象引用存储在堆栈上,并且实际对象位于堆中,因此对象引用指向实际对象。

但是,当我遇到如何在内存中创建数组的示例时,它们总是显示如下内容:

(其中对数组对象的引用存储在堆栈上,并且该引用指向堆中的实际对象,其中还存在指向特定内存位置的显式索引)

Internal working of objects taught by various websites and teachers

但最近我在网上看到了Java的笔记,其中他们说数组的显式索引没有在内存中指定。编译器只需在运行时查看提供的数组索引号即可知道要去哪里。

就像这样:

Enter image description here

在阅读了笔记后,我也在Google上搜索了有关此事的内容,但是关于这个问题的内容要么非常模糊,要么根本不存在。

我需要进一步澄清这个问题。数组对象索引是否显式显示在内存中?如果不是,那么Java如何在运行时管理命令以转到数组中的特定位置?


答案 1

数组对象是否显式包含索引?

简短的回答:不。

更长的答案:通常不会,但理论上可以做到。

完整答案:

Java 语言规范和 Java 虚拟机规范都不保证数组在内部的实现方式。它所需要的只是数组元素由具有值 from 到 的索引号访问。实现如何实际获取或存储这些索引元素的值是实现私有的详细信息。int0length-1

完全符合的 JVM 可以使用哈希表来实现数组。在这种情况下,元素将是非连续的,分散在内存周围,并且需要记录元素的索引,以了解它们是什么。或者它可以向月球上的一个人发送消息,这个人将数组值写在标记的纸上,并将它们存储在许多小文件柜中。我不明白为什么JVM会想要做这些事情,但它可以。

在实践中会发生什么?典型的 JVM 会将数组元素的存储分配为平面、连续的内存块。查找特定元素是微不足道的:将每个元素的固定内存大小乘以所需元素的索引,并将其添加到数组开头的内存地址:。这意味着数组存储只包含原始元素值,按索引连续排序。将索引值与每个元素一起存储是没有目的的,因为元素在内存中的地址暗示了其索引,反之亦然。但是,我不认为您显示的图表试图说它显式存储了索引。该图只是标记图上的元素,以便您知道它们是什么。(index * elementSize) + startOfArray

使用连续存储并通过公式计算元素地址的技术简单且非常快速。它还具有非常小的内存开销,假设程序仅分配其数组与它们真正需要的大小一样大。程序依赖于并期望阵列的特定性能特征,因此对阵列存储执行奇怪操作的 JVM 可能性能不佳且不受欢迎。因此,实际的JVM将受到限制,以实现连续存储,或者类似地执行。

我只能想到该方案的几个变体,这些变体将永远有用:

  1. 堆栈分配或寄存器分配数组:在优化期间,JVM 可能通过转义分析确定数组仅在一个方法中使用,如果数组也是较小的固定大小,则它将成为直接在堆栈上分配的理想候选对象,计算相对于堆栈指针的元素地址。如果数组非常小(固定大小可能最多为4个元素),JVM可以走得更远,将元素直接存储在CPU寄存器中,所有元素访问都展开和硬编码。

  2. 打包的布尔数组:计算机上最小的直接可寻址内存单元通常为 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的工作是决定如何实现数组,编译器既不知道也不关心它们是如何工作的。


答案 2

在Java中,数组是对象。请参阅 JLS - 第 10 章。数组

在 Java 编程语言中,数组是对象§4.3.1),是动态创建的,可以分配给 Object 类型的变量 (§4.3.2)。类的所有方法都可以在数组上调用。Object

如果你看看10.7。数组成员一章中,您将看到索引不是数组成员的一部分:

数组类型的成员全部为以下各项:

字段 ,它包含数组的分量数。长度可以是正数或零。public finallength

  • 方法 ,它重写类中同名的方法,并且不引发任何选中的异常。数组类型的克隆方法的返回类型为 。publiccloneObjectT[]T[]

  • 从类继承的所有成员 ;对象唯一未继承的方法是其克隆方法。Object

由于每种类型的大小是已知的,因此在给定第一个组件的情况下,您可以轻松确定数组中每个组件的位置。

访问元素的复杂性为 O(1),因为它只需要计算地址偏移量。值得一提的是,并非所有编程语言都假定此行为。