【问题标题】:Prevent Java 7 from premature GC防止 Java 7 过早 GC
【发布时间】:2013-03-04 02:55:34
【问题描述】:

类似于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() 调用之后添加对这些对象的调用,但这是一个相当手动的解决方案 - 每次我测试一个对象时,我都必须在最后一次 logMemory() 调用之后进行某种副作用触发调用,否则我可能会得到不一致的结果。

我正在寻找一般案例解决方案;我知道在 main() 方法的末尾添加像 System.out.println(mh.hashCode()+mh2.hashCode()) 这样的调用就足够了,但我不喜欢这个有几个原因。首先,它引入了对上述测试的外部依赖——如果删除了 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 处理?
  • 循环遍历这些对象并在 finalize 方法中调用像 .hashCode() 这样的微不足道的东西就够了吗?
  • 用这种方法计算和打印一些结果是否足够?
  • JIT 是否知道其他方法(如 System.out.println)无法优化掉,或者更好的是,有什么方法可以告诉 JIT 不要优化掉一个方法调用/代码块?

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

【问题讨论】:

  • 将对象锚定在对象实例或静态变量中。或者只是在测量后以某种方式在main 中引用您的对象引用。
  • 首先,这是我的第一个问题,够不够。第二,我在我的问题中说我想避免需要强制执行。
  • 必须在某处引用对象。没关系,只要包含这些引用的任何内容本身都被引用(以及被引用的对象等),就可以是静态或堆栈引用。 (请注意,main 中的引用可以简单地创建一个 Object 数组并将引用放在那里——无需“使用”该对象。)
  • 制作 mh 和 mh2 静态变量。还能比这更简单吗?
  • 我了解 GC 的工作原理。我的问题的目的是探索以一般和用户防错的方式防止 GC 处理我仍然感兴趣的内存使用的对象的最佳方法。将 mhmh2 设为静态会使前者失败,而强制在 main 方法中进行打印调用会使后者失败。

标签: java garbage-collection benchmarking jit


【解决方案1】:

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

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

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

【讨论】:

  • 是的,这是我目前的解决方案,但它很挑剔,我不喜欢不必要的 println。根据我问题的第 4 部分,我想知道是否还有其他操作看起来不那么 hacky。 JIT 以某种方式知道无法清理 System.out.println() 调用,我如何同样指出不同的操作具有无法跳过的副作用?
  • @dimo414 这是一个小风险,但您可以在不同的类中拥有一个方法,该方法不执行您使用哈希码总和作为参数调用的方法。风险在于 JIT 会发现它什么都不做。从历史上看,当我构建基准测试时,我曾多次被智能编译器发现,因此我唯一真正信任的能确保活性的就是实际输出。
  • 确实,我想我可以找到某种解决方法,例如深入堆栈,该堆栈适用于当前 GC,但将来会神秘地中断。那将是非常危险的,我想避免它。如果输出 really 是防止这种情况的唯一方法,这就是我要做的,但至少在我看来,如果 JIT 知道它无法优化的特殊方法,你应该能够以某种方式控制它。
  • @dimo414 我认为 JIT 不知道特殊方法。相反,它会检查方法以查看是否可以优化它们。具有外部影响的操作系统调用(例如 write 调用)将成为优化交易杀手。
  • 如果您担心可见的副作用,请记住您可以测试哈希码总和的条件并在输出中进行不显眼的更改,例如制表符与空格。我倾向于更无情 - 我输出一行解释正在输出的数字以确保活跃度。
【解决方案2】:

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

稍后调用System.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 无法确定永远不会引用 static。考虑以下情况:

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

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

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

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

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

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

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

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

不正确。 static 列表无论如何都不能被核弹;见上文。

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

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

【讨论】:

  • 1) 您禁止收集的示例都涉及通过 SOUT 阻止 JIT,我想避免这种有点骇人听闻的解决方案。 2)我更新了我的代码,以明确保存我的对象的列表是主类的静态变量,但似乎如果 JIT 足够聪明,一旦它知道主方法没有,它理论上仍然可以 GC 这些值需要他们。
  • 3) 我同样更新了我的代码,以澄清我说的是主类的 finalize 方法。在 main 方法返回之前它不应该被 GC,此时 JVM 退出,但也许(这是我的问题的一部分)即使它没有被调用,它的存在也阻止了 JIT 对我的静态列表进行攻击。 4)“使对象可达的任何东西”像什么?据我了解,JIT 知道 SOUT 有一些特别之处,这会阻止它优化此类调用。我可以用任何其他方法复制该行为吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-24
  • 1970-01-01
相关资源
最近更新 更多