1

在某些情况下,需要高效的内存来存储大量对象。要在 Java 中做到这一点,您必须使用几个原始数组(见下文为什么)或一个大字节数组,这会产生一点 CPU 开销来进行转换。

示例:您有一个class Point { float x; float y;}. 现在,您想将 N 个点存储在一个数组中,在 32 位 JVM 上,该数组至少需要 N * 8 个字节用于浮点数和 N * 4 个字节用于引用。所以至少 1/3 是垃圾(这里不计入普通对象开销)。但是,如果您将其存储在两个浮点数组中,一切都会好起来的。

我的问题:为什么 Java 不优化引用数组的内存使用?我的意思是为什么不像在 C++ 中那样直接将对象嵌入到数组中?

例如,标记类 Point final 应该足以让 JVM 看到 Point 类的数据的最大长度。或者这会违反规范吗?在处理大型 n 维矩阵等时,这也会节省大量内存

更新

我想知道 JVM 理论上是否可以优化它(例如在幕后)以及在什么条件下 - 我不能以某种方式强制 JVM。我认为结论的第二点是它不能轻易完成的原因。

结论 JVM 需要知道的内容:

  1. 该类需要是 final 才能让 JVM 猜测一个数组条目的长度
  2. 该数组需要只读。当然,您可以更改值,Point p = arr[i]; p.setX(i)但不能通过inlineArr[i] = new Point(). 否则 JVM 将不得不引入与“Java 方式”相反的复制语义。见阿罗斯的回答
  3. 如何初始化数组(调用默认构造函数或将成员初始化为其默认值)
4

3 回答 3

3

Java 没有提供这样做的方法,因为它不是语言级别的选择。C、C++ 等公开了执行此操作的方法,因为它们是系统级编程语言,您需要了解系统级功能并根据您使用的特定架构做出决策。

在 Java 中,您的目标是 JVM。JVM 没有指定这是否是允许的(我假设这是真的;我没有彻底梳理 JLS 以证明我就在这里)。这个想法是,当您编写 Java 代码时,您相信 JIT 可以做出明智的决策。那就是可以将引用类型折叠成数组等的地方。因此,这里的“Java 方式”是您无法指定它是否发生,但如果 JIT 可以进行优化并提高性能,它可以而且应该这样做。

我不确定是否特别实现了这种优化,但我知道类似的优化是:例如,分配的对象new在概念上位于“堆”上,但如果 JVM 注意到(通过称为逃逸分析的技术) object 是方法本地的,它可以在堆栈上甚至直接在 CPU 寄存器中分配对象的字段,完全消除“堆分配”开销而无需更改语言。

更新更新的问题

如果问题是“这完全可以做到”,我认为答案是肯定的。有一些极端情况(例如空指针),但您应该能够解决它们。对于空引用,JVM 可以说服自己永远不会有空元素,或者如前所述保留位向量。这两种技术都可能基于转义分析,表明数组引用永远不会离开方法,因为如果您尝试将其存储在对象字段中,我可以看到簿记变得很棘手。

于 2012-02-22T00:07:46.833 回答
1

您描述的场景可能会节省内存(尽管实际上我不确定它是否会这样做),但在实际将对象放入数组时可能会增加相当多的计算开销。考虑一下,当您new Point()创建的对象是在堆上动态分配的。因此,如果您Point通过调用分配 100 个实例,new Point()则无法保证它们的位置在内存中是连续的(实际上它们很可能不会分配到连续的内存块)。

那么Point实例如何真正进入“压缩”数组?在我看来,Java 必须将每个字段显式复制Point到为数组分配的连续内存块中。对于具有许多字段的对象类型,这可能会变得昂贵。不仅如此,原始Point实例还在堆上以及数组内部占用空间。因此,除非它立即被垃圾收集(我想任何引用都可以被重写以指向放置在数组中的副本,从而理论上允许对原始实例立即进行垃圾收集)你实际上使用的存储空间比你会如果您刚刚将引用存储在数组中。

此外,如果您有多个“压缩”数组和一个可变对象类型怎么办?将对象插入数组必然会将该对象的字段复制到数组中。因此,如果您执行以下操作:

Point p = new Point(0, 0);
Point[] compressedA = {p};  //assuming 'p' is "optimally" stored as {0,0}
Point[] compressedB = {p};  //assuming 'p' is "optimally" stored as {0,0}

compressedA[0].setX(5)  
compressedB[0].setX(1)  

System.out.println(p.x);
System.out.println(compressedA[0].x);
System.out.println(compressedB[0].x);

...你会得到:

0
5
1

...即使在逻辑上应该只有一个Point. 存储引用避免了这种问题,并且还意味着在任何情况下,在多个数组之间共享一个重要对象时,您的总存储使用量可能低于每个数组存储该对象所有字段的副本时的总存储使用量。

于 2012-02-22T00:58:24.823 回答
0

这不等于提供了诸如以下这些琐碎的类吗?

class Fixed {
   float hiddenArr[];
   Point pointArray(int position) {
      return new Point(hiddenArr[position*2], hiddenArr[position*2+1]);
   }
}

此外,可以在不让程序员明确声明他们喜欢它的情况下实现它;JVM 已经知道“值类型”(C++ 中的 POD 类型);那些里面只有其他普通旧数据类型的。我相信 HotSpot 在堆栈省略期间使用此信息,没有理由不能对数组执行此操作吗?

于 2012-02-21T23:51:08.687 回答