5

我需要澄清一下次要 gc 集合的行为方式。调用a()或调用b()一个长期存在的应用程序,如果它们在旧空间变大时表现得最差

//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
    }

}

我的疑惑从何而来?我发现在使用的永久空间变大的应用程序中,较小的 gc 暂停会增加。

进行了一些测试,我发现如果我强制 jvm 使用 optiona()而另一个 jvm 使用 option b(),那么b()当旧空间变大时,带有 option 的 jvm 的暂停持续时间会更短,但我不知道为什么。

gc cpu使用时间

我在应用程序中解决了这个问题,在 4096 中使用了这个属性XX:ParGCCardsPerStrideChunk,但我想知道我上面描述的情况是否会导致 gctimes 增加导致 gccard 表中的扫描速度较慢或者我不知道或者是什么完全不相关。

4

1 回答 1

2

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

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

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

如果您确实有一个大堆(数十 GB)一个大的年轻空间,并且您有足够的 GC 线程,那么将该参数设置为更大的值可能确实有帮助,甚至可能card table您提到的有关。进一步阅读原因。

CMS将堆拆分为old spaceand young space,它本可以选择任何其他鉴别器,但他们选择了age(就像G1)。为什么需要这个?能够仅扫描和收集堆的部分区域(完全扫描它非常昂贵)。young spacestop-the-world暂停收集,所以最好是小,否则你会不开心;这也是为什么您通常会看到更多young collections的原因old ones

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

这会跟踪对引用的old space引用young space,因此它知道究竟什么是垃圾。G1也使用了 a card table,但也添加了 a RememberedSet(此处不详述)。在实践中,RememberedSets结果是巨大的,这就是为什么G1变成了代际。(仅供参考:Shenandoah使用matrix而不是card table- 使其不是世代相传)。

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

256 * 512 = 128KB per stride of old generation

例如,如果您有一堆数十万步,那是多少32 GB?恐怕太多了。

现在,你为什么还要reference counting在这里讨论?我不知道。


您展示的示例具有不同的语义,因此很难推理;不过我还是会努力的。您必须了解对象的可达性只是从某些(称为GC roots)开始的图。我们先来看这个例子:

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

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

因此,即使Example 可访问的(意味着它不能被收集),ShortLivedObject并且new Object() 可能被收集。它看起来像这样:

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

您可以看到,一旦GC将扫描Example实例,它可能根本不扫描ShortLivedObject(这就是为什么垃圾被识别为与活动对象相反的原因)。因此 GC 算法将简单地丢弃整个图而不扫描它。


第二个例子不同:

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

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

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

很明显,ShortLivedObject在这种情况下可以收集,但不能 longLived

Example如果可以收集实例,您必须了解这根本不重要;这个图不会被遍历,所有Example用到的都可以收集。

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

于 2019-11-21T21:54:44.737 回答