JVM 会优化出未使用的字段吗

问题描述

在优化我的代码时,我试图更多地了解 JVM,并且很好奇它是否(或更具体地以哪些方式)优化了未使用的字段?

我假设如果您在类中有一个从未写入或读取过的字段,则在运行代码时,该字段将不存在于类中。假设你有一个看起来像这样的类:

public class Foo {
    public final int A;
    public final float B;
    private final long[] C = new long[512];
}

并且您只使用了变量 A 和 B,那么您可能会看到初始化、维护和释放变量 C 对于本质上是垃圾数据的内容来说是多么浪费时间。首先,我假设 JVM 会发现这一点是否正确?

现在我的第二个也是更重要的例子是 JVM 是否在这里考虑了继承?比如说 Foo 看起来更像这样:

public class Foo {
    public final int A;
    public final float B;
    private final long[] C = new long[512];

    public long get(int i) {
        return C[i];
    }
}

然后我假设这个类将存储在内存中的某个地方,就像:

[ A:4 | B:4 | C:1024 ]

所以如果我有一个看起来像这样的第二堂课:

public class Bar extends Foo {
    public final long D;
    
    @Override public long get(int i) {
        return i * D;
    }
}

然后突然这意味着字段 C 从未使用过,因此内存中的 Bar 实例将如下所示:

[ A:4 | B:4 | C:1024 | D:8 ][ A:4 | B:4 | D:8 ]

解决方法

为了证明一个字段是完全未使用的,即不仅直到现在还没有使用,而且在将来也没有使用,仅仅private和声明类未使用是不够的。字段也可以通过反射或类似方式访问。基于此构建的框架甚至可能位于不同的模块中,例如序列化在 java.base 模块内实现。

此外,在对象的垃圾收集是可观察的情况下,例如对于具有非平凡 finalize() 方法或指向对象的弱引用的类,适用其他限制:

JLS §12.6.1.,Implementing Finalization

可以设计优化程序的转换,将可达的对象数量减少到比天真地认为可达的对象数量更少。例如,Java 编译器或代码生成器可能会选择将不再用于 null 的变量或参数设置为 null,从而使此类对象的存储可能更快地被回收。

如果对象字段中的值存储在寄存器中,则会出现另一个例子。然后程序可能会访问寄存器而不是对象,并且永远不会再次访问该对象。这意味着该对象是垃圾。请注意,只有当引用在堆栈上,而不是存储在堆中时,才允许进行这种优化。

本节还给出了一个禁止此类优化的示例:

class Foo {
    private final Object finalizerGuardian = new Object() {
        protected void finalize() throws Throwable {
            /* finalize outer Foo object */
        }
    }
}

规范强调,即使在其他情况下完全未使用,内部对象也不能在外部对象变得无法访问之前完成。

这不适用于没有终结器的 long[] 数组,但它需要进行更多检查,同时降低这种假设优化的通用性。

由于 Java 的典型执行环境允许动态添加新代码,因此无法证明这种优化将保持不可观察。所以答案是,没有这样的优化可以在实践中消除类中未使用的字段。


然而,有一种特殊情况。当优化器正在查看的代码覆盖了对象的整个生命周期时,JVM 可能会优化类的特定用例。这由 Escape Analysis 检查。

当满足前提条件时,可以执行Scalar Replacement,这将消除堆分配并将字段转换为等效的局部变量。一旦您的对象被分解为三个变量 ABC,它们将受到与局部变量相同的优化。因此,如果它们从未被读取或包含可预测的值,它们最终可能会出现在 CPU 寄存器中而不是 RAM 中,或者被完全消除。

不是在这种情况下,您不必担心继承关系。由于此优化仅适用于跨越对象整个生命周期的代码路径,因此它包括其分配,因此,其确切类型是已知的。并且对对象进行操作的所有方法都必须已经内联。

从那时起,外部对象不再存在,消除未使用的内部对象也不会与上面引用的规范相矛盾。

因此通常没有优化删除未使用的字段,但对于特定的 FooBar 实例,它可能会发生。对于这些情况,即使存在可能使用该字段的方法也不会造成问题,因为优化器此时知道它们是否在对象的生命周期内被实际调用。