为了账号安全,请及时绑定邮箱和手机立即绑定

JVM 何时将成员变量引用存储在堆栈上?

JVM 何时将成员变量引用存储在堆栈上?

猛跑小猪 2022-06-04 10:39:19
我正在阅读Java SE 规范的第 12.6.1 节,它说:可以设计优化程序的转换,将可到达的对象的数量减少到比那些天真地认为是可到达的要少。例如,Java 编译器或代码生成器可能会选择将不再使用的变量或参数设置为 null,以使此类对象的存储空间可能更快地被回收。如果对象字段中的值存储在寄存器中,则会出现另一个示例。然后程序可能会访问寄存器而不是对象,并且永远不会再次访问对象。这意味着该对象是垃圾。请注意,仅当引用在堆栈上而不是存储在堆中时才允许进行这种优化。相关代码为:class Foo {    private final Object finalizerGuardian = new Object() {        protected void finalize() throws Throwable {            /* finalize outer Foo object */        }    }} 我的问题是哪种 JVM 会将 finalizerGuardian 存储在堆栈而不是堆中,为什么?
查看完整描述

2 回答

?
湖上湖

TA贡献2003条经验 获得超2个赞

代码示例用于说明引用文本的最后一句话,“请注意,仅当引用在堆栈上而不是存储在堆中时才允许进行这种优化”,您将其从解释中扯下来有点奇怪文本:


例如,考虑Finalizer Guardian模式:


 class Foo {

     private final Object finalizerGuardian = new Object() {

         protected void finalize() throws Throwable {

             /* finalize outer Foo object */

         }

     }

 } 

super.finalize如果子类覆盖并且finalize未显式调用super.finalize.


如果允许对存储在堆上的引用进行这些优化,那么 Java 编译器可以检测到该finalizerGuardian字段从未被读取,将其清空,立即收集对象,并尽早调用终结器。这与意图背道而驰:程序员可能想在 Foo 实例变得无法访问时调用 Foo 终结器。因此,这种转换是不合法的:只要外部类对象可访问,内部类对象就应该是可访问的。


所以代码示例说明了一个限制。规范中提到的“优化转换”包括在逃逸分析证明对象是纯本地的之后应用的对象标量化,换句话说,优化下的代码跨越对象的整个生命周期。


但它不需要这样的本地对象。正如规范已经提到的,优化的代码可以将对象的字段保留在 CPU 寄存器中,而无需重新读取它们,因此,不再需要保留对象引用。同样,仍然在范围内的引用变量可能未被使用。如果该引用是对某个对象的唯一引用,则将其从优化代码中删除允许更早地进行垃圾回收。


这两种情况仍然允许Foo实例被更早地消除或收集。这反过来将允许较早地收集 . 引用的对象(不再是)finalizerGuardian。但这并不能抵消这种限制的意图。规范将优化限制为不允许内部对象早于外部对象被收集,但将两者收集在一起没有问题,包括早于天真的预期。


通常,任意大的对象图可能会在单个垃圾收集周期中被收集,可能比天真预期的要早,甚至完全被优化掉。


查看完整回答
反对 回复 2022-06-04
?
largeQ

TA贡献2039条经验 获得超7个赞

这种优化(逃逸分析)的一个经典例子是一个带有Point类的计算:



class Point {

    double x;

    double y;


    public Point(final double x, final double y) {

        this.x = x;

        this.y = y;

    }


    double length() {

        return Math.sqrt(x * x + y * y);

    }


    static double calc() {

        double result = 0;

        for (int i = 0; i < 100; i++) {

            // this allocation will be optimized 

            Point point = new Point(i, i);

            result += point.length();

        }

        return result;

    }

}

内联后就new不需要了,因为我们可以将所有字段提取到局部变量中,例如


Point point = new Point(i, i);

double x = point.x;

double y = point.y;

result += Math.sqrt(x * x + y * y);

->


Point point = new Point(i, i);

double x = i;

double y = i;

result += Math.sqrt(x * x + y * y);

现在很明显这new Point(i, i)是没用的,JIT 只是删除了这一行。


请注意,分配是在堆栈上,即在局部变量中。如果它在一个字段中,我们将无法进行优化,因为它存储在堆中。它是如何工作的。


关于您的代码被剪断:finalizerGuardian将始终在字段中(存储在堆中),而 JVM 对此分配无能为力。此外,如果Point上面示例中的类包含此类字段,我认为转义分析无法删除分配,因为它可能会改变原始行为。


查看完整回答
反对 回复 2022-06-04
  • 2 回答
  • 0 关注
  • 98 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信