我需要一些关于次要 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 使用选项
a()
和另一个 jvm 使用选项 b()
,然后是带有选项 b()
的 jvm当旧空间变大时,暂停持续时间更短,但我不知道为什么。我在应用程序中解决了这个问题,使用这个属性 XX:ParGCcardsPerStrideChunk 在 4096 中,但我想知道我上面描述的情况是否会导致 gctime 增加,导致 gccard 表中的扫描速度变慢,或者我不知道或根本不相关的东西。
最佳答案
免责声明:我目前还不是 GC 专家,但最近为了好玩而深入了解这些细节。
正如我在评论中所说,您正在使用不推荐使用的收集器,没有人支持它,也没有人想使用它,切换到 G1
或者更好的恕我直言切换到 Shenandoah
: 首先从这个简单的事情开始。
我只能假设你增加了ParGCCardsPerStrideChunk
从它的默认值开始,这可能有助于一些 ms
(虽然我们没有证据证明这一点)。我们也没有来自 GC、CPU Activity 、日志等的日志;因此,回答这个问题非常复杂。
如果确实有一个很大的堆(几十 GB)和一个很大的年轻空间,并且有足够的 GC 线程,那么将该参数设置为更大的值可能确实有帮助,甚至可能与 card table
有关。你提到的。进一步阅读原因。CMS
将堆拆分为 old space
和 young space
,它可以选择任何其他鉴别器,但他们选择了 age
(就像 G1
)。为什么需要这样?能够仅扫描和收集堆的部分区域(完全扫描它非常昂贵)。 young space
用 stop-the-world
收集暂停,所以最好小一点,否则你不会快乐;这也是为什么您通常会看到更多 young collections
比较 old ones
.
扫描时唯一的问题young space
是:如果有来自 old space
的引用会发生什么到来自 young space
的对象?收集那些显然是错误的,但扫描整个old space
找出答案会违背 generational collections
的目的完全。因此:card table
.
这会跟踪来自 old space
的引用至 young space
引用,所以它知道到底什么是垃圾。 G1
使用 card table
也是,而且还增加了一个 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
根本没有(这就是为什么垃圾被识别为 Activity 对象的对立面)。因此,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
可能会有所帮助;但是这个高度投机并且您需要一个非常糟糕的分配模式才能发生所有这些。没有日志,我非常怀疑。
关于java - 可以增加引用旧对象的 gc 时间短命对象吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58924737/