【问题标题】:How does the Garbage Collector update the references pushed to the operand stack?垃圾收集器如何更新推送到操作数堆栈的引用?
【发布时间】:2020-03-03 16:21:29
【问题描述】:

在堆中移动对象时,JVM可以轻松更新局部变量、静态引用、类实例或对象数组实例的引用。但是它如何更新推送到操作数堆栈的引用呢?

【问题讨论】:

  • 你认为什么会使这变得困难?
  • 操作数栈中的值可以是任何类型。我们如何知道它是引用还是整数?
  • 我们到底是怎么知道你是如何实现你的虚拟机的?
  • 你为什么假设 JVM跟踪堆栈上每个值的类型?
  • HotSpot JVM 使用OopMap structures 来查找和更新堆栈上的对象引用。

标签: java garbage-collection jvm bytecode


【解决方案1】:

局部变量和操作数堆栈中的条目之间没有根本区别。两者都存在于同一个堆栈帧中。两者都没有正式声明,并且都需要 JVM 执行推理以识别它们的实际用途。

以下代码

public static void example() {
    {
        int foo = 42;
    }
    {
        Object bar = "text";
    }
    {
        long x = 100L;
    }
    {
        Object foo, bar = new Object();
    }
}

将(通常)编译为

  public static void example();
    Code:
       0: bipush        42
       2: istore_0
       3: ldc           #1                  // String text
       5: astore_0
       6: ldc2_w        #2                  // long 100l
       9: lstore_0
      10: new           #4                  // class java/lang/Object
      13: dup
      14: invokespecial #5                  // Method java/lang/Object."<init>":()V
      17: astore_1
      18: return

请注意堆栈帧中索引0 处的局部变量如何被重新分配不同类型的值。作为奖励,变量索引1 的最后存储会使索引0 处的变量无效,否则它将包含long 值的悬空一半。

没有关于局部变量类型的额外提示,调试信息是可选的,堆栈映射表仅在代码包含分支时存在。

判断一个局部变量是否包含引用的唯一方法是跟随程序流程并追溯指令的效果。这确实意味着推断操作数堆栈上的值,因为没有它,我们甚至不知道store 指令将什么放入变量中。

验证器会这样做,甚至是强制性的,垃圾收集器或 JVM 的任何支持代码也可以这样做。一个实现甚至可以有一个分析代码来保存第一次分析的类型信息,这将是验证。

但是即使每次垃圾收集器需要这些信息时都重新构建这些信息,开销也不会是天文数字。垃圾收集器只定期运行,它只需要当前执行的方法的这些信息。这只是关于解释执行的全部内容。

当 JIT 编译器生成代码时,它无论如何都需要利用类型信息,并且可以为垃圾收集器准备信息,但它只会在生成的代码检查的称为 safepoints 的某些点上这样做是否有出色的垃圾收集。这意味着在这些点之间,数据不需要采用垃圾收集器可以理解的形式,优化后的代码可能会假设垃圾收集器在处理对象时不会重新定位对象。

这也意味着在编译后的优化代码中,可达性可能与简单解释执行中完全不同,即可能不存在未使用的变量,但即使从源代码的角度来看正在使用的对象在优化时也可能被认为是未使用的代码适用于其字段的副本,例如在 CPU 寄存器中。

【讨论】:

  • 在特定执行点(程序计数器)的局部变量表和操作数堆栈的类型是否始终相同(独立于先前的执行分支)?也许 JVM 可以为每个可能的程序计数器值的每个方法“提前”构建一个类型映射表。
  • 是的,事实上这正是它在字节码验证期间所做的,这是在加载类时执行的。
  • 这就是我所说的“一个实现甚至可以有一个分析代码来保存第一次分析的类型信息”,这通常是验证期间收集的信息。但是,您必须记住,为每条指令保留它是大量数据,而在停止世界阶段成为当前指令的可能性对于单个指令来说非常低。将它们仅保留用于分支合并点(堆栈映射表提供)、方法调用和分配指令并即时推断其他内容会更有意义。
猜你喜欢
  • 2013-12-30
  • 2012-06-03
  • 2012-05-29
  • 2015-02-21
  • 2014-03-27
  • 2013-02-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多