JVM GC 会不会在引用比较过程中移动对象,导致双方都引用同一个对象时比较失败?

Can the JVM GC move objects in the middle of a reference comparison, causing a comparison to fail even when both sides refer to the same object?

众所周知,GC 有时会在内存中移动对象。据我了解,只要在对象移动时(在调用任何用户代码之前)更新所有引用,这应该是绝对安全的。

但是,我看到有人提到引用比较可能不安全,因为对象在引用比较过程中被 GC 移动,这样即使两个引用都指向同一个对象,比较也可能失败?

也就是说,有没有什么情况下下面的代码不会打印"true"?

Foo foo = new Foo();
Foo bar = foo;
if(foo == bar) {
    System.out.println("true");
}

我试着用谷歌搜索这个,但缺乏可靠的结果让我相信说这个的人是错误的,但我确实找到了各种各样的论坛帖子 (like this one),这似乎表明他是正确的。但是那个帖子也有人说不应该这样。

Java 对象持有对 "object" 的引用,而不是对存储对象的内存 space 的引用。

Java 这样做是因为它允许 JVM 自行管理内存使用(例如垃圾收集器)并在不直接影响客户端程序的情况下改善全局使用。

作为改进实例,第一个 X int(我不记得有多少)总是在内存中分配以执行 for loop fatser(例如:for (int i =0; i<10; i++)

作为对象引用的示例,只需尝试创建并打印它

int[] i = {1,2,3};
System.out.println(i);

你会看到 Java 返回一个以 [I@ 开头的东西。它说的是 "array of int at" 上的点,然后是对该对象的引用。不是内存区!

来源:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.21.3

简短的回答是,查看 java 8 规范:否。

== 运算符将始终执行对象相等性检查(假设两个引用都不为空)。即使物体移动了,物体还是原来的物体.

如果你看到这样的效果,你刚刚发现了一个JVM bug。去提交吧。

当然,这可能是 JVM 的某些模糊实现出于任何奇怪的性能原因而没有强制执行此操作。如果是这种情况,明智的做法是简单地从该 JVM 继续前进...

GC 仅发生​​在程序中状态为 well-defined 且 JVM 准确了解所有内容在 registers/the stack/on 堆中的位置,因此可以修复所有引用当对象移动时向上。

即它们不能在执行任意汇编指令之间发生。从概念上讲,您可以认为它们发生在 JVM 的字节码指令与 GC 调整先前指令生成的所有引用之间。

Java 字节码指令对于 GC 而言始终是原子的(即在执行单个指令时不会发生循环)。

GC 唯一一次 运行 是在两个字节码指令之间。

查看 javac 为代码中的 if 指令生成的字节码,我们可以简单地检查 GC 是否会产生任何影响:

// a GC here wouldn't change anything
ALOAD 1
// a GC cycle here would update all references accordingly, even the one on the stack
ALOAD 2
// same here. A GC cycle will update all references to the object on the stack
IF_ACMPNE L3
// this is the comparison of the two references. no cycle can happen while this comparison
// "is running" so there won't be any problems with this either

此外,即使 GC 能够在执行字节码指令期间 运行,对象的引用也不会改变。循环前后仍然是同一个对象。

所以,简而言之,你的问题的答案是否定的,它将始终输出 true。

TL;DR

你不应该考虑那种东西,这是一个黑暗的地方。 Java 已经清楚地说明了它的规格,你永远不要怀疑它。

2.7. Representation of Objects

The Java Virtual Machine does not mandate any particular internal structure for objects.

来源:JVMS SE8.

我对此表示怀疑! 如果您可能怀疑这个非常基本的运算符,您可能会发现自己怀疑其他一切,对信任问题感到沮丧和偏执不是您想要的地方.

万一发生在我身上怎么办?这样的bug不应该存在。您提供的 Oracle 讨论报告了一个多年前发生的错误,并且不知何故讨论 OP 决定无缘无故地弹出它,或者没有关于此类错误的可靠文档。但是,如果您遇到此类错误或任何其他错误,请提交 here

为了消除您的后顾之忧,Java 已将指针到指针的方法调整到 JVM pointer table, you can read more about it's efficenty here

我知道你是在有人说它的行为方式之后才问这个问题,但真正询问它是否确实如此行为并不是评估他们所说内容的正确方法。

您真正应该问的(主要是您自己,只有当您无法决定答案时才问其他人)是否允许 GC 导致比较失败而逻辑上应该成功(基本上任何不包含弱引用的比较)。

答案显然是 "no",因为它几乎会破坏 "hello, world" 之外的任何东西,甚至可能会破坏。

因此,如果允许的话,它是一个错误 -- 无论是在规范中还是在实现中。现在,由于规范和实现都是由人编写的,因此可能存在这样的错误。如果是这样,它将被报告并且几乎肯定会被修复。

你问的问题前提错误。由于 == 运算符不比较内存位置,因此它对内存位置 本身 的变化不敏感。应用于引用的 == 运算符比较引用对象的 身份 ,而不管 JVM 如何实现它。

举个反常理解的例子,分布式JVM可能有对象保存在不同计算机的RAM中,包括本地副本的可能性。所以简单地比较地址是行不通的。当然,由 JVM 实现来确保 Java 语言规范中定义的语义不会改变。

如果一个特定的JVM实现通过直接比较对象的内存位置来实现引用比较并且有一个可以改变内存位置的垃圾收集器,当然,这取决于JVM以确保这两个功能不会以不兼容的方式相互干扰。

如果您对它的工作原理感到好奇,例如在经过优化的 JIT 编译代码中,粒度并不像您想象的那么精细。每个顺序代码,包括前向分支,都可以被认为 运行 足够快以允许延迟垃圾收集完成。因此垃圾回收不会在优化代码中随时发生,但必须在某些点允许,例如

  • 向后分支(请注意,由于循环展开,并非每个循环迭代都意味着向后分支)
  • 内存分配
  • 线程同步操作
  • 调用了一个尚未被调用的方法inlined/analyzed
  • 可能有什么特别的,我忘记了

因此 JVM 发出的代码包含已知的某些“安全点”,当前持有哪些引用,如何替换它们,如果有必要,当然,更改位置对正确性没有影响。在这些点之间,代码可以 运行 而不必关心更改内存位置的可能性,而垃圾收集器将在必要时等待代码到达安全点,这保证在有限的、相当短的时间内发生。

但是,如前所述,这些是实施细节。在形式层面上,像改变内存位置这样的事情是不存在的,所以没有必要明确规定它们不允许改变Java代码的语义。不允许任何实施细节这样做。

不,因为那将是公然荒谬和专利错误。

GC 在幕后非常小心,以避免灾难性地破坏一切。特别是,它只会在线程暂停在 safepoints 时移动对象,这是 JVM 为要暂停的线程生成的 运行 代码中的特定位置。处于安全点的线程处于已知状态,其中所有可能的对象引用在寄存器和内存中的位置都是已知的,因此 GC 可以更新它们以指向对象的新地址。垃圾收集不会破坏您的比较操作。