短生命周期对象如果引用老生命周期对象会增加垃圾回收时间吗?

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
    }

}

我的疑惑是从哪里来的?我发现在一个应用程序中,使用的tenured空间越大,次要gc暂停时间就会增加。

进行了一些测试后,我发现如果我强制jvm使用选项a()和另一个jvm使用选项b(),那么具有选项b()的jvm在老年代空间变大时暂停时间更短,但我无法解释为什么。

gc cpu utilization time

我通过将属性XX:ParGCCardsPerStrideChunk设置为4096来解决了该问题,但我想知道我上面描述的情况是否会导致gctimes增加,因为gccard表中的扫描速度较慢或者还有其他原因。


@LouisWasserman 当旧空间变大时会发生这种情况,但也许你是正确的,这个例子并不是真正发生的事情,我正在寻找一些澄清。我注意到时间响应中99th和p99.9的百分比存在差异。 - nachokk
这里最大的问题是你正在使用一个已经被弃用的GC收集器 - CMS。你需要切换到G1(甚至更好的是_Shendandoah_)并查看那里会发生什么。第二个问题是我怀疑你是否确实知道LongLivedObject是一个真正的长期存在的对象 - 它是否被GC根引用?第三个问题是你混淆了很多术语:CMS对于_年轻_代有一个STW暂停,对于老年代有两个短暂的暂停以及许多其他你混淆的事情。 - Eugene
你在这里提出了4个不同的问题,都比较通用 - 因此你最好得到一个通用的答案。 - Eugene
你说你“知道”这是一个长生命周期的对象,这并不重要,无论你将它作为参数还是方法参数传递,可达性与此无关。 - Eugene
@Holger 我不知道,我想了解在垃圾回收方面哪个更高效,或者没有实际区别,想知道为什么一个选项比另一个更好,因为我无法弄清楚。 - nachokk
显示剩余8条评论
1个回答

2
免责声明:我绝对不是GC专家,但最近为了好玩而深入研究这些细节。
正如我在评论中所说,您正在使用一个已被弃用、没有人支持且没有人想使用的收集器,请切换到G1,甚至更好的是IMHO切换到Shenandoah:首先从这个简单的事情开始。
我只能假设您将ParGCCardsPerStrideChunk从其默认值增加,并且可能帮助了几毫秒(虽然我们没有证据)。我们也没有GC、CPU活动、日志等记录;因此这很复杂,无法回答。
如果确实有一个大堆(数十GB)和一个大的年轻空间,并且您有足够的GC线程,则将该参数设置为更大的值可能确实有所帮助,甚至可能与您提到的card table有关。请继续阅读原因。
CMS将堆分为旧空间和年轻空间,它可以选择任何其他区分符,但他们选择了年龄(就像G1一样)。为什么需要这样做?为了能够扫描和仅收集堆的部分区域(完全扫描非常昂贵)。年轻空间使用stop-the-world暂停来进行收集,因此最好保持较小,否则您将不会感到满意;这也是为什么通常会看到比旧集合更多的年轻集合的原因。
当您扫描年轻空间时唯一的问题是:如果从旧空间引用到年轻空间的对象有引用会发生什么?收集这些显然是错误的,但扫描整个旧空间以找出答案将完全破坏分代收集的目的。因此:card table。
这跟踪从旧空间到年轻空间引用的引用,因此它知道什么是垃圾或不是垃圾。G1也使用card table,但还添加了一个RememberedSet(这里不详细说明)。实际上,RememberedSets被证明是巨大的,这就是为什么G1变成了分代的原因。(FYI:Shenandoah使用矩阵而不是卡片表,使其不是分代的。)
所以这个长长的介绍是为了说明,增加ParGCCardsPerStrideChunk可能会有所帮助。这样可以给每个GC线程更多的工作空间。默认值为256,卡表为512字节,这意味着...
256 * 512 = 128KB per stride of old generation

如果你有一个堆大小为32 GB,那么这是多少个数十万的步幅?可能太多了。
现在,为什么你要在这里讨论引用计数呢?我不知道。
你展示的例子具有不同的语义,因此很难进行推理;但我仍然会尝试。你必须理解对象的可达性只是从一些根(称为GC roots)开始的图表。让我们先看看这个例子:
public void b(){
   new ShortLivedObject().doSomething(new Object()); // actually now is shortlived
}
ShortLivedObject实例在doSomething方法调用完成后被"遗忘",其作用域仅限于该方法内部,没有其他途径可以访问到它。因此,剩下的部分涉及到doSomething方法的参数:new Object。如果doSomething不对它进行任何可疑的操作(使其通过GC根图成为可达对象),那么在doSomething完成之后,它也将变得可以被垃圾回收。但是,即使doSomething使new Object可达,这仍意味着ShortLivedObject实例也可以被垃圾回收。
因此,即使Example可达(表示它无法被回收),ShortLivedObjectnew 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 使用的所有内容。
现在您应该能够理解,使用方法 a 可以保留更多垃圾并潜在地将其移动到 old space(当它们变得足够旧时),并且可以使您的 young pauses 变得更长,而且增加 ParGCCardsPerStrideChunk 可能会有所帮助;但这是高度推测的,需要发生一种糟糕的分配模式才能实现所有这些。没有日志,我非常怀疑。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接