Почему незначительная продолжительность GC изменяется так сильно, когда обновляется элемент большого массива? - PullRequest
3 голосов
/ 01 октября 2019

У меня есть следующая простая программа:

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 все еще сканирует весь массив, чтобы найти живые объекты во время незначительного сбора. Правильно ли мое предположение, объясняющее более длинные паузы GC? Почему весь массив необходимо сканировать, если в этом случае изменен только один элемент?

1 Ответ

3 голосов
/ 01 октября 2019

Эта статья объясняет концепцию грязных карт и их роль в молодом GC.

В обоих случаях один адрес памяти в старом пространстве «загрязнен» и, таким образом, представляет собой одну карту. Для объекта эталонного массива, охватывающего несколько карт (блоки по 512 байт), модифицируется только карта для действительно измененного поддиапазона индекса.

Так как только одна карта «перезагружена», 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 отсутствует оптимизация для сканирования только грязной части массива объектов.

...