1

类似于可以防止 JIT 优化远离方法调用吗?我正在尝试跟踪长期存在的数据存储对象的内存使用情况,但是我发现如果我初始化一个存储,记录系统内存,然后初始化另一个存储,有时编译器(可能是 JIT)足够聪明请注意,不再需要这些对象。

public class MemTest {
    public static void main(String[] args) {
       logMemory("Initial State");
       MemoryHog mh = new MemoryHog();
       logMemory("Built MemoryHog");
       MemoryHog mh2 = new MemoryHog();
       logMemory("Built Second MemoryHog"); // by here, mh may be GCed
    }
}

现在链接线程中的建议是保留指向这些对象的指针,但 GC 似乎足够聪明,可以告诉这些对象不再被使用main()。我可以在最后一次调用之后添加对这些对象的logMemory()调用,但这是一个相当手动的解决方案 - 每次我测试一个对象时,我都必须在最后一次调用之后进行某种副作用触发调用logMemory(),否则我可能会得到不一致的结果.

我正在寻找一般案例解决方案;我知道System.out.println(mh.hashCode()+mh2.hashCode())在末尾添加一个电话main()方法就足够了,但我不喜欢这个有几个原因。首先,它引入了对上述测试的外部依赖——如果删除了 SOUT 调用,则 JVM 在内存记录调用期间的行为可能会发生变化。第二,容易出现用户错误;如果上面测试的对象发生变化,或者添加了新对象,用户必须记得手动更新这个 SOUT 调用,否则他们会在测试中引入难以检测的不一致。最后,我根本不喜欢这个解决方案的打印——这似乎是一个不必要的黑客攻击,我可以通过更好地理解 JIT 的优化来避免。最后一点,Patricia Shanahan 的回答提供了一个合理的解决方案(明确打印输出是出于内存健全的目的),但如果可能的话,我仍然想避免它。

所以我最初的解决方案是将这些对象存储在一个静态列表中,然后在主类的 finalize 方法*中迭代它们,如下所示:

public class MemTest {
    private static ArrayList<Object> objectHolder = new ArrayList<>();

    public static void main(String[] args) {
       logMemory("Initial State", null);
       MemoryHog mh = new MemoryHog();
       logMemory("Built MemoryHog", mh); // adds mh to objectHolder
       MemoryHog mh2 = new MemoryHog();
       logMemory("Built Second MemoryHog", mh2); // adds mh2 to objectHolder
    }

    protected void finalize() throws Throwable {
        for(Object o : objectHolder) {
            o.hashCode();
        }
    }
}

但是现在我只解决了这个问题一步——如果 JIT 优化了 finalize 方法中的循环,并决定不需要保存这些对象怎么办?诚然,对于 Java 7 来说,也许简单地将对象保存在主类中就足够了,但是除非有文档证明 finalzie 方法不能被优化掉,否则理论上仍然没有什么可以阻止 JIT/GC 尽早摆脱这些对象,因为我的 finalize 方法的内容没有副作用。

一种可能性是将 finalize 方法更改为:

protected void finalize() throws Throwable {
    int codes = 0;
    for(Object o : loggedObjects) {
        codes += o.hashCode();
    }
    System.out.println(codes);
}

据我了解(我在这里可能是错的),调用System.out.println()会阻止 JIT 摆脱这段代码,因为它是一种具有外部副作用的方法,所以即使它不会影响程序,也不能删除。这是有希望的,但如果我能提供帮助,我真的不希望输出某种乱码。JIT 不能(或不应该!)优化离开System.out.println()调用的事实向我表明 JIT 有副作用的概念,如果我可以告诉它这个 finalize 块有这样的副作用,它永远不应该优化它离开。

所以我的问题:

  • holdijng 是否是主类中的对象列表足以防止它们被 GC 处理?
  • 循环遍历这些对象并.hashCode()在 finalize 方法中调用一些微不足道的东西就足够了吗?
  • 用这种方法计算和打印一些结果是否足够?
  • JIT 是否知道其他方法(例如System.out.println)无法优化掉,或者更好的是,有没有办法告诉 JIT 不要优化掉方法调用/代码块?

*一些快速测试证实,正如我所怀疑的,JVM 通常不会运行主类的 finalize 方法,它会突然退出。JIT/GC 可能仍然不够聪明,无法仅仅因为 finalize 方法存在而对我的对象进行 GC,即使它没有运行,但我不确定情况总是如此。如果它没有记录在案的行为,我不能有理由相信它会保持真实,即使它现在是真实的。

4

2 回答 2

1

mh1的,那时收集垃圾是合法的。那时,没有可能使用该变量的代码。如果 JVM 可以检测到这一点,则相应的MemoryHog对象将被视为不可访问……如果 GC 将在该点运行。

稍后调用 likeSystem.out.println(mh1)将足以阻止对象的收集。在“计算”中使用它也是如此;例如

    if (mh1 == mh2) { System.out.println("the sky is falling!"); }

在主类中保存一个对象列表是否足以防止它们被 GC?

这取决于声明列表的位置。如果列表是一个局部变量,并且之前变得不可访问mh1,那么将对象放入列表中没有任何区别。

循环遍历这些对象并在 finalize 方法中调用像 .hashCode() 这样的微不足道的东西就足够了吗?

在调用该finalize方法时,GC 已经确定该对象不可达。该finalize方法可以防止对象被删除的唯一方法是将其添加到其他(可达)数据结构或将其分配给(可达)变量。

JIT 是否知道其他方法(如 System.out.println)不能被优化掉,

是的......任何使对象可到达的东西。

甚至更好,有没有办法告诉 JIT 不要优化方法调用/代码块?

没有办法做到这一点......除了确保方法调用或代码块做一些有助于执行计算的事情。


更新

首先,这里发生的并不是真正的 JIT 优化。相反,JIT 正在发出某种“映射”,GC 使用该“映射”来确定局部变量(即堆栈上的变量)何时死亡……取决于程序计数器(PC)。

您禁止收集的示例都涉及通过 SOUT 阻止 JIT,我想避免这种有点 hacky 的解决方案。

嘿......任何取决于垃圾收集时间的确切时间都是黑客攻击。您不应该在适当设计的应用程序中这样做。

我更新了我的代码,以明确保存我的对象的列表是主类的静态变量,但似乎如果 JIT 足够聪明,一旦它知道主方法不需要它们,理论上它仍然可以 GC 这些值.

我不同意。在实践中,JIT 无法确定 astatic永远不会被引用。考虑这些情况:

  • 在 JIT 运行之前,似乎什么都不会static s再次使用。JIT 运行后,应用程序加载一个新类,该类引用s. 如果 JIT “优化”了s变量,GC 会将其视为不可访问,并且要么null它要么创建一个悬空引用。当动态加载的类然后查看s它时,会看到错误的值......或更糟。

  • 如果应用程序......或应用程序使用的任何库......使用反射,那么它可以引用任何static变量的值,而 JIT 不会检测到这一点。

因此,虽然理论上可以进行这种优化,但在少数情况下:

  • 在绝大多数情况下,你不能,并且
  • 在极少数情况下,回报(在性能改进方面)很可能可以忽略不计。

我同样更新了我的代码,以澄清我说的是主类的 finalize 方法。

主类的 finalize 方法无关紧要,因为:

  • 您没有创建主类的实例,并且
  • finalize 方法不能引用另一个方法(例如main方法)的局部变量。

...它的存在阻止了 JIT 破坏我的静态列表。

不对。无论如何,该static列表不能被核弹;往上看。

据我了解,JIT 知道 SOUT 有一些特别之处,这会阻止它优化此类调用。

没有什么特别的sout。这只是我们知道会影响计算结果的东西,因此我们知道JIT 不能合法地优化掉。

于 2013-03-04T03:28:54.953 回答
1

这是一个可能有点矫枉过正的计划,但应该是安全且相当简单的:

  • 保留对对象的引用列表。
  • 最后,遍历对 hashCode() 结果求和的列表。
  • 打印哈希码的总和。

打印总和可确保最终循环无法优化。对于每个对象创建,您唯一需要做的就是将其放入 List add 调用中。

于 2013-03-04T03:34:03.587 回答