В Java
у разработчика есть доступ только к созданию объекта, мы не можем явно
его удалить. Для этого в JVM есть такой механизм как GC, который и подчищает
нашу память от неиспользуемых объектов.
Stack
память в JavaСтековая память в Java работает по схеме LIFO (Последний-зашел-Первый-вышел). Всякий раз, когда вызывается метод, в памяти стека создается новый блок, который содержит примитивы и ссылки на другие объекты в методе. Как только метод заканчивает работу, блок также перестает использоваться, тем самым предоставляя доступ для следующего метода.
Heap
память в JavaЭта область памяти используется для объектов и классов. Новые объекты всегда создаются в куче, а ссылки на них хранятся в стеке.
Эти объекты имеют глобальный доступ и могут быть получены из любого места программы
JMM - это синтетическое представление физической памяти
Так как память в JVM освобождает GC, то аллокатору нужно лишь знать, где эту свободную память искать, фактически управлять доступом к одному указателю на эту самую свободную память. bump-the-pointer: каждому потоку выделяется большой кусок памяти, который принадлежит только ему.
Аллокации внутри такого буфера происходят всё тем же инкрементом указателя (но уже локальным, без синхронизации) пока это возможно, а новая область запрашивается каждый раз, когда текущая заканчивается. Такая область и называется thread-local allocation buffer(TLAB).
Утечка памяти — это ситуация, когда в куче есть объекты, которые больше не используются, но сборщик мусора не может удалить их, что приводит к нерациональному расходованию памяти.
Проблема:
Утечка блокирует ресурсы памяти, что со
временем приводит к ухудшению
производительности системы. И если ее не
устранить, приложение исчерпает свои ресурсы и
завершиться с ошибкой
java.lang.OutOfMemoryError
В Java время жизни статических полей обычно совпадает со временем работы приложения
public class StaticTest {
public static List<Double> list
= new ArrayList<>();
public void populateList() {
for (int i=0; i<10_000_000; i++){
list.add(Math.random());
}
}
public static main(String[] args) {
new StaticTest().populateList();
}
}
Однако, если мы отбросим слово static в строке номер 2, то это приведет к резкому изменению использования памяти:
public class StaticTest {
public List<Double> list
= new ArrayList<>();
public void populateList() {
for (int i=0; i<10_000_000; i++){
list.add(Math.random());
}
}
public static main(String[] args) {
new StaticTest().populateList();
Log.info("Free");
}
}
finally
блок для закрытия ресурсовfinally
), который закрывает ресурсы, не должен иметь
никаких необработанных исключений
При написании новых классов очень распространенной ошибкой является
некорректное написание переопределяемых методов equals()
и hashCode()
HashSet
и HashMap
используют эти методы во многих операциях и если они не
переопределены правильно, то эти методы могут стать источником потенциальных
проблем, связанных с утечкой памяти
Поскольку Map не позволяет использовать дубликаты ключей, многочисленные объекты Person, которые мы добавили, не должны увеличить занимаемую ими пространство в памяти.
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
}
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
Map<Person, Integer> map = new HashMap<>();
for (int i = 0; i < 100; i++) {
map.put(new Person("jon"), 1);
}
Assert.assertFalse(map.size() == 1);
}
Поскольку мы не определили правильные метод equals()
, дублирующие
объекты накопились и заняли память. В этом случае потребление памяти кучи
выглядит следующим образом:
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Person)) {
return false;
}
Person person = (Person) o;
return person.name.equals(name);
}
@Override
public int hachCode() {
int result = 17;
result = 31 * result + name.hachCode();
return result;
}
}
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
Map<Person, Integer> map = new HashMap<>();
for (int i = 0; i < 100; i++) {
map.put(new Person("jon"), 1)
}
Assert.assertTrue(map.size() == 1);
}
equals()
и hashCode()
Как это предотвратить? Если внутреннему классу не нужен доступ к членам внешнего класса, подумайте о превращении его в статический класс
ThreadLocal
— это механизм, который позволяет изолировать состояние
(значения переменных) в определенном потоке, что делает его безопасным.ThreadLocal
переменных, когда они больше не
используются. ThreadLocal
предоставляет метод remove()
, который удаляет значение
переменной для текущего потокаThreadLocal.set(null)
для очистки значения — на самом деле оно не
очищает значение, а вместо этого ищет мапу, связанную с текущим потоком, и
устанавливает пару ключ-значение — текущий поток и null соответственноThreadLocal
как ресурс, который необходимо закрыть в блоке
finally
, чтобы убедиться, что он всегда будет закрыт, даже в случае исключения:Максимальная оптимизация производится по 2 вершинам в ущерб третей 3-й.
Например :
Уменьшая потребляемые ресурсы и
увеличивая пропускную способность нам
необходимо жертвовать временем
максимальной задержки, которая в нашем
случае вырастет
Когда приложение остановлено, всё тривиально! Никто не мешается под ногами.
Нашли все корни, покрасили их в чёрный, т.к. они по определению достижимы
Ссылки из чёрных теперь серые, сканируем ссылки из серых
Сканирование из серых завершено, красим их в чёрные; новые ссылки – серые
Серые → чёрные; достижимые из серых → серые
Серые → чёрные; достижимые из серых → серые
Серые → чёрные; достижимые из серых → серые
Серые → чёрные; достижимые из серых → серые
Конец: всё достижимое – чёрное; весь мусор – белый
В concurrent mark всё сложнее: там есть приложение, которое меняет граф объектов. За это его презрительно называют мутатором.
Добрался указатель сюда, и только он начал сканировать ссылки...
Мутатор снёс ссылку из серого ... и вставил её в чёрный!
Или даже когда-нибудь потом вставил ссылку на транзитивно достижимый белый объект
Или даже когда-нибудь потом вставил ссылку на транзитивно достижимый белый объект
Марк завершился, и опаньки: есть достижимые белые объекты, которые мы сейчас снесём!
Ещё хуже: появился новый объект и ссылку на него записали под конец марка
Красим все новые ссылки в серый
Конец!
Бонус: если объект создали, но не записали, его не маркаем
Бонус: если ссылка на объект пропала, ну и ладно!
Красим все старые ссылки в серый
Красим новые объекты в серый
Доделываем...
Конец!
«Snapshot At The Beginning»: пометили все достижимые на начало сборки
❖ Слабая гипотеза о поколениях основана на 2 наблюдениях
➢ Большинство объектов умирают молодыми
➢ Мало ссылок из старых объектов на молодые
❖ Вся память разбита на поколения
➢ Обычно 2 поколения – молодое и старшее
➢ Молодое поколение собирается отдельно
➢ Пережившие несколько сборок объекты переходят
из молодого поколения в старшее
➢ Используется всеми сборщиками в HotSpot VM
Последовательная сборка молодого и старого поколений
Как только места для вновь создаваемого объекта в Eden нет, запускается малая сборка мусора.
Как только места для вновь создаваемого объекта в Eden нет, JVM снова попытается провести малую сборку.
Пока места в регионах Survivor достаточно, все идет хорошо.
JVM постоянно следит за тем, как долго объекты перемещаются между Survivor 0 и Survivor 1.
Если регион Survivor оказывается заполненным, то объекты из него отправляются в Tenured.
В случае, когда места для новых объектов не хватает уже в Tenured, в дело вступает полная сборка мусора, работающая с объектами из обоих поколений.
В разделе Eden создается среднестатистический объект, а не любой
Объекты-акселераты
Бывают еще объекты-акселераты, размер которых настолько велик, что
создавать их в Eden, а потом таскать за собой по Survivor’ам слишком
накладно. В этом случае они размещаются сразу в Tenured.
❖По умолчанию младшее поколение занимает одну треть всей кучи, а старшее,
соответственно, две трети.
❖При этом каждый регион Survivor занимает одну десятую младшего поколения, то
есть Eden занимает восемь десятых.
В итоге пропорции регионов по умолчанию выглядят так.
А что же происходит, если даже после выделения максимального объема памяти и ее полной чистки, места для новых объектов так и не находится?
❖ В этом случае мы ожидаемо получаем java.lang.OutOfMemoryError: Java heap space
и приложение прекращает работу, оставляя нам на память свою кучу в виде файла
для анализа.
❖Технически, это происходит в случае, если работа сборщика начинает занимать не
менее 98% времени и при этом сборки мусора освобождают не более 2% памяти.
❖С этим сборщиком все достаточно просто, так как вся его работа — это один
сплошной STW.
❖В начале каждой сборки мусора работа основных потоков приложения
останавливается и возобновляется только после окончания сборки.
+ Непритязателен по части ресурсов компьютера
+ Так как всю работу он выполняет последовательно в одном потоке, никаких заметных
оверхедов и негативных побочных эффектов у него нет.
‒ Долгие паузы на сборку мусора при заметных объемах данных.
Если приложению не требуется большой размер кучи для работы (Oracle указывает условную границу 100 МБ), оно не очень чувствительно к коротким остановкам и ему для работы доступно только одно ядро процессора, то можно приглядеться к этому варианту. В противном случае можно поискать вариант по-лучше.
Parallel GC (параллельный сборщик) развивает идеи, заложенные последовательным сборщиком, добавляя в них параллелизм и немного интеллекта.
Принципиальные отличия от Serial GC
❖ Сборкой мусора занимаются несколько потоков параллельно.
❖ Данный сборщик может самостоятельно подстраиваться под требуемые
параметры производительности.
При подключении параллельного сборщика используются те же самые подходы к организации кучи, что и в случае с Serial GC
❖ По умолчанию и малая и полная сборка задействуют многопоточность.
❖ Малая пользуется ею при переносе объектов в старшее поколение, а полная —
при уплотнении данных в старшем поколении
В случае, если вы задали слишком жесткие требования, которые сборщик не может
выполнить, он будет ориентироваться на следующие приоритеты (в порядке убывания
важности):
● Снижение максимальной паузы.
● Повышение пропускной способности.
● Минимизация используемой памяти.
❖Как и в случае с последовательным сборщиком, на время операций по очистке
памяти все основные потоки приложения останавливаются.
❖Разница только в том, что пауза, как правило, короче за счет выполнения части
работ в параллельном режиме.
+ Возможность автоматической подстройки под требуемые параметры
производительности и меньшие паузы на время cборок.
‒ Определенная фрагментация памяти(в зависимости от кол-ва потоков ).
‒ Все настройки Serial GC крутятся вокруг размеров различных регионов кучи
В целом, Parallel GC — это простой, понятный и эффективный сборщик, подходящий для большинства приложений. У него нет скрытых накладных расходов, мы всегда можем поменять его настройки и ясно увидеть результат этих изменений.
❖ CMS GC использует ту же самую организацию памяти, что и уже рассмотренные
Serial / Parallel GC:
➢ регионы Eden + Survivor 0 + Survivor 1 + Tenured
➢ и такие же принципы малой сборки мусора.
❖ Отличия начинаются только тогда, когда дело доходит до полной сборки
Важным отличием сборщика CMS от рассмотренных ранее является то, что он не дожидается заполнения Tenured для того, чтобы начать старшую сборку.
Старшая (major) сборка
CMS трудится в фоновом режиме постоянно, пытаясь поддерживать Tenured в компактном состоянии.
Отдельно следует рассмотреть ситуацию, когда сборщик не успевает очистить Tenured до того момента, как память полностью заканчивается. В этом случае работа приложения останавливается, и вся сборка производится в последовательном режиме. Такая ситуация называется сбоем конкурентного режима
+ Ориентированность на минимизацию времен простоя, что является критическим
фактором для многих приложений.
─ Но для выполнения этой задачи приходится жертвовать ресурсами процессора и
зачастую общей пропускной способностью.
‒ Сборщик не уплотняет объекты в старшем поколении, что приводит к фрагментации
Tenured.
В целом, CMS GC может подойти приложениям, использующим большой объем долгоживущих данных. В этом случае некоторые его недостатки нивелируются. Но в любом случае, не стоит принимать решение о его использовании пока вы не познакомились с еще одним сборщиком в обойме Java HotSpot VM.
Изменен подход к организации кучи.
❖ Память разбивается на множество регионов одинакового размера.
❖ Размер этих регионов зависит от общего размера кучи и по умолчанию выбирается
так, чтобы их было не больше 2048, обычно получается от 1 до 32 МБ.
Исключение
❖ Громадные (humongous) регионы, которые создаются объединением обычных
регионов для размещения очень больших объектов.
❖ Молодое поколение
➢ Набор регионов
■ Eden
■ Survivor
➢ Выбирается динамически
❖ Старое поколение
➢ Набор регионов
➢ Выбирается динамически
❖ Большие объекты
➢ Не помещается в регион
➢ Называется “humongous”
➢ Хранится в наборе смежных
регионов
❖ Collection Set
➢ Регионы, в которых будет происходить GC
■ Все молодое поколение
■ Некоторые регионы из старшего
поколения
● Фоновая маркировка определяет
наиболее подходящие
Типы сборок
❖ В молодом поколении
❖ Смешанные (mixed)
❖ FullGC
Сборка
❖ Копирование объектов в регионы,
помеченные как часть «To»-пространства
➢ Survivor - регионы
➢ Регионы из старшего поколения
Освобождение памяти
❖ From - space больше чем
To-space(не обязательно!)
❖ Компактификация за счет
копирования
RSet== Remembered Set
❖ Информация о местонахождении
ссылок на объекты из региона
❖ Позволяет собирать регионы
независимо
❖ RSet поддерживается
➢ Из старого в молодое
поколение
➢ Между регионами в старом
поколении
Малая сборка
❖ MinorGC - убираются регионы Eden/Survived
○ Вместо переноса объектов может быть изменен тип региона, с Survived на Tenured
○ Используется алгоритм предсказания кол-ва мусора в регионе. Убираются имеющие
более высокую вероятность - отсюда название алгоритма
Полная сборка(смешанной (mixed))
Процесс цикла пометки (marking cycle), который работает параллельно с основным приложением
и составляет список живых объектов.
❖ (Initial mark)Маркировка корневых объектов полученных из малых циклов, остановка
приложения
❖ (Concurrent marking)Параллельная пометка живых объектов , во время работающего
приложения
❖ (Remark)Остановка приложения и повторный поиск неучтенных объектов
❖ (Cleanup)Очистка от мусора при остановленном приложении, поиск пустых регионов для
новых объектов параллельно с работающим приложением
Следует иметь в виду, что для получения списка живых объектов G1 использует алгоритм Snapshot-At-The-Beginning (SATB), то есть в список живых попадают все объекты, которые были таковыми на момент начала работы алгоритма, плюс все объекты, созданные за время его выполнения. Это, в частности, означает, что G1 допускает наличие плавающего мусора, с которым мы познакомились при рассмотрении сборщика CMS.
❖ Процессы переноса объектов между поколениями. Для минимизации
таких пауз G1 использует несколько потоков.
❖ Короткая фаза начальной пометки корней в рамках цикла пометки.
❖ Более длинная пауза в конце фазы remark и в начале фазы cleanup
цикла пометки.
Недостатки
● Чувствителен к большим объектам
Достоинства
● Адаптивный, подстраивается под требования к производительности и
выделенным ресурсам
● Уменьшена вероятность SWT
● Нет проблем с фрагментацией Tenured
● Новые интересные стратегии тюнинга приложений
В целом, G1 GC это достойная замена CMS для серверных приложений, а так же как плацдарм для экспериментов.
● Использование Serial GC включается опцией -XX:+UseSerialGC
.
● C помощью опций Xms и Xmx можно настроить начальный и максимально допустимый размер кучи
соответственно.
● Существуют опции -XX:MinHeapFreeRatio=?
и -XX:MaxHeapFreeRatio=?
, которые задают
минимальную и максимальную долю свободного места в каждом поколении, при достижении которой
размер поколения будет автоматически увеличен или уменьшен соответственно.
● Установить желаемое отношение размера старшего поколения к суммарному размеру регионов
младшего поколения можно с помощью опции -XX:NewRatio=?
● При желании можно ограничить размер младшего поколения абсолютными величинами снизу и сверху
с помощью опций -XX:NewSize=?
и -XX:MaxNewSize=?
.
● C помощью опции -XX:-UseGCOverheadLimit
можно отключить порог активности сборщика в 98%, при
достижении которого возникает OutOfMemoryError
.
● Параллельный сборщик включается опцией -XX:+UseParallelGC.
● Вы можете вручную указать количество потоков, которое хотели бы выделить для сборки мусора. Это
делается с помощью опции -XX:ParallelGCThreads=?.
● При желании вы можете полностью отключить параллельные работы по уплотнению объектов в
старшем поколении опцией -XX:-UseParallelOldGC.
● Установка желаемых параметров производительности сборщика выполняется с помощью опций
-XX:MaxGCPauseMillis=?
и -XX:GCTimeRatio=?
.
● Опции -XX:YoungGenerationSizeIncrement=?
и -XX:TenuredGenerationSizeIncrement=?
устанавливают,
на сколько процентов следует при необходимости увеличивать младшее и старшее поколение соотвественно.
По умолчанию оба этих параметра равны 20.
● А вот скорость уменьшения размеров поколений регулируется не процентами, а специальным фактором
через опцию -XX:AdaptiveSizeDecrementScaleFactor
. Она указывает, во сколько раз уменьшение должно
быть меньше увеличения.
● Использование CMS GC включается опцией -XX:+UseConcMarkSweepGC.
● Так как подходы к организации памяти у CMS аналогичны используемым в Serial / Parallel GC, для него
применимы те же опции определения размеров регионов кучи, а также опции автоматической подстройки
под требуемые параметры производительности.
● Обычно CMS, основываясь на собираемой статистике о поведении приложения, сам определяет, когда
ему выполнять старшую сборку, но у него также есть порог наполненности региона Tenured, при
достижении которого должна обязательно быть инициирована старшая сборка. Этот порог можно задать с
помощью опции -XX:CMSInitiatingOccupancyFraction=?
, значение указывается в процентах.
● G1 включается опцией Java -XX:+UseG1GC
● Так как основной целью сборщика G1 является минимизация пауз в работе основного приложения, то и
главной опцией при его настройке можно считать уже встречавшуюся нам -XX:MaxGCPauseMillis=?
, задающую
приемлемое для нас максимальное время разовой сборки мусора.
● Опции -XX:ParallelGCThreads=?
и -XX:ConcGCThreads=?
задают количество потоков, которые будут
использоваться для сборки мусора и для выполнения цикла пометок соответственно.
● Если вас не устраивает автоматический выбор размера региона, вы можете задать его вручную с помощью
опции -XX:G1HeapRegionSize=?
. Значение должно быть степенью двойки, если мерить в мегабайтах.
● При желании можно изменить порог заполненности кучи, при достижении которого инициируется
выполнение цикла пометок и переход в режим смешанных сборок. Это делается опцией
-XX:InitiatingHeapOccupancyPercent=?
, принимающей значение в процентах. По умолчанию, этот порог равен
45%.
● Если же вы решите залезть в дебри настроек G1 по-глубже, то можете включить дополнительные функции
опциями -XX:+UnlockExperimentalVMOptions
и -XX:+AggressiveOpts
и поиграть с экспериментальными
настройками.