垃圾收集器如何更新推送到操作数堆栈的引用?

neo*_*ert 2 java garbage-collection jvm bytecode

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

Hol*_*ger 8

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

以下代码

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

将(通常)编译为

  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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

这也意味着在编译的优化代码中,可达性可能与简单解释执行中完全不同,即可能不存在未使用的变量,但当优化代码与它们的字段的副本,例如在 CPU 寄存器中。

  • 是的,事实上这正是它在字节码验证期间所做的事情,这是在加载类时执行的。 (3认同)
  • 这就是我所说的“*一个实现甚至可能有一个分析代码来保存第一次分析的类型信息*”,这通常是在验证期间收集的信息。但是,您必须记住,为每条指令保存大量数据,而对于单个指令来说,在停止世界阶段成为当前指令的可能性非常低。仅将它们保留用于分支合并点(堆栈映射表提供)、方法调用和分配指令并即时推断其他指令会更有意义。 (2认同)