【问题标题】:Will the JVM optimise out unused fieldsJVM会优化未使用的字段吗
【发布时间】:2021-09-11 05:13:47
【问题描述】:

在优化我的代码时,我正在尝试更多地了解 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];
    }
}

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

[答: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 实例将如下所示:

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

【问题讨论】:

  • JIT 可能。但在现代系统中,担心如此微小的优化几乎肯定是毫无意义的。你意识到计算机现在有千兆字节的内存,对吧?在编写 Java 时,您很幸运拥有 32 兆字节。我想我的电脑当时有 5 兆字节。担心单个int 的空间仍然毫无意义。也许在 Atari 2600 时代,这是个问题。
  • 其实我觉得大部分情况下JIT都做不到。问题在于,如果它确实优化了它们,然后证明它们是必需的(用于调试,在新的子类中等),JVM 无法找到/更新对象节点以添加空间来存储字段。
  • @ElliottFrisch 是的,但是如果您正在处理一个 65536 字节长的数组,那感觉会浪费很多内存。
  • “但是如果你正在处理一个 65536 字节长的数组呢?”。在Java中,数组是独立的堆节点,所以字段只需要空间来引用数组;即 4 或 8 个字节。如果不需要数组,则字段不应该被初始化......由该字段所属的类。 (理论上,冗余分配可以被优化掉,但我怀疑它会被实现......原因与我之前的评论类似。)
  • 因此,在您的示例中,C 字段使用的内存是 4 或 8 个字节,而不是 1024。

标签: java optimization memory jvm unused-variables


【解决方案1】:

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

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

JLS §12.6.1., Implementing Finalization

可以设计优化程序的转换,将可到达的对象的数量减少到比那些天真地认为是可到达的要少。例如,Java 编译器或代码生成器可能会选择将不再使用的变量或参数设置为 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 实例,它可能会发生。对于这些情况,即使存在可能使用该字段的方法也不会造成问题,正如优化器此时所知道的那样,它们是否在对象的生命周期内实际被调用。

【讨论】:

  • 嗯,有道理,我有点忘记了在 java 中,大型数据结构没有与它们所属的对象捆绑在一起。所以本质上,如果我从不构造 long[512],它就永远不会被创建,并且内存不会被闲置。
猜你喜欢
  • 2010-09-11
  • 1970-01-01
  • 1970-01-01
  • 2011-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多