可以增加 gc 时间短生命对象引用旧生命对象吗?

Could increase gc time short lived object that has references to old lived object?

我需要一些关于次要 gc 集合行为的说明。在长期应用程序中调用 a() 或调用 b(),如果它们在旧的 space 变大时表现最差

//an example instance lives all application life cycle 24x7
public class Example {

    private Object longLived = new Object(); 

    public void a(){
        var shortLived = new ShortLivedObject(longLived); // longLived now is attribute
        shortLived.doSomething();
    }


    public void b(){
       new ShortLivedObject().doSomething(new Object()); // actually now is shortlived
    }

}

我的疑惑从何而来?我发现在使用的 tenured space 变大的应用程序中,轻微的 gc 暂停增加。

做一些测试我发现如果我强制 jvm 使用选项 a() 和另一个 jvm 使用选项 b(),那么带有选项 b() 的 jvm 有更短的停顿旧 space 变大的持续时间,但我不明白为什么。

我在应用程序中解决了这个问题,使用这个 属性 XX:ParGCCardsPerStrideChunk in 4096,但我想知道我上面描述的情况是否会导致增加 gctimes 会导致 gccard 表中的扫描变慢或者我不知道或根本不相关的东西。

免责声明:到目前为止,我不是 GC 专家,但最近为了好玩而了解这些细节。

正如我在评论中所说,您正在使用一个已弃用的收集器,没有人支持它,也没有人想使用它,切换到 G1 或者更好的恕我直言切换到 Shenandoah :先从这个简单的事情做起。

我只能假设增加了ParGCCardsPerStrideChunk的默认值,这可能有一些帮助ms(尽管我们没有证据证明这一点)。我们也没有来自 GC、CPU activity、日志等的日志;因此这很难回答。

如果你确实有一个大堆(几十GB)and一个大年轻人spaceand你有足够的GC 线程,将该参数设置为更大的值 可能 确实有帮助 它可能 甚至与您提到的 card table 有关.进一步阅读原因。

CMS 将堆拆分为 old spaceyoung space,它本可以选择任何其他鉴别器,但他们选择了 age(就像 G1 ).为什么需要这个?能够仅扫描和收集堆的部分区域(扫描整个区域非常昂贵)。 young space 是用stop-the-world 暂停收集的,所以最好小一点,否则你会不高兴;这就是为什么你通常会看到 young collectionsold ones.

更多的原因

扫描 young space 时的唯一问题是:如果 old space 引用了 young space 的对象,会发生什么情况?收集这些显然是错误的,但扫描整个 old space 以找出答案将完全破坏 generational collections 的目的。因此:card table.

这会跟踪从 old spaceyoung space 的引用,因此它知道究竟什么是垃圾。 G1也用了一个card table,还加了一个RememberedSet(这里不赘述)。在实践中,RememberedSets 被证明是巨大的,这就是 G1 成为世代的原因。 (仅供参考:Shenandoah 使用 matrix 而不是 card table - 使其成为 而非 世代)。

所以这个巨大的介绍,是为了表明确实增加 ParGCCardsPerStrideChunk 可能有所帮助。您正在为每个 GC 线程提供更多 space 的工作。默认值是256,卡table是512 bytes,也就是说

256 * 512 = 128KB per stride of old generation

例如,如果你有一堆32 GB,那是多少数十万的步幅?可能太多了。

现在,为什么您也将 reference counting 带入这里的讨论?我不知道。


您展示的示例具有不同的语义,因此有点难以推理;不过,我仍然会尝试。您必须了解对象的 reachability 只是一个从某些 roots(称为 GC roots)开始的图形。先举这个例子:

public void b(){
   new ShortLivedObject().doSomething(new Object()); // actually now is shortlived
}

ShortLivedObject 实例是 "forgotten" 一旦 doSomething 方法调用完成并且其 scope 仅在方法内,因此没有人能达到它。因此,剩下的部分是关于 doSomething 的参数:new Object。如果 doSomething 没有对它获得的参数做任何可疑的事情(使其可以通过 GC root 图表访问),那么在 doSomething 完成后,它也将有资格进行 GC。但即使 doSomething 使 new Object 可达,它仍然意味着 ShortLivedObject 实例符合 GC 条件。

因此,即使 Example 可达(意味着无法收集),ShortLivedObjectnew Object() 可以 可能被收集。它可能看起来像这样:

                 new Object()
                      |
                     \ /
               ShortLivedObject           
                      |
                     \ /
GC Root -> ... - > Example

你可以看到,一旦 GC 将扫描 Example 个实例,它可能 不会 扫描 ShortLivedObject (这就是为什么 garbage 被识别为 live 对象的对立面)。因此 GC 算法将简单地丢弃整个图而不扫描它。


第二个例子不同:

public void a(){
    var shortLived = new ShortLivedObject(longLived);
    shortLived.doSomething();
}

不同之处在于 longLived 这里是一个 实例 字段,因此,图表看起来会有点不同:

                ShortLivedObject
                      |
                     \ /
                  longLived         
                     / \
                      |
GC Root -> ... - > Example

显然这种情况下ShortLivedObject是可以采集的,但是不是longLived

什么你要明白这根本不重要,如果Example实例可以收集;该图不会被遍历,Example 使用的所有内容都可以收集。

您现在应该能够理解,使用方法 a 可以 保留更多的垃圾,并有可能将其移动到 old space (当他们足够大)并且可以可能使您的young pauses更长确实增加ParGCCardsPerStrideChunk可能 有点帮助;但是这个 是高度推测性的 并且你需要一个非常糟糕的相同分配模式才能发生所有这一切。没有日志,我非常对此表示怀疑。