我有以下简单程序:
public class GCArrays {
public static void main(String[] args) {
Object[] bigArr = new Object[1 << 24];
Object[] smallArr = new Object[1 << 12];
bigArr[0x897] = new Object();
smallArr[0x897] = new Object();
for (int i = 0; i < 1e10; i++) {
smallArr[0x897] = new Object(); // (*)
//bigArr[0x897] = new Object();
}
// to prevent bigArr and smallArr from being garbage collected
bigArr[0x897] = new Object();
smallArr[0x897] = new Object();
}
}
当我使用 ParallelGC 作为年轻代的 GC 算法运行它时:
java -classpath . -XX:InitialHeapSize=4G -XX:MaxHeapSize=4G -XX:NewRatio=3 -XX:+PrintGC -XX:+PrintGCDetails -XX:+UseParallelGC -XX:+UseParallelOldGC GCArrays
我得到的平均暂停时间低于 1 毫秒:
[GC (Allocation Failure) [PSYoungGen: 1047584K->32K(1048064K)] 1113476K->65924K(4193792K), 0.0007385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
但是,如果我将标有 (*)
的行更改为修改 bigArr
而不是 smallArr
,则暂停时间会增加到 10 毫秒:
[GC (Allocation Failure) [PSYoungGen: 1047584K->32K(1048064K)] 1113468K->65916K(4193792K), 0.0101251 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
请注意,该程序仅修改数组的单个元素。然而,看起来 JVM 仍然会扫描整个数组以在次要收集期间查找 Activity 对象。我对较长 GC 暂停的解释是否正确?为什么在这种情况下只修改一个元素就需要扫描整个数组?
最佳答案
This article解释脏卡的概念及其在年轻 GC 中的作用。
在这两种情况下,旧空间中的单个内存地址都是“脏的”,因此是单卡。对于跨越多个卡片(512 字节 block )的引用数组对象,仅修改真正修改的索引子范围的卡片。
由于只有单卡被“弄脏”,GC只需要扫描对应的512字节内存。
与 -XX:+UseConcMarkSweepGC
“smallArr”和“bigArr”版本都显示相似的时间。
-XX:+UseConcMarkSweepGC
+ smallArr
[GC (Allocation Failure) [ParNew: 419458K->2K(471872K), 0.0015320 secs] 485365K->65909K(996160K), 0.0015635 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
-XX:+UseConcMarkSweepGC
+ bigArr
[GC (Allocation Failure) [ParNew: 419458K->2K(471872K), 0.0020550 secs] 485365K->65909K(996160K), 0.0020885 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
尽管这 -XX:+UseParallelOldGC
, 似乎 GC 必须扫描整个 "bigArr"
-XX:+ParallelOldGC
+ smallArr
[GC (Allocation Failure) [PSYoungGen: 522768K->16K(523520K)] 588691K->65939K(1047808K), 0.0009430 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
-XX:+ParallelOldGC
+ bigArr
[GC (Allocation Failure) [PSYoungGen: 522768K->16K(523008K)] 588687K->65935K(1047296K), 0.0149276 secs]
[Times: user=0.03 sys=0.00, real=0.02 secs]
-XX:+ParallelOldGC
+ bigArr = new Object[1 << 25]
[GC (Allocation Failure) [PSYoungGen: 522768K->16K(523520K)] 654219K->131467K(1047808K), 0.0413473 secs]
[Times: user=0.09 sys=0.00, real=0.04 secs]
直观计数器ParallelOldGC
和 ConcMarkSweepGC
正在使用非常相似的年轻 GC 算法的不同实现。
看起来像PSYoungGen
缺少仅扫描对象数组脏部分的优化。
关于java - 为什么在更新大型数组的元素时次要 GC 持续时间会发生如此大的变化?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58187596/