JVM GC能否在参考比较的中间移动对象,即使双方都指向相同的对象,也会导致比较失败?
众所周知,GC有时会在内存中移动对象。 而且据我的理解,只要所有的引用在对象被移动的时候(在任何用户代码被调用之前)都被更新,这应该是完全安全的。
但是,我看到有人提到参考比较可能是不安全的,因为GC在参考比较的中间移动了对象,即使两个参考都指的是同一个对象,比较也会失败。
也就是说,有什么情况下下面的代码不会打印“真”?
Foo foo = new Foo(); Foo bar = foo; if(foo == bar) { System.out.println("true"); }
我试着用googlesearch,缺乏可靠的结果让我相信说这个的人是错误的,但是我find了一个似乎表明他是正确的论坛post( 就像这个 )。 但是这个线程也有人说,不应该是这样的。
Java字节码指令总是相对于GC是primefaces的(即,在执行单个指令时不会发生循环)。
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在执行字节码指令期间能够运行,对象的引用也不会改变。 这个循环之前和之后仍然是同一个对象。
所以,总之,对你的问题的答案是否定的,它总是会输出真实的。
资源:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.21.3
简短的回答是,看看java 8规范:不。
==
运算符将始终执行对象相等性检查(假定两个引用都为空)。 即使对象被移动, 对象仍然是同一个对象 。
如果你看到这样的效果,你刚刚发现了一个JVM的错误 。 去提交吧。
当然,这可能是因为一些奇怪的性能原因,JVM的一些模糊的实现并没有强制执行。 如果是这样的话,那么简单地从JVM上移动是明智的…
TL; DR
你不应该那么想那种东西,这是一个黑暗的地方。 Java已经明确地说明了它的规范,你不应该怀疑它。
2.7。 对象的表示
Java虚拟机没有为对象指定任何特定的内部结构。
资料来源: JVMS SE8。
我对此表示怀疑! 如果你可能怀疑这个非常基本的操作者,你会发现自己怀疑其他的一切,对信任问题感到沮丧和偏执不是你想成为的地方。
如果发生在我身上呢? 这样的错误不应该存在。 您提供的Oracle讨论报告了几年前发生的一个错误,不知何故,讨论OP决定无缘无故地将其popup来,要么没有这些错误的可靠文档现在存在。 但是,如果发生了这样的错误或其他问题,请在此提交。
为了让您的担心消失,Java已经将指针指针调整到了JVM 指针表中 ,您可以在这里阅读更多有关它的信息 。
GC只在程序中的状态被定义好的地方发生,JVM有确切的知识,一切都在寄存器/堆栈/堆中,所以当一个对象被移动时,所有的引用都可以被修正。
即它们不能在执行任意汇编指令之间发生。 从概念上讲,您可以将它们视为在JVM的字节码指令之间发生,GC调整之前指令生成的所有引用。
你用错误的前提提出一个问题。 由于==
运算符不会比较内存位置,因此改变内存位置本身并不明智。 应用于引用的==
运算符比较所引用对象的标识 ,而不pipeJVM如何实现它。
为了命名一个抵消通常理解的例子,分布式JVM可能会在不同计算机的RAM中保存对象,包括本地拷贝的可能性。 所以简单地比较地址是行不通的。 当然,这取决于JVM实现,以确保Java语言规范中定义的语义不会改变。
如果一个特定的JVM实现通过直接比较对象的内存位置来实现引用比较, 并且具有可以改变内存位置的垃圾回收器,当然,由JVM来确保这两个特性不能相互干扰不兼容的方式。
如果你对这种工作方式感到好奇,比如在优化的JIT编译代码中,粒度不如你想象的那么好。 包括前向分支在内的每个顺序代码都可以被认为运行得足够快,从而可以将垃圾回收延迟到完成。 所以垃圾收集不能在优化代码内部随时发生,但必须允许在某些点上,例如
- 后向分支(注意,由于循环展开,不是每个循环迭代都意味着后向分支)
- 内存分配
- 线程同步操作
- 调用未被内联/分析的方法
- 也许有些特别的,我忘了
因此,JVM会发出包含某些已知的“安全点”的代码,这些引用当前正在被保留,如果有必要,如何replace它们,当然,更改位置对正确性没有影响。 在这些点之间,代码可以运行,而不必关心改变内存位置的可能性,而垃圾收集器将在必要时等待代码达到安全点,这是保证在有限的短时间内发生的。
但是,如上所述,这些是实施细节。 在forms层面上,诸如改变内存位置之类的东西不存在,所以不需要明确指定不允许改变Java代码的语义。 没有实现细节被允许做到这一点。
我知道你问这个问题后,有人说这样做的行为,但真的问,如果这样做的行为是不正确的方法来评估他们说什么。
你真正应该问什么(主要是你自己,别人只有当你不能决定答案的时候)是否允许GC使比较失败,在逻辑上应该成功是有意义的(基本上,包括一个弱的参考)。
答案显然是“不”,因为除了“你好,世界”之外,它可能会打破任何东西,甚至可能是这样。
所以,如果允许,这是一个错误 – 无论是在规范或实施。 现在由于规范和实现都是由人写的,所以可能存在这样的错误。 如果是这样,它将被报告,几乎可以肯定是固定的。
不,因为这将是公然荒谬的和专利错误。
GC在幕后非常谨慎,避免灾难性地破坏一切 。 特别是,当线程在安全点暂停时,它只会移动对象,而安全点是由JVM生成的运行代码中的特定位置,以便线程暂停。 处于安全点的线程处于已知状态,其中寄存器和内存中所有可能的对象引用的位置是已知的,所以GC可以更新它们以指向对象的新地址。 垃圾收集不会破坏你的比较操作。
Java对象持有对“对象”的引用而不是存储对象的内存空间。
Java是这样做的,因为它允许JVM自己pipe理内存使用情况(例如垃圾回收器),并在不影响客户端程序的情况下提高全局使用率。
作为改进的例子,第一个X int(我不记得有多less)总是在内存中分配来执行for循环fatser(例如: for (int i =0; i<10; i++)
)
作为对象引用的例子,试着创build一个并尝试打印它
int[] i = {1,2,3}; System.out.println(i);
你会看到Java返回一个以[I@
开始的东西。 它指的是指向一个“int的数组”,然后是对该对象的引用。 不是内存区!