Вступление
Я должен признать, что я был в шоке. Действительно, довольно потрясенный, когда я понял, что этот пост календаря появления будет о сборке мусора. Тема GC вызвала такую страсть среди сторонников Java и тех, кто считает, что управление памятью должно быть ручным. Было написано много статей о крошечных незначительных изменениях в странно выглядящих аргументах командной строки, которые влияют на производительность в процентах на приложения Java. Как я могу добавить к этому огромному кузову?
Я надеюсь, что этот пост не добавит в GC горячего воздуха, но вместо этого будет глотком свежего воздуха. Давайте не будем смотреть на процессорное время, потребляемое сборщиком мусора или время паузы; как насчет рассмотрения скрытого, но потенциально критического аспекта управления памятью в целом и сбора мусора в частности: кэширование данных является одной из основных задач современного проектирования компьютерного программного обеспечения (другими являются кэширование инструкций и многоядерное распределение работы).
Современные процессоры работают так быстро, что основная память не надеется их поддерживать. Способ, которым некоторые из этих катастрофических потерь производительности могут быть возвращены обратно, — это кэширование. Память загружается параллельно в высокоскоростную кэш-память, а затем процессор обращается к этому кешу. Если нам повезет, и код заставляет ЦП несколько раз считывать и записывать одну и ту же память (например, внутри цикла), ЦП может с радостью получить доступ к кешу и избавиться от ожидания загрузки и сохранения в и из основной памяти ,
«Как сбор мусора влияет на производительность кэша ?» Есть много способов, некоторые из них очень тонкие, но вот несколько важных вещей:
Сборщик мусора перебирает ссылки в памяти. Это приводит к тому, что строки кэша (блоки памяти в кэше) содержат память, окружающую ссылку, и, следовательно, больше не содержат другие данные, которые использует программа.
Хотя мы называем это сборщиком мусора, на самом деле это распределитель, движитель и сборщик. Это действительно важно, когда мы думаем о кэшировании данных:
- Распределение: на основе аппаратных правил адреса памяти сопоставляются со строками кэша. Если части памяти разделяют строку кэша, но на самом деле к ним обращаются из разных потоков, мы получаем эффект, называемый ложным разделением. Однако, если небольшие биты данных распределяются, но получают доступ из одного потока, мы получаем плохое использование кэша.
- Перемещение: объекты не остаются в одном месте на протяжении всей их жизни. Сборщик мусора избегает фрагментации памяти, перемещая объекты вокруг. Это имеет интересный эффект гарантии того, что строки кэша, связанные с объектом, больше не будут связаны с ним после перемещения.
- Коллекционирование. Самое смешное в том, что коллекционирование — это легко. Это может быть так просто, как просто пометить память как доступную для повторного использования. Обход графов объектов (множественные корни) — это поиск того, что может быть собрано, что приведет к загрузке строк кэша данных и, таким образом, к удалению строк из кэша, которые считывались или записывались пользовательским кодом.
Итак, теперь мы можем видеть, что конструкция сборщика мусора имеет решающее значение для работы кэша данных. Замена используемого нами коллектора не только повлияет на паузы GC и другие очевидные проблемы, но также на низком уровне и фундаментально повлияет на весь пользовательский код.
Пример
Я не собираюсь представлять исчерпывающую научную статью по этой концепции. Цель этого поста — показать альтернативный подход к настройке JVM. Итак, я запустил простое, короткое, многопоточное исправление в моей личной программе синтезатора Sonic-Field. Патч использует обратную связь, резонанс и кучу других концепций для синтеза струнных инструментов, а затем свертки для размещения звуков в акустической среде.
Причина выбора звукового поля не в том, что он имеет разумную сложность, с высокой степенью многопоточности и использует Spring, а в том, что я недавно обнаружил, что могу получить более высокую производительность от него, используя сборщик мусора CMS. Задержка с Sonic-Field не представляет интереса, потому что это пакетный процессор. Однако стандартный сборщик мусора в Java 7 плохо взаимодействовал с тем, как Sonic Field записывает файлы подкачки на диск при нехватке памяти. Я попробовал CMS, потому что он постоянно держит память (теоретически — не плачь по мне), потому что он постоянно пытается делать небольшие сборки мусора вдоль пользовательских потоков.
Если мы соберем все это вместе, мы вполне можем придумать разумную теорию
«Сборщик мусора CMS может дать меньше пауз и может сократить использование памяти, но при этом он почти наверняка вызовет больше ошибок кеширования данных» . Постоянный обход контрольного графа в памяти, чтобы попытаться собрать мертвые объекты, приведет к загрузке кэша, и эти загрузки приведут к сбросу других данных из кэша (он имеет конечный размер). Таким образом, когда пользовательские потоки приходят к чтению снова, они будут вызывать больше пропусков кэша и так далее.
Это имеет значение? Этот ответ будет полностью зависеть от приложения, оборудования и нагрузки на приложение. Я не повторяю, я не защищаю одного сборщика мусора над другим! Тем не менее, на этот вопрос я бы хотел ответить, поэтому давайте ответим на него для моего небольшого тестового патча.
Эти эффекты кэширования данных сборщика мусора не видны из обычных инструментов профилирования виртуальной машины. Это означает, что их мало обсуждают в сообществе JVM, а в настройке JVM их рассматривают еще меньше. Тем не менее, есть инструмент (на самом деле несколько — но я собираюсь поговорить о самом простом в использовании), который может пролить свет на эту тему. Я говорю о Intel PCM (Performance Counter Monitor). Его также можно использовать для настройки кода, но я подумал, что говорить о GC будет веселее сегодня.
Рабочий пример
PCM это просто инструмент командной строки. Мы передаем командную строку для запуска Java в кавычках, и он выполняет свои измерения. С помощью других инструментов счетчики производительности можно использовать для получения всевозможных других подробностей о приложении. Преимущество инструмента командной строки pcm заключается в его простоте и отсутствии вмешательства в общий запуск приложения. Недостатком является то, что он будет измерять JVM и фазы прогрева приложения. Однако для приложений в стиле сервера или пакетных процессоров (например, Sonic Field) эти издержки обычно незначительны по сравнению с фактическим запуском приложения.
Я запустил свой патч на своем персональном Macbook Pro Retina (2012) с 16 Гб оперативной памяти. JVM была:
1
2
3
4
5
|
java version "1.8.0-ea" Java(TM) SE Runtime Environment (build 1.8 . 0 -ea-b61) Java HotSpot(TM) 64 -Bit Server VM (build 25.0 -b05, mixed mode) |
Показания из pcm просто записываются в стандартный при выходе из приложения. Я сравнил прогоны без настроек для сборщика мусора (поэтому по умолчанию) и с моим текущим набором настроек. Если честно, я не уверен, что твики оптимальны; Я как бы поднял их из кучки онлайн статей …
Вот скрипт запуска для Java:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/Users/alexanderturner/x/IntelPerformanceCounterMonitorV2. 5.1 \ 2 /pcm.x "java \ -Xmx12G -Xms12G -DsonicFieldTemp=/Users/alexanderturner/temp -DsonicFieldThreads= 12 -DsonicFieldSwapLimit= 4.0 -XX:+UseConcMarkSweepGC -XX:+UseCompressedOops -XX:ParallelGCThreads= 8 -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction= 60 -XX:+UseCMSInitiatingOccupancyOnly \ -classpath \ bin:\ ing-framework- 3.1 . 2 .RELEASE/dist/org.springframework.asm- 3.1 . 2 .RELEASE.jar:\ spring/sp spring/sp rring-framework- 3.1 . 2 .RELEASE/dist/org.springframework.beans- 3.1 . 2 .RELEASE.jar:\ ing-framework- 3.1 . 2 .RELEASE/dist/org.springframework.core- 3.1 . 2 .RELEASE.jar:\ spring/sprin spring/spring-framework- 3.1 . 2 .RELEASE/dist/org.springframework.context- 3.1 . 2 .RELEASE.jar:\ spring/spring-framework- 3.1 . 2 .RELEASE/dist/org.springframework.context-support- 3.1 . 2 .RELEASE.jar:\ spring/sp rg-framework- 3.1 . 2 .RELEASE/dist/org.springframework.expression- 3.1 . 2 .RELEASE.jar:\ spring/spring-framework- 3.1 . 2 .RELEASE/dist/org.springframework.test- 3.1 . 2 .RELEASE.jar:\ spring/otherJars/commons-logging- 1.1 . 1 .jar \ com.nerdscentral.sfpl.RenderRunner $ 1 " |
Надеюсь, понятно, насколько просто работает Java под IntelPerformanceCounterMonitorV2. Итак, вот результат:
Стандартный ГК
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
Core (SKT) | EXEC | IPC | FREQ | AFREQ | L3MISS | L2MISS | L3HIT | L2HIT | L3CLK | L2CLK | READ | WRITE | TEMP 0 0 0.53 0.75 0.70 1.31 422 M 621 M 0.32 0.32 0.14 0.01 N/A N/A 32 2 0 0.56 0.77 0.73 1.31 346 M 466 M 0.26 0.31 0.11 0.01 N/A N/A 28 1 0 0.22 0.69 0.32 1.31 144 M 192 M 0.25 0.28 0.11 0.01 N/A N/A 32 3 0 0.21 0.68 0.31 1.31 135 M 171 M 0.21 0.28 0.10 0.01 N/A N/A 28 4 0 0.55 0.77 0.71 1.31 332 M 410 M 0.19 0.38 0.11 0.01 N/A N/A 22 7 0 0.18 0.68 0.26 1.30 124 M 134 M 0.08 0.30 0.11 0.00 N/A N/A 27 5 0 0.19 0.68 0.29 1.31 133 M 155 M 0.14 0.30 0.11 0.00 N/A N/A 22 6 0 0.61 0.79 0.78 1.32 343 M 382 M 0.10 0.35 0.10 0.00 N/A N/A 27 ------------------------------------------------------------------------------------------------------------------- SKT 0 0.38 0.75 0.51 1.31 1982 M 2533 M 0.22 0.33 0.11 0.01 N/A N/A 22 Instructions retired: 2366 G ; Active cycles: 3166 G ; Time (TSC): 773 Gticks ; C0 (active,non-halted) core resi ------------------------------------------------------------------------------------------------------------------- TOTAL * 0.38 0.75 0.51 1.31 1982 M 2533 M 0.22 0.33 0.11 0.01 N/A N/A N/A dency: 39.04 % C : 1.49 => corresponds to 37.36 % utilization for cores in active state Instructions per no C1 core residency: 23.92 %; C3 core residency: 0.01 %; C6 core residency: 0.00 %; C7 core residency: 37.02 % C2 package residency: 0.00 %; C3 package residency: 0.00 %; C6 package residency: 0.00 %; C7 package residency: 0.00 % PHYSICAL CORE I Pminal CPU cycle: 0.76 => corresponds to 19.12 % core utilization over time interval |
Одновременная метка развертки
Скорее, чем
01
02
03
04
05
06
07
08
09
10
11
12
13
|
-XX:+UseConcMarkSweepGC -XX:+UseCompressedOops -XX:+CMSParallelRemark -XX:ParallelGCThreads= 8Enabled -XX:CMSInitiatingOccupancyFraction= 60 -XX:+UseCMSInitiatingOccupancyOnly |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
Core (SKT) | EXEC | IPC | FREQ | AFREQ | L3MISS | L2MISS | L3HIT | L2HIT | L3CLK | L2CLK | READ | WRITE | TEMP 0 0 0.53 0.69 0.76 1.31 511 M 781 M 0.35 0.35 0.17 0.02 N/A N/A 26 2 0 0.54 0.71 0.75 1.31 418 M 586 M 0.29 0.40 0.14 0.01 N/A N/A 29 1 0 0.31 0.66 0.47 1.30 207 M 285 M 0.27 0.26 0.11 0.01 N/A N/A 26 3 0 0.30 0.66 0.46 1.30 198 M 258 M 0.23 0.27 0.11 0.01 N/A N/A 29 4 0 0.59 0.73 0.81 1.31 397 M 504 M 0.21 0.46 0.12 0.01 N/A N/A 29 7 0 0.30 0.66 0.45 1.30 186 M 204 M 0.09 0.29 0.11 0.00 N/A N/A 30 5 0 0.30 0.66 0.45 1.30 188 M 225 M 0.16 0.28 0.11 0.01 N/A N/A 29 6 0 0.58 0.73 0.79 1.31 414 M 466 M 0.11 0.49 0.13 0.00 N/A N/A 30 ------------------------------------------------------------------------------------------------------------------- SKT 0 0.43 0.70 0.62 1.31 2523 M 3313 M 0.24 0.38 0.13 0.01 N/A N/A 25 Instructions retired: 2438 G ; Active cycles: 3501 G ; Time (TSC): 708 Gticks ; C0 (active,non-halted) core resi ------------------------------------------------------------------------------------------------------------------- TOTAL * 0.43 0.70 0.62 1.31 2523 M 3313 M 0.24 0.38 0.13 0.01 N/A N/A N/A dency: 47.22 % C : 1.39 => corresponds to 34.83 % utilization for cores in active state Instructions per no C1 core residency: 17.84 %; C3 core residency: 0.01 %; C6 core residency: 0.01 %; C7 core residency: 34.92 % C2 package residency: 0.00 %; C3 package residency: 0.00 %; C6 package residency: 0.00 %; C7 package residency: 0.00 % PHYSICAL CORE I Pminal CPU cycle: 0.86 => corresponds to 21.51 % core utilization over time interval |
Вся информация, представленная здесь, представляет интерес, однако, ее так много, что я считаю, что лучшее, что можно сделать, это вырезать кейс и проверить мое утверждение о сборщике CMS. Для этого мы можем рассмотреть только две строки, формирующие вывод для каждого прогона:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
Default: SKT 0 0.38 0.75 0.51 1.31 1982 M 2533 M 0.22 0.33 0.11 0.01 N/A N/A 22 Instructions retired: 2366 G ; Active cycles: 3166 G ; Time (TSC): 773 Gticks ; C0 (active,non-halted) core residency: 39.04 % CMS: 0 0.43 0.70 0.62 1.31 2523 M 3313 M 0.24 0.38 0.13 0.01 N/A N/A 25 SKT Instructions retired: 2438 G ; Active cycles: 3501 G ; Time (TSC): 708 Gticks ; C0 (active,non-halted) core residency: 47.22 % |
обсуждение
Мы видим, что под сборщиком CMS было значительно больше промахов в кеше. Промахи L2 были на 30% больше, а L2 выросли на 27% по сравнению с коллектором по умолчанию. Тем не менее, общее время, затрачиваемое на гига тики (708CMS / 773Default), показывает, что все эти пропущенные лишние данные не оказали негативного влияния на всю производительность. Я предполагаю, что это означает, что гораздо больше исследований можно и нужно сделать, прежде чем делать какие-либо выводы относительно правильного подхода для этого приложения!
Если вы оставите этот пост, думая, что я не полностью обсудил тему, вы правы. Мое намерение состояло в том, чтобы заинтересовать читателя в размышлениях об этом аспекте производительности Java и открыть дверь новому подходу.