Мне было любопытно измерить время, потраченное на выделение памяти в JDK 13 с использованием G1 и Epsilon. Результаты, которые я наблюдал, являются неожиданными, и мне интересно понять, что происходит. В конечном счете, я хочу понять, как сделать использование Epsilon более производительным, чем G1 (или, если это невозможно, почему).
Я написал небольшой тест, который неоднократно выделяет память. В зависимости от ввода в командной строке он будет либо:
- создать 1024 новых массива 1 МБ или
- создайте 1024 новых массива размером 1 МБ, измерьте время, выделенное для выделения, и распечатайте прошедшее время для каждого выделения. Это измеряет не только само распределение, но включает время, потраченное на все остальное, что происходит между двумя вызовами к
System.nanoTime()
- тем не менее, это, кажется, полезный сигнал для прослушивания.
Вот код:
public static void main(String[] args) {
if (args[0].equals("repeatedAllocations")) {
repeatedAllocations();
} else if (args[0].equals("repeatedAllocationsWithTimingAndOutput")) {
repeatedAllocationsWithTimingAndOutput();
}
}
private static void repeatedAllocations() {
for (int i = 0; i < 1024; i++) {
byte[] array = new byte[1048576]; // allocate new 1MB array
}
}
private static void repeatedAllocationsWithTimingAndOutput() {
for (int i = 0; i < 1024; i++) {
long start = System.nanoTime();
byte[] array = new byte[1048576]; // allocate new 1MB array
long end = System.nanoTime();
System.out.println((end - start));
}
}
Вот информация о версии JDK, которую я использую:
$ java -version
openjdk version "13-ea" 2019-09-17
OpenJDK Runtime Environment (build 13-ea+22)
OpenJDK 64-Bit Server VM (build 13-ea+22, mixed mode, sharing)
Вот несколько способов запуска программы:
- распределение только с использованием G1:
$ time java -XX:+UseG1GC Scratch repeatedAllocations
- только выделение, Эпсилон:
$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
- распределение + синхронизация + вывод с использованием G1:
$ time java -XX:+UseG1GC Scratch repeatedAllocationsWithTimingAndOutput
- выделение + синхронизация + выход, эпсилон:
time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocationsWithTimingAndOutput
Вот некоторые моменты запуска G1 только с выделениями:
$ time java -XX:+UseG1GC Scratch repeatedAllocations
real 0m0.280s
user 0m0.404s
sys 0m0.081s
$ time java -XX:+UseG1GC Scratch repeatedAllocations
real 0m0.293s
user 0m0.415s
sys 0m0.080s
$ time java -XX:+UseG1GC Scratch repeatedAllocations
real 0m0.295s
user 0m0.422s
sys 0m0.080s
$ time java -XX:+UseG1GC Scratch repeatedAllocations
real 0m0.296s
user 0m0.422s
sys 0m0.079s
Вот некоторые моменты запуска Epsilon только с выделенными ресурсами:
$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real 0m0.665s
user 0m0.314s
sys 0m0.373s
$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real 0m0.652s
user 0m0.313s
sys 0m0.354s
$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real 0m0.659s
user 0m0.314s
sys 0m0.362s
$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real 0m0.665s
user 0m0.320s
sys 0m0.367s
С или без синхронизации + выход, G1 быстрее, чем Epsilon. В качестве дополнительного измерения, используя временные числа из repeatedAllocationsWithTimingAndOutput
, среднее время выделения больше при использовании Epsilon. В частности, один из локальных прогонов показал, что G1GC в среднем составляла 227 218 нанограмм на выделение, тогда как Epsilon составляла в среднем 521 217 нанограмм (я записал выходные числа, вставил их в электронную таблицу и использовал функцию average
для каждого набора чисел).
Я ожидал, что тесты Epsilon будут заметно быстрее, однако на практике я вижу примерно в 2 раза медленнее. Максимальное время выделения с G1 определенно выше, но только с перерывами - большинство распределений G1 значительно медленнее, чем у Epsilon, почти на порядок медленнее.
Вот график 1024 раза от запуска repeatedAllocationsWithTimingAndOutput()
с G1 и Epsilon. Темно-зеленый - для G1; светло-зеленый для Эпсилон; Ось Y - "нанос на распределение"; Меньшие линии сетки по оси Y каждые 250000 нанос. Это показывает, что время выделения Epsilon очень стабильно, каждый раз около 300-400 тыс. Нанос. Это также показывает, что время G1 значительно быстрее в большинстве случаев, но также периодически - в 10 раз медленнее, чем у Epsilon. Я предполагаю, что это может быть связано с работой сборщика мусора, что было бы нормально и нормально, но также, похоже, сводит на нет идею, что G1 достаточно умен, чтобы знать, что ему не нужно выделять какую-либо новую память.