防止Java 7过早GC

时间:2013-03-04 02:55:34

标签: java garbage-collection benchmarking jit

类似于Can JIT be prevented from optimising away method calls?我试图跟踪长期存储数据存储对象的内存使用情况,但是我发现如果我初始化存储,记录系统内存,然后初始化另一个存储,有时候编译器(可能是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()调用后添加对这些对象的调用,但这是一个相当手动的解决方案 - 每次测试一个对象时,我都必须在最后{{1}之后进行某种副作用触发调用调用,或者我可能得到不一致的结果。

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

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

main()

但是现在我只是将问题卸载了一步 - 如果JIT在finalize方法中优化了循环,并决定不需要保存这些对象怎么办?不可否认,也许简单地将主题类中的对象保存在Java 7中已经足够了,但除非有文档证明finalzie方法无法优化,否则理论上仍然没有 阻止JIT / GC获取尽早摆脱这些对象,因为我的finalize方法的内容没有任何副作用。

一种可能性是将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();
        }
    }
}

据我了解(我可能在这里错了),调用protected void finalize() throws Throwable { int codes = 0; for(Object o : loggedObjects) { codes += o.hashCode(); } System.out.println(codes); } 将阻止JIT摆脱此代码,因为它是一种具有外部副作用的方法,因此即使它不会影响该程序,它无法删除。这很有希望,但如果我能提供帮助的话,我真的不想要输出某种胡言乱语。 JIT不能(或不应该!)优化System.out.println()次调用的事实告诉我,JIT有一个副作用的概念,如果我能告诉它这个最终化块有这样的副作用,它永远不应该优化它。

所以我的问题:

  • 是否在主类中保留了一个对象列表,以防止它们被GCed?
  • 是否循环遍历这些对象并在finalize方法中调用像System.out.println()这样的微不足道的东西?
  • 计算和打印这个方法的结果是否足够?
  • JIT是否知道其他方法(如.hashCode())无法优化,或者甚至更好,是否有某种方法可以告诉JIT不要优化方法调用/代码块?< / LI>

*正如我所怀疑的,一些快速测试确认JVM通常不会运行主类的finalize方法,它会突然退出。 JIT / GC可能仍然不够智能GC我的对象只是因为finalize方法存在,即使它没有运行,但我不相信总是如此。如果没有记录的行为,我无法合理地相信它会保持正确,即使现在也是如此。

2 个答案:

答案 0 :(得分:1)

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

稍后调用System.out.println(mh1)就足以抑制对象的收集。所以会在“计算”中使用它; e.g。

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

  

是否在主类中保留了一个对象列表,以防止它们被GCed?

取决于声明列表的位置。如果列表是局部变量,并且在mh1之前变为无法访问,那么将对象放入列表将没有任何区别。

  

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

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

  

JIT是否知道其他方法(如System.out.println)无法优化,

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

  

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

无法做到这一点......除了确保方法调用或代码块执行有助于执行计算的内容之外。


<强>更新

首先,这里发生的事情并不是真正的JIT优化。更确切地说,JIT正在发出某种“地图”,GC用它来确定何时局部变量(即堆栈上的变量)已经死...取决于程序计数器(PC)。

  

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

嘿......任何事情都取决于什么时候收集垃圾的确切时间是一个黑客。你不应该在一个设计合理的应用程序中这样做。

  

我更新了我的代码,以清楚地表明持有我的对象的列表是主类的静态变量,但似乎JIT足够聪明,一旦它知道主要方法没有理论上它仍然可以GC这些值。需要他们。

我不同意。实际上,JIT无法确定永远不会引用static。考虑以下情况:

  • 在JIT运行之前,似乎没有任何内容会再次使用static s。运行JIT后,应用程序将加载一个引用s的新类。如果JIT“优化”s变量,GC会将其视为无法访问,并null或创建悬空引用。当动态加载的类然后查看s时,它会看到错误的值......或者更糟。

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

因此,虽然理论上可以做到这一点,但优化的案例很少:

  • 在绝大多数情况下,你不能和
  • 在少数情况下,你可以获得的回报(在绩效改善方面)很可能是微不足道的。
  

我同样更新了我的代码以澄清我正在谈论主类的finalize方法。

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

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

...它的存在阻止了JIT核对我的静态列表。

不正确。无论如何,static列表无法被激活;见上文。

  

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

sout没有什么特别之处。这只是我们知道影响计算结果的因素,因此我们知道 JIT无法合法优化。

答案 1 :(得分:1)

这是一个可能过度的计划,但应该是安全且相当简单的:

  • 保留对象的引用列表。
  • 最后,迭代整理hashCode()结果的列表。
  • 打印哈希码的总和。

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