垃圾收集是否会更改 Java 中的对象地址?

Does garbage collection change the object addresses in Java?

我读到垃圾收集会在 运行 时导致内存碎片问题。为了解决这个问题,压缩是由 JVM 完成的,它获取所有活动对象并为它们分配连续的内存。 这意味着对象地址必须不时更改?另外,如果发生这种情况,

  1. 是否也重新分配了对这些对象的引用?
  2. 这不会导致严重的性能问题吗? Java如何应对?

如果您在内存中移动一个对象,它的地址将会改变。因此,指向它的引用将需要更新。当连续(在内存中)对象序列中的对象被删除时,就会发生内存碎片。这会在内存 space 中创建一个 hole,这通常是不好的,因为连续的内存块具有更快的访问时间和更高的适合 chache 行的可能性等等。应该注意的是,使用间接表可以防止引用更新达到所使用的最大间接级别。

垃圾收集具有适度的性能开销,不仅在 Java 中而且在其他语言中也是如此,例如 C#。至于如何 Java 处理这个问题,执行垃圾收集的策略以及如何最小化其对性能的影响取决于所使用的特定 JVM,因为每个 JVM 都可以随心所欲地执行垃圾收集;唯一的要求就是满足JVM规范。

但是,作为程序员,您应该遵循一些最佳实践,以充分利用垃圾收集并最大限度地减少其对应用程序的性能影响。见this, also this, , this blog post, and this other blog post. You might want to check the JVM specs不过有点密。

I read that garbage collection can lead to memory fragmentation problem at run-time.

这不是垃圾收集堆独有的问题。当您有一个手动管理的堆和空闲内存的顺序与前面的分配不同时,您也可能会得到一个碎片化的堆。并且能够具有不同于 last-in-first-out 自动存储顺序(即堆栈内存)的生命周期,是使用堆内存的主要动机之一。

To solve this problem, compacting is done by the JVM where it takes all the active objects and assigns them contiguous memory.

不一定都是 object。典型的实现策略会将内存划分为逻辑区域,并且只将 object 从一个特定区域移动到另一个区域,而不是一次移动所有现有的 object。这些策略可能包含 objects 的年龄,例如世代收集器将年轻一代的 objects 从 Eden space 移动到 Survivor space,或者分布剩下的 objects,就像“垃圾优先”收集器一样,顾名思义,它会首先清除垃圾率最高的片段,这意味着获得空闲连续内存块的工作量最少。

This means that the object addresses must change from time to time?

当然可以。

Also, if this happens,

  1. Are the references to these objects also re-assigned?

规范没有规定如何实现 object 引用。间接指针可以消除调整所有引用的需要,另请参见 this Q&A。但是,对于使用直接指针的 JVM,这确实意味着需要调整这些指针。

  1. Won't this cause significant performance issues? How does Java cope with it?

首先,我们必须考虑我们从中获得了什么。 “消除碎片化”本身并不是目的。如果我们不这样做,我们必须扫描可达的 objects 以寻找它们之间的间隙,并创建一个数据结构来维护这些信息,我们称之为“空闲内存”。我们还需要将内存分配实现为在此数据结构中搜索匹配块,或者在未找到精确匹配时拆分块。与从连续空闲内存块进行分配相比,这是一个相当昂贵的操作,我们只需要将指针指向下一个空闲字节所需的大小。

鉴于分配比垃圾 collection 更频繁地发生,后者仅在内存已满(或超过阈值)时运行,这确实证明了更昂贵的复制操作是合理的。它还意味着仅使用更大的堆就可以解决性能问题,因为它减少了所需的垃圾收集器运行次数,而幸存者 objects 的数量不会随着内存的增加而扩展(无法访问 objects保持无法访问,无论您将 collection 推迟多长时间)。事实上,推迟 collection 增加了更多 object 在此期间变得无法访问的可能性。也与 this answer.

比较

适配引用的成本并不比标记阶段遍历引用的成本高多少。事实上,non-concurrent 收集器甚至可以将这两个步骤结合起来,在第一次遇到时传输 object 并适应随后遇到的引用,而不是标记 object。实际复制是更昂贵的方面,但如上所述,通过不复制所有 object 而是使用基于典型应用程序行为的某些策略(如分代方法或“垃圾优先”策略)来减少它,以最小化所需的工作。