java - 为什么在更新大型数组的元素时次要 GC 持续时间会发生如此大的变化?

标签 java performance garbage-collection

我有以下简单程序:

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] 

直观计数器ParallelOldGCConcMarkSweepGC正在使用非常相似的年轻 GC 算法的不同实现。

看起来像PSYoungGen缺少仅扫描对象数组脏部分的优化。

关于java - 为什么在更新大型数组的元素时次要 GC 持续时间会发生如此大的变化?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58187596/

相关文章:

java - 房间 - LiveData 未触发

java - 如何使用 Java ScriptEngine 创建 Javascript 函数

c - 将数据添加到闪存中的一个段怎么会搞砸程序的计时?

java - 使用弱(或软等)引用作为树中的父引用是否值得?

cocoa - Cocoa 垃圾收集导致内存泄漏

java - 如何为Maven设置特定的Java版本?

java - 为什么 QueryResultIterator#getCursor() 在 GAE/J 中返回 null

performance - awk 超慢处理多行但不多列

python - 只保留列表中唯一项的最有效方法?

java - Java 中的零垃圾大字符串反序列化,Humongous 对象问题