Top-office11.ru

IT и мир ПК
2 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Как работает garbage collector java

Сборщик мусора Garbage Collection

Чтобы понять, как работает сборщик мусора Garbage Collection, необходимо иметь представление о распределении памяти в JVM (Java Virtual Machine). Данная статья не претендует на то, чтобы покрыть весь объем знаний о распределении памяти в JVM и описании Garbage Collection, поскольку он слишком огромен. Да, к тому же, об этом достаточно информации уже имеется в Сети, чтобы желающие могли докапаться до ее глубин. Но, думаю, данной статьи будет достаточно, чтобы иметь представление о том, как JVM работает с памятью java-приложения.

Респределение памяти в JVM

Для рассмотрения вопроса распределения памяти JVM будем использовать широко распространенную виртуальную машину для Windows от Oracle HotSpot JVM (раньше был от Sun). Другие виртуальные машины (из комплекта WebLogic или open source JVM из Linux) работают с памятью по похожей на HotSpot схеме. Возможности адресации памяти, предоставляемые архитектурой ОС, зависят от разрядности процессора, определяющего общий диапазон емкости памяти. Так, например, 32-х разрядный процессор обеспечивает диапазон адресации 2 32 , то есть 4 ГБ. Диапазон адресации для 64-разрядного процессора (2 64 ) составляет 16 экзабайт.

Разделение памяти JVM

Память процесса делится на Stack (стек) и Heap (куча) и включает 5 областей :

  • Stack
    • Permanent Generation — используемая JVM память для хранения метаинформации; классы, методы и т.п.
    • Code Cache — используемая JVM память при включенной JIT-компиляции; в этой области памяти кешируется скомпилированный платформенно-зависимый код.
  • Heap
    • Eden Space — в этой области выделяется память под все создаваемые программой объекты. Жизненный цикл большей части объектов, к которым относятся итераторы, объекты внутри методов и т.п., недолгий.
    • Survivor Space — здесь хранятся перемещенные из Eden Space объекты после первой сборки мусора. Объекты, пережившие несколько сборок мусора, перемещаются в следующую сборку Tenured Generation.
    • Tenured Generation хранит долгоживущие объекты. Когда данная область памяти заполняется, выполняется полная сборка мусора (full, major collection).

Permanent Generation

Область памяти Permanent Generation используется виртуальной машиной JVM для хранения необходимых для управления программой данных, в том числе метаданные о созданных объектах. При каждом создании объекта JVM будет сохранять некоторый набор данных об объекте в области Permanent Generation. Соответственно, чем больше создается в программе объектов, тем больше требуется «пространства» в Permanent Generation.

Размер Permanent Generation можно задать двумя параметрами виртуальной машины JVM :

  • -XX:PermSize – минимальный размер выделяемой памяти для Permanent Generation;
  • -XX:MaxPermSize – максимальный размер выделяемой памяти для Permanent Generation.

Для «больших» Java-приложений можно при запуске определить одинаковые значения данных параметров, чтобы Permanent Generation была создана с максимальным размером. Это может увеличить производительность, поскольку динамическое изменение размера Permanent Generation является «дорогостоящей» (трудоёмкой) операцией. Определение одинаковых значений этих параметров может избавить JVM от выполнения дополнительных операций, связанных с проверкой необходимости изменения размера Permanent Generation.

Область памяти Heap

Куча Heap является основным сегментом памяти, где хранятся создаваемые объекты. Heap делится на два подсегмента : Tenured (Old) Generation и New Generation. New Generation в свою очередь делится на Eden Space и Survivor.

При создании нового объекта, когда используется оператор ‘new’, например byte[] data = new byte[1024], этот объект создаётся в сегменте Eden Space. Кроме, собственно данных для массива байт, создается также ссылка (указатель) на эти данные. Если места в сегменте Eden Space уже нет, то JVM выполняет сборку мусора. При сборке мусора объекты, на которые имеются ссылки, не удаляются, а перемещаются из одной области в другую. Так, объекты со ссылками перемещаются из Eden Space в Survivor Space, а объекты без ссылок удаляются.

Если количество используемой Eden Space памяти превышает некоторый заданный объем, то Garbage Collection может выполнить быструю (minor collection) сборку мусора. По сравнению с полной сборкой мусора данный процесс занимает немного времени, и затрагивает только область Eden Space — устаревшие объекты без ссылок удаляются, а выжившие перемещаются в область Survivor Space.

Размер сегмента Heap можно определить двумя параметрами : Xms (минимум) и -Xmx (максимум).

В чем отличие между сегментами Stack и Heap?

  • Heap (куча) используется всеми частями приложения, а Stack используется только одним потоком исполнения программы.
  • Новый объект создается в Heap, а в памяти Stack’a размещается ссылка на него. В памяти стека также размещаются локальные переменные примитивных типов.
  • Объекты в куче доступны из любого места программы, в то время, как стековая память не доступна для других потоков.
  • Если память стека полностью занята, то Java Runtime вызывает исключение java.lang.StackOverflowError, а если память кучи заполнена, то вызывается исключение java.lang.OutOfMemoryError: Java Heap Space.
  • Размер памяти стека, как правило, намного меньше памяти в куче. Из-за простоты распределения памяти (LIFO), стековая память работает намного быстрее кучи.

Garbage Collector

Сборщик мусора Garbage Collector выполняет всего две задачи, связанные с поиском мусора и его очисткой. Для обнаружения мусора существует два подхода :

  • Reference counting – учет ссылок;
  • Tracing – трассировка.

Reference counting

Суть подхода «Reference counting» связана с тем, что каждый объект имеет счетчик, который хранит информацию о количестве указывающих на него ссылок. При уничтожении ссылки счетчик уменьшается. При нулевом значении счетчика объект можно считать мусором.

Главным недостатком данного подхода является сложность обеспечения точности счетчика и «невозможность» выявлять циклические зависимости. Так, например, два объекта могут ссылаться друг на друга, но ни на один из них нет внешней ссылки. Это сопровождается утечками памяти. В этой связи данный подход не получил распространения.

Tracing

Главная идея «Tracing» связана с тем, что до «живого» объекта можно добраться из корневых точек (GC Root). Всё, что доступно из «живого» объекта, также является «живым». Если представить все объекты и ссылки между ними как дерево, то необходимо пройти от корневых узлов GC Roots по всем узлам. При этом узлы, до которых нельзя добраться, являются мусором.

Данный подход, обеспечивающий выявление циклических ссылок, используется в виртуальной машине HotSpot VM. Теперь, осталось понять, а что представляет из себя корневая точка (GC Root)? «Источники» говорят, что существуют следующие типы корневых точек :

  • Основной Java поток.
  • Локальные переменные в основном методе.
  • Статические переменные основного класса.

Таким образом, простое java-приложение будет иметь следующие корневые точки:

  • Параметры main метода и локальные переменные внутри main метода.
  • Поток, который выполняет main.
  • Статические переменные основного класса, внутри которого находится main метод.

Очистка памяти

Имеется несколько подходов к очистке памяти, которые в совокупности определяют принцип функционирования Garbage Collection.

Copying collectors

При использовании «Copying collectors» область памяти делится на две части : в одной части размещаются объекты, а вторая часть остается чистой. На время очистки мусора приложение останавливает работу и запускается сборщик мусора, который находит в первой области объекты со ссылками и переносит их во вторую (чистую) область. После этого, первая область очищается от оставшихся там объектов без ссылок, и области меняются местами.

Главным достоинством данного подхода является плотное заполнение памяти. Недостатком «Copying collectors» является необходимость остановки приложения и размеры двух частей памяти должны быть одинаковыми на случай, когда все объекты остаются «живыми».

Данный подход в чистом виде в HotSpot VM не используется.

Mark-and-sweep

При использовании «mark-and-sweep» все объекты размещаются в одном сегменте памяти. Сборка мусора также приостанавливает приложение, и Garbage Collection проходит по дереву объектов, помечая занятые ими области памяти, как «живые». После этого, все не помеченные участки памяти сохраняются в «free list», в которой будут, после завершения сборки мусора, размещаться новые объекты.

К недостаткам данного подхода следует отнести необходимость приостановки приложения. Кроме этого, время сборки мусора, как и время приостановки приложения, зависит от размера памяти. Память становится «решетчатой», и, если не применить «уплотнение», то память будет использоваться неэффективно.

Данный подход также в чистом виде в HotSpot VM не используется.

Generational Garbage Collection

JVM HotSpot использует алгоритм сборки мусора типа «Generational Garbage Collection», который позволяет применять разные модули для разных этапов сборки мусора. Всего в HotSpot реализовано четыре сборщика мусора :

  • Serial Garbage Collection
  • Parallel Garbage Collection
  • CMS Garbage Collection
  • G1 Garbage Collection
Читать еще:  Java как возвести число в степень

Serial Garbage Collection относится к одним из первых сборщиков мусора в HotSpot VM. Во время работы этого сборщика приложение приостанавливается и возобновляет работу только после прекращения сборки мусора. В Serial Garbage Collection область памяти делится на две части («young generation» и «old generation»), для которых выполняются два типа сборки мусора :

  • minor GC – частый и быстрый c областью памяти «young generation»;
  • mark-sweep-compact – редкий и более длительный c областью памяти «old generation».

Область памяти «young generation», представленная на следующем рисунке, разделена на две части, одна из которых Survior также разделена на 2 части (From, To).

Алгоритм работы minor GC

Алгоритм работы minor GC очень похож на описанный выше «Copying collectors». Отличие связано с дополнительным использованием области памяти «Eden». Очистка мусора выполняется в несколько шагов :

  • приложение приостанавливается на начало сборки мусора;
  • «живые» объекты из Eden перемещаются в область памяти «To»;
  • «живые» объекты из «From» перемещаются в «To» или в «old generation», если они достаточно «старые»;
  • Eden и «From» очищаются от мусора;
  • «To» и «From» меняются местами;
  • приложение возобновляет работу.

В результате сборки мусора картинка области памяти изменится и будет выглядеть следующим образом :

Некоторые объекты, пережившие несколько сборок мусора в области From, переносятся в «old generation». Следует, также отметить, что и «большие живые» объекты могут также сразу же пеместиться из области Eden в «old generation» (на картинке не показаны).

Алгоритм работы mark-sweep-compact

Алгоритм «mark-sweep-compact» связяан с очисткой и уплотнением области памяти «old generation».

Принцип работы «mark-sweep-compact» похож на описанный выше «Mark-and-sweep», но добавляется процедура «уплотнения», позволяющая более эффективно использовать память. В процедуре живые объекты перемещаются в начало. Таким образом, мусор остается в конце памяти.

При работе с областью памяти используется механизм «bump-the-pointer», определяющий указатель на начало свободной памяти, в которой размещается создаваемый объект, после чего указатель смещается. В многопоточном приложении используется механизм TLAB (Thread-Local Allocation Buffers), который для каждого потока выделяет определенную область памяти.

Garbage Collection наглядно

В последнее время я работаю с клиентами над вопросами настроек JVM. Смахивает ситуация на то, что далеко не все из разработчиков и администраторов знают о том, как работает garbage collection и о том, как JVM использует память. Поэтому я решил дать вводную в эту тему с наглядным примером. Пост не претендует на то, чтобы покрыть весь объем знаний о garbage collection или настройке JVM (он огромен), ну и, в конце концов, об этом много чего хорошего написано уже в Сети.

Пост посвящён HotSpot JVM – ‘обычной’ JVM от Oracle (раньше Sun), JVM, которую вы скорее всего будете использовать в Windows. В случае Linux это может быть open source JVM. Или JVM может идти в комплекте с другим ПО, например WebLogic, или даже можно использовать Jrockit JVM от Oracle (раньше BEA). Или другие JVM от IBM, Apple и др. Большинство этих «других» JVM работают по похожей на HotSpot схеме, за исключением Jrockit, чье управление памятью отлично от других и который, например, не имеет выделенного Permanent Generation (см. ниже).

Давайте начнём с того, как JVM использует память. В JVM память делится на два сегмента – Heap и Permanent Generation. На диаграмме Permanent Generation обозначено зеленым, остальное – heap.

The Permanent Generation

Permanent generation используется только JVM для хранения необходимых данных, в том числе метаданные о созданных объектах. При каждом создании объекта JVM будет «класть» некоторый набор данных в PG. Соответственно, чем больше вы создаете объектов разных типов, тем больше «жилого пространства» требуется в PG.
Размер PG можно задать двумя параметрами JVM: -XX:PermSize – задаёт минимальный, или изначальный, размер PG, и -XX:MaxPermSize – задаёт максимальный размер. При запуске больших Java-приложений мы часто задаём одинаковые значения для этих параметров, так что PG создаётся сразу с размером «по-максимуму», что может увеличить производительность, так как изменение размера PG – дорогостоящая (трудоёмкая) операция. Определение одинаковых значений для этих двух параметров может избавить JVM от выполнения дополнительных операций, таких как проверки необходимости изменения размера PG и, естественно, непосредственного изменения.

Heap – основной сегмент памяти, где хранятся все ваши объекты. Heap делится на два подсегмента, Old Generation и New Generation. New Generation в свою очередь делится на Eden и два сегмента Survivor.
Размер heap также можно указать параметрами. На диаграмме это Xms (минимум) и -Xmx (максимум). Дополнительные параметры контролируют размеры сегментов heap. Мы позднее посмотрим один из них, остальные за рамкой этого поста.
При создании объекта, когда вы пишете что-то типа byte[] data = new byte[1024], этот объект создаётся в сегменте Eden. Новые объекты создаются в Eden. Кроме собственно данных для нашего массива байт здесь есть ещё ссылка (указатель) на эти данные.
Дальнейшее объяснение упрощено. Когда вы хотите создать новый объект, но места в Eden уже нет, JVM проводит garbage collection, что значит, что JVM ищет в памяти все объекты, которые более не нужны, и избавляется от них.

Garbage collection – это круто! Если вы когда-либо программировали на языках типа C или Objective-C, то вы знаете, что ручное управление памятью – вещь утомительная и порой вызывающая ошибки. Наличие JVM, которая автоматически позаботится о неиспользуемых объектах, делает разработку проще и сокращает время отладки. Если же вы никогда не писали на подобных языках, значит возьмите С и попробуйте написать программу, и ощутите, как ценно то, что предоставляется вашим языком совершенно бесплатно.

Существует множество алгоритмов, которыми может воспользоваться JVM для проведения garbage collection. Можно указать, какие из них будут использоваться JVM с помощью параметров.
Давайте посмотрим на пример. Допустим, у нас есть следующий код:

В Eden создаётся («размещается») пять объектов, как показано пятью желтыми квадратиками на диаграмме. После «чего-нибудь» мы освобождаем a,b,c и e, присваивая ссылкам null. Имея в виду, что больше ссылок на них нет, они теперь не нужны, и показаны красным на второй диаграмме. При этом мы всё ещё нуждаемся в String d (показано зелёным).

Если мы попробуем разместить ещё объект, JVM обнаружит, что Eden полон и надо провести чистку. Самый простой алгоритм для garbage collection называется Copy Collection, и работает он так, как показано на диаграмме. На первом этапе Mark помечаются неиспользуемые объекты (красные). На втором (Copy) объекты, которые ещё нужны (d) копируется в сегмент survivor – квадрат справа. Сегментов Survivor два, и они меньше Eden. Теперь все объекты, которые мы хотим, чтобы они были сохранены, скопированы в Survivor, и JVM просто удаляет всё из Eden. На этом всё.
Этот алгоритм создаёт кое-что, что называется моментом, «когда мир остановился». Во время выполнения GC все другие треды в JVM переводятся в состояние паузы, ради того, чтобы никакой из них не попробовал влезть в память после того, как мы скопировали всё оттуда, что привело бы к потере того, что сделано. Это не великая проблема, если она в небольшом приложении, но если у нас на руках серьёзная программа, скажем, с 8-гигабайтным heap, то выполнение GC займёт много времени – секунды или даже минуты. Естественно, что каждый раз останавливать приложение – вариант не подходящий. Потому и существуют другие алгоритмы, и используются часто. Copy Collection же работает хорошо в том случае, если у нас много мусора и мало полезных объектов.
В этом посте мы поговорим насчёт двух распространённых алгоритмов. Для интересующихся есть много информации в Сети и несколько хороших книг.
Следующий алгоритм называется Mark-Sweep-Compact Collection. У алгоритма три этапа:

1) «Mark»: помечаются неиспользуемые объекты (красные).

2) «Sweep»: эти объекты удаляются из памяти. Обратите внимание на пустые слоты на диаграмме.

3) «Compact»: объекты размещаются, занимая свободные слоты, что освобождает пространство на тот случай, если потребуется создать «большой» объект.

Но это всё теория, так что давайте посмотрим, как это работает, на примере. К счастью, JDK имеет визуальный инструмент для наблюдения за JVM в реальном времени, а именно jvisualvm. Лежит он прямо в bin JDK. Воспользуемся ею немного позже, сначала займёмся приложением.
Для разработки, билдов и зависимостей я воспользовался Maven, но вам он не нужен – просто скомпилируйте и запустите приложение, если вам так угодно.

Читать еще:  Javascript модуль числа

Я выбрал простой JAR (98) и все по умолчанию для остального. Дальше переключился в директорию memoryTool и отредактировал pom.xml (ниже, добавил блок plugin). Это позволило мне запустить приложение прямо из Maven, передав необходимые параметры.

Если вы предпочитаете не использовать Maven, то можете запустить приложение следующей командой:

При этом:
-Xms определяем исходный/минимальный размер heap в 512 мб
-Xmx определяем максимальный размер heap в 512 мб
-XX:NewRatio определяем размер old generation большим в три раза чем размер new generation
-XX:+PrintGCTimeStamps, -XX:+PrintGCDetails и -Xloggc:gc.log JVM печатает дополнительную информацию касаемо garbage collection в файл gc.log
-classpath определяем classpath
com.redstack.App main класс

Ниже – код main-класса. Простая программа, в которой мы создаём объекты и далее выкидываем их, так что понятно, сколько памяти используется и мы можем посмотреть, что происходит с JVM.

Для сборки и выполнения кода используем следующую команду Maven:
mvn package exec:exec

Как только скомпилируете и будете готовы к дальнейшим действиям, запускайте ее и jvisualvm. Если вы не использовали jvisualvm ранее, то нужно установить плагин VisualGC: выберите Plugins в меню Tools, дальше вкладку Available Plugins. Выберите Visual GC и нажмите Install.
Вы должны увидеть список процессов JVM. Два раза кликните на том, в котором исполняется ваше приложение (в данном примере com.redstack.App) и откройте вкладку Visual GC. Должно появиться что-то типа того, что на скриншоте ниже.

Обратите внимание, что можно визуально наблюдать состояние permanent generation, old generation, eden и сегментов survivor (S0 и S1). Цветные колонки показывают используемую память. Справа находится historical view, которое показывает, когда JVM проводило garbage collections и количество памяти в каждом из сегментов.

В окне приложения начните создавать объекты (опция 1) и наблюдайте, что будет происходить в Visual GC. Обратите внимание на то, что новые объекты всегда создаются в eden. Теперь сделайте пару объектов ненужными (опция 2). Возможно, вы не увидите изменений в Visual GC. Это потому, что JVM не чистит это пространство до тех пор, пока не закончена процедура garbage collection.

Чтобы инициировать garbage collection, создайте ещё объектов, заполнив Eden. Обратите внимание, что произойдет в момент заполнения. Если в Eden много мусора, вы увидите, как объекты из Eden «переезжают» в survivor. Однако, если в Eden мало мусора, вы увидите, как объекты «переезжают» в old generation. Это случается тогда, когда объекты, которые необходимо оставить, больше чем survival.
Пронаблюдайте также за постепенным увеличением Permanent Generation.
Попробуйте заполнить Eden, но не до конца, потом выбросьте почти все объекты, оставьте только 20 мб. Получиться так, что Eden на большую часть заполнен мусором. После этого создайте ещё объектов. На этот раз вы увидите, что объекты из Eden переходят в Survivor.

А теперь давайте посмотрим, что будет, если у нас не хватит памяти. Создавайте объекты до тех пор, пока их не будет на 460 мб. Обратите внимание, что и Eden и Old Generation заполнены практически полностью. Создайте ещё пару объектов. Когда больше не останется памяти, приложение «упадёт» с исключением OutOfMemoryException. Вы уже могли сталкиваться с подобным поведением и думать, почему же это произошло – особенно, если у вас большой объем физической памяти на компьютере и вы удивлялись, как такое вообще могло произойти, что не хватает памяти – теперь вы знаете, почему. Если так случится, что Permanent Generation заполнится (довольно сложно в случае нашего примера добиться этого), у вас будет брошено другое исключение, сообщающее, что PermGen заполнен.

И, наконец, ещё один способ посмотреть, что происходило, это обратиться к логу. Вот немного из моего:

В логе можно увидеть, что происходило в JVM — обратите внимание, что использовался алгоритм Concurrent Mark Sweep Compact Collection algorithm (CMS), есть описание этапов и YG — Young Generation.

Можно воспользоваться этими настройками и в «продакшене». Есть даже инструменты, визуализирующие лог.

Ну, вот и закончилось наше краткое введение в теории и практики JVM garbage collection. Надеюсь, что приложение из примера помогло вам чётко представить, что происходит в JVM когда запущено ваше приложение.
Спасибо Rupesh Ramachandran за то, чему он научил меня относительно настроек JVM и garbage collection.

Сборщик мусора Parallel GC в Java

Параллельный сборщик мусора или Parallel GC, также его называют throughput collector, схож с Serial GC. Он тоже использует деление на молодое и старшее поколения. И также он использует области Eden и Survivor. Однако порядок областей другой:

Включить параллельный сборщик мусора можно с помощью аргумента -XX:+UseParallelGC , переданного в качестве параметра командной строки виртуальной машины Java. Параллельный сборщик мусора выбирается по умолчанию для серверных машин.

По умолчанию параллельно основным потокам программы происходит сборка мусора как из молодого поколения, так и полная.

Параллельный сборщик мусора особенно полезен для систем с несколькими физическими процессорами. Для машин с N физическими процессорами Parallel GC создаёт N потоков для случая N =8.

Количество создаваемых потоков можно задать с помощью командной строки -XX:ParallelGCThreads= .

Так как при сборке мусора работают несколько потоков, то при перемещении объектов из молодого поколения в старшее возможна фрагментация старшего поколения, так как каждому потоку выделяется своя, изолированная от остальных область в tenured.

При тюнинге parallel GC можно указать не размеры поколений и другие низкоуровневые параметры, а более высокоуровневые настройки: максимальную паузу для сборки мусора, throughput (производительность) и footprint (размер кучи).

  1. Максимальная пауза для сборки мусора устанавливается с помощью параметра XX_MaxGCPauseMillis= в миллисекундах. Причём указанное значение не всегда будет успешно выдерживаться, но сборщик мусора будет пытаться подстраивать размер кучи и другие параметры так, чтобы сборка мусора была меньше указанного здесь значения.
  2. Throughput или производительность устанавливается с помощью параметра — XX : GCTimeRatio = N > , где указывается отношение времени затраченного на сборку мусора к остальному времени работы приложения. Parallel GC будет стараться придерживаться этого целевого значения производительности.
  3. Footprint или максимальный размер кучи устанавливается с помощью параметра -Xmx , причём сборщик будет пытаться выдерживать как можно меньший размер кучи при достижении остальных целевых параметров.

Цели рассматриваются в следующем приоритете:

  1. Максимальная пауза при сборке мусора
  2. Производительность
  3. Минимальный размер кучи.

Если больше 98 % времени тратится на сборку мусора и удалось очистить меньше 2 % кучи, то параллельный сборщик мусора бросает исключение OutOfMemoryError .

Размеры поколений при необходимости увеличиваются и уменьшаются. По умолчанию увеличение идёт на 20 %, а уменьшение на 5 %. Эти значения настраиваются с помощью параметров командной строки. Для шага увеличения размера молодого поколения используйте параметр -XX:YoungGenerationSizeIncrement= (в процентах), для шага увеличения размера старшего поколения используйте -XX:TenuredGenerationSizeIncrement= (тоже в процентах). Для шага уменьшения размеров поколения используется -XX:AdaptiveSizeDecrementScaleFactor= , где процент уменьшения размера поколения вычисляется по формуле X/D, (X — процент увеличения размера поколения, D — значение параметра -XX:AdaptiveSizeDecrementScaleFactor ).

zigins >Заметки Android

Функции

Garbage Collector (GB) часть JVM, который призван очищать память, выделенную приложению. Он должен:

  • найти мусор (неиспользуемые объекты)
  • удалить мусор

Есть различные реализации GB.

Поиск мусора

  • Reference counting — у каждого объекта счетчик ссылок. Когда он равен нулю, объект считается мусором. Проблема такого подхода в том, что могут быть цикличные ссылки у объектов друг на друга, в то время как они фактически мусор и не используются программой.
  • Tracing — объект считается не мусором, если до него можно добраться с корневых точек (GC Root: локальные переменные и параметры методов, java-потоки, статичные переменные, ссылки из JNI.

Организация памяти JVM

Делится на две части:

  • Heap — куча. Основной сегмент памяти, где содержатся все объекты и происходит сборка мусора.
  • Permanent Generation — содержит мета-данные классов.

Сразу про Permanent Generation. Может менять размер во время выполнения, и это довольно дорогостоящая операция. Размер настраивается (-XX: PermSize — мин размер, -XX: MaxSize — макс размер). Часто мин = макс.

Читать еще:  Установка среды разработки java

Heap. Куча. Тут и работает GC.

Делится на две области:

  • New (Yang) Generation — объекты, кот. тут считаются короткоживущими.
  • Old Generation (Tenured) — обекты считаются долгоживущими.

Алгоритм GC исходит из того предположения, что большинство java-объектов живут недолго. Быстро становятся мусором. От них необходимо довольно оперативно избавляться. Что и происходит в New Generation. Там сбор мусора гораздо чаще, чем в Old Generation, где хранятся долгоживущие объекты. После создания объект попадает в New Generation и имеет шанс попасть в Old Generation по прошествии некоторого времени (циклов GC).

Heap состоит из:

  • Eden — переводится как Едем (?). Сюда аллоцируются объекты. Если нет места запускается GC.
  • Survivor — точнее их два, S1 и S2, и они меняются ролями. Хранятся объекты, которые признаются живыми во время GC.

Размер Heap настраивается.

Принцип работы 4 сборщиков HotSpot VM (одна из JVM)

  • Serial
  • Parallel
  • Concurent Mark Sweep (CMS)
  • Garbage-First (G1)

Serial. Когда нет места в Eden, запускается GC, живые объекты коприруются в S1. Вся область Eden очищается. S1 и S2 меняются местами. При последующих циклах в S1 будут записаны живые объекты как из Eden, так и из S2. После нескольких циклов обмена S1 и S2 или заполнения области S2, обекты, которые живут достаточно долго перемещаются в Old Greneration.

Следует сказать, что не всегда объекты при создании аллоцируюся в Eden. Если объект слишком велик, он сразу идет в Old Generation.

Когда после очередной сборки мусора места нехватает уже в New Generation, то запускается сбор мусора в Old Generation (наряду со сборкой New Generation). В old Generation объекты уплотняются (алгоритм Mark-Sweep-Compact).

Если после полной сборки мусора места нехватает, то вылетает Java.lang.OutOfMemoryError.

Но во время работы VW может запрашивать увеличение памяти и Heap может увеличиваться.

Как правило, Old Generation занимает 2/3 объема Heap.

Эффективоность алгоритма сборки мусора считается по параметру STW (Stop The World) — время, когда все процессы кроме GC останавливаются. Serial в этом смысле не слишком эффективен, т.к. делает свою работу не торопясь, в одном потоке.

Parallel. То же, что и Serial, но использует для работы несколько потоков. Таким образом STW чуть меньше.

Concurent Mark Sweep. Принцип работы с New Generation такой же, как и в случае алгоритмов Serial и Parallel, отличия в том, что данный алгоритм разделяет младшую (New Generation) и старшую (Old Generation) сборку мусора во времени. Причем сбор мусора в Old Generation происходит в отдельном потоке, независимо от младшей сборки. При этом сначала приложение останавливается, сборщик помечает все живые объекты доступные из GC Root (корневых точек) напрямую, затем приложение вновь начинает работу, а сбощик проверяет объекты доступные по ссылкам из этих самых помеченных, и также помечает их как живые. Эта особенность создает так называемые плавающие объекты, которые помечены как живые, но таковыми по факту не являющимися. Но они будут удалены в следующих циклах. Т.е. пропускная способность растет, STW уменьшается, но требутся больше места для хранения плавающих объектов.

В этом алгоритме уплотнения нет. Т.е. область Old Generation дефрагментированна.

Garbage-First. G1 сильно отличается от своих предшественников. Он делит область Heap не физически, а скорее логически на те же области: Eden, Survivor, Old Generation. Причем дефрагментированно. Физически область Heap делится на регионы одинакового размера, каждый из которых может быть Eden, Survivor или Old Generation + область для больших объектов (громадный регион).

Над очисткой регионов Eden работает сразу несколько потоков, объекты переносятся в регионы Survivor или регионы старшего поколения (Tenured). Это знакомый по предыдущим алгоритмам очистки подход. На время очистки работа приложения останавливается. Отличие в том, что очистка производится не по всем регионам Eden, а только по некоторым, которые более всего в ней нуждаются, таким образом регулируется время очистки. Отсюда название алгоритма — в первую очередь мусор.

А с полной сборкой (точнее, здесь она называется смешанной (mixed)) все немного хитроумнее, чем в рассмотренных ранее сборщиках. В G1 существует процесс, называемый циклом пометки (marking cycle), который работает параллельно с основным приложением и составляет список живых объектов. За исключением последнего пункта, этот процесс выглядит уже знакомо для нас:

  • Initial mark. Пометка корней (с остановкой основного приложения) с использованием информации, полученной из малых сборок.
  • Concurrent marking. Пометка всех живых объектов в куче в нескольких потоках, параллельно с работой основного приложения.
  • Remark. Дополнительный поиск не учтенных ранее живых объектов (с остановкой основного приложения).
  • Cleanup. Очистка вспомогательных структур учета ссылок на объекты и поиск пустых регионов, которые уже можно использовать для размещения новых объектов. Первая часть этого шага выполняется при остановленном основном приложении.

После окончания цикла пометки G1 переключается на выполнение смешанных сборок. Это значит, что при каждой сборке к набору регионов младшего поколения, подлежащих очистке, добавляется некоторое количество регионов старшего поколения. Количество таких сборок и количество очищаемых регионов старшего поколения выбирается исходя из имеющейся у сборщика статистики о предыдущих сборках таким образом, чтобы не выходить за требуемое время сборки. Как только сборщик очистил достаточно памяти, он переключается обратно в режим малых сборок.

Очередной цикл пометки и, как следствие, очередные смешанные сборки будут запущены тогда, когда заполненность кучи превысит определенный порог.

Опираясь на уже упомянутую статистику о предыдущих сборках, G1 может менять количество регионов, закрепленных за определенным поколением, для оптимизации будущих сборок.

Громадные регионы. С точки зрения JVM объекты которые превышают размер половины региона являются громадными. Особенности:

  • никогда не перемещается между регионами
  • может удаляться в рамках цикла пометки или полной сборки мусора
  • в регионе, занятом громадным объектом, может находится только он сам.

Громадные объекты в силу небольшого размера регионов могут порождать проблемы с точки зрения STW.

G1 выигрывает по времени STW, но расплатой является меньшая пропускная способность (около 90%, ср., например у Paraller ок. 99%) т.е. большие затраты ресурсов процессора.

Bonus

Вопрос: Расскажите почему именно два региона survival и зачем перекладывать объекты между ними?

Ответ:
Представьте себя на месте сборщика. У вас есть регион памяти, который нужно очистить. После удаления мусора регион оказывается сильно дефрагментированным и если вы хотите это исправить, то у вас есть два варианта: либо уплотнять объекты в рамках этого же региона, либо скопировать их в другой, пока еще пустой регион, располагая один-к-одному, а старый регион объявить пустым. Но задача осложняется тем, что объекты ссылаются друг на друга и при перемещении любого объекта необходимо производить обновление всех имеющихся на него ссылок. И вот эту задачу намного легче решать при копировании, причем сразу объединяя ее с задачей поиска живых объектов:

Вы просто заводите два указателя на начало новой области. Первый указатель (назовем его T) смещается вправо каждый раз, когда в новую область копируется объект, то есть он всегда указывает на первый свободный блок новой области. При этом на том месте старой области, где находился перемещаемый объект, мы делаем пометку о том, что он был перемещен, и там же оставляем его новый адрес. Первым делом перемещаем таким образом все руты из старой области в новую. И вот тут вступает в действие второй указатель (назовем его R). Он тоже начинает смещаться вправо по уже размещенным в новой области объектам. В каждом объекте он ищет ссылки на другие объекты и смотрит на то место в старом регионе, куда они указывают. Если там стоит метка о перемещении и новый адрес, то этот адрес используется для подмены. Если же там лежит объект, то он перемещается в новый регион, на его месте ставится метка и новый адрес, на который так же заменяется ссылка, по которой его нашли, при этом T опять смещается вправо. Как только R догонит T, окажется, что мы собрали все живые объекты в новой области, размещенные компактно, да еще и с корректно обновленными ссылками, а старый регион можем объявить пустым. Все быстро и просто.

Ссылка на основную публикацию
Adblock
detector