Очистка оперативной памяти java
у меня проблема с Java-приложением, работающим под Linux.
когда я запускаю приложение, используя максимальный размер кучи по умолчанию (64 МБ), я вижу, используя приложение tops, что 240 МБ виртуальной памяти выделены приложению. Это создает некоторые проблемы с некоторым другим программным обеспечением на компьютере, который относительно ограничен ресурсами.
зарезервированная виртуальная память все равно не будет использоваться, насколько я понимаю, потому что как только мы достигнем кучи ограничить OutOfMemoryError бросается. Я запустил то же приложение под windows, и я вижу, что размер виртуальной памяти и размер кучи похожи.
есть ли в любом случае, что я могу настроить виртуальную память, используемую для процесса Java под Linux?
изменить 1: проблема не в куче. Проблема в том, что если я установил кучу 128 МБ, например, все еще Linux выделяет 210 МБ виртуальной памяти, которая не нужна никогда.**
изменить 2: используя ulimit -v позволяет ограничить объем виртуальной памяти. Если размер установлен ниже 204 МБ, то приложение не будет работать, даже если ему не нужно 204 МБ, только 64 МБ. Поэтому я хочу понять, почему Java требует так много виртуальной памяти. Можно ли это изменить?
Edit 3: есть несколько других приложений, работающих в системе, которая встроена. И у системы есть ограничение виртуальной памяти (из комментариев, важная деталь).
это была давняя жалоба с Java, но она в значительной степени бессмысленна и обычно основана на взгляде на неправильную информацию. Обычная фразировка-это что-то вроде "Hello World на Java занимает 10 мегабайт! Зачем ему это нужно?"Ну, вот способ сделать Hello World на 64-битной претензии JVM, чтобы занять 4 гигабайта . по крайней мере, по одной форме измерения.
- VIRT-это пространство виртуальной памяти: сумма всего на карте виртуальной памяти (см. ниже). Это в значительной степени бессмысленно, за исключением случаев, когда это не так (см. ниже).
- RES-размер резидентного набора: количество страниц, которые в настоящее время находятся в ОЗУ. Почти во всех случаях это единственное число, которое вы должны использовать, говоря "слишком большой."Но это все еще не очень хорошее число, особенно когда речь идет о Java.
- SHR-это объем резидентной памяти, которая используется совместно с другими процессами. Для процесса Java это обычно ограничивается общими библиотеками и сопоставленными с памятью JARfiles. В этом примере у меня был только один процесс Java, поэтому я подозреваю, что 7k является результатом библиотек, используемых ОС.
- SWAP по умолчанию не включен и не отображается здесь. Он указывает объем виртуальной памяти, который в настоящее время резидент на диске,является ли это на самом деле в пространстве подкачки. ОС очень хорошо поддерживает активные страницы в ОЗУ, и единственными средствами для замены являются (1) покупка больше памяти или (2) уменьшение количества процессов, поэтому лучше игнорировать это число.
ситуация для диспетчера задач Windows немного сложнее. В Windows XP есть столбцы" использование памяти "и" размер виртуальной памяти", но официальный документация молчит о том, что они означают. Windows Vista и Windows 7 добавляют больше столбцов, и они на самом деле документирована. Из них измерение "рабочего набора" является наиболее полезным; оно примерно соответствует сумме RES и SHR в Linux.
виртуальная память, потребляемая процессом, - это общая сумма всего, что находится на карте памяти процесса. Это включает в себя данные (например, кучу Java), но и все динамические библиотеки и отображенные в память файлы используются в программе. В Linux вы можете использовать pmap команда, чтобы увидеть все вещи, отображенные в пространство процесса (отсюда я буду ссылаться только на Linux, потому что это то, что я использую; я уверен, что есть эквивалентные инструменты для Windows). Вот отрывок из карты памяти программы "Hello World"; вся карта памяти имеет длину более 100 строк, и нет ничего необычного в том, чтобы иметь список из тысячи строк.
A краткое объяснение формата: каждая строка начинается с адреса виртуальной памяти сегмента. За этим следует размер сегмента, разрешения и источник сегмента. Этот последний элемент является либо файлом, либо "anon", что указывает на блок памяти, выделенный через вызов mmap.
начиная с самого верха, у нас есть
- загрузчик JVM (т. е. программа, которая запускается при вводе java ). Это очень мало; все, что он делает, это загрузка в общий библиотеки, где хранится реальный код JVM.
- куча блоков anon, содержащих кучу Java и внутренние данные. Это Sun JVM, поэтому куча разбивается на несколько поколений, каждое из которых является собственным блоком памяти. Обратите внимание, что JVM выделяет пространство виртуальной памяти на основе -Xmx значение; это позволяет ему иметь непрерывную кучу. The -Xms value используется внутренне, чтобы сказать, сколько кучи "используется" при запуске программы и запускать сборку мусора как это предела.
- сопоставленный с памятью JARfile, в этом случае файл, содержащий " классы JDK."Когда вы составляете карту памяти банки, вы можете получить доступ к файлам внутри нее очень эффективно (по сравнению с чтением с самого начала каждый раз). Sun JVM будет сопоставлять все банки на пути к классам; если вашему коду приложения нужно получить доступ к банку, вы также можете сопоставить его с памятью.
- данные по каждому потоку для двух потоков. Блок 1M-это стек потоков; я не знаю, что входит в блок 4K. Для реального приложения вы увидите десятки, если не сотни этих записей, повторяющихся через карту памяти.
- одна из общих библиотек, которая содержит фактический код JVM. Их несколько.
- общая библиотека для стандартной библиотеки языка Си. Это всего лишь одна из многих вещей, которые загружает JVM, которые не являются строго частью Java.
общие библиотеки особенно интересны: каждая общая библиотека имеет по крайней мере два сегмента: только для чтения сегмент, содержащий код библиотеки, и сегмент чтения и записи, содержащий глобальные данные для каждого процесса для библиотеки (я не знаю, что такое сегмент без разрешений; я видел его только на x64 Linux). Часть библиотеки, доступная только для чтения, может совместно использоваться всеми процессами, использующими библиотеку; например, libc имеет 1,5 м пространства виртуальной памяти, которое может быть общим.
карта виртуальной памяти содержит много вещей. Некоторые из них доступны только для чтения, некоторые из них являются общими, а некоторые из них выделяются, но никогда не затрагиваются (например, почти все 4GB кучи в этом примере). Но операционная система достаточно умна, чтобы загружать только то, что ей нужно, поэтому размер виртуальной памяти в значительной степени не имеет значения.
где важен размер виртуальной памяти, это если вы работаете в 32-разрядной операционной системе, где вы можете выделить только 2 ГБ (или, в некоторых случаях, 3 ГБ) адресного пространства процесса. В таком случае вы имеете дело с редким и, возможно, придется пойти на компромисс, например, уменьшить размер кучи, чтобы отобразить в памяти большой файл или создать много потоков.
но, учитывая, что 64-битные машины вездесущи, я не думаю, что пройдет много времени, прежде чем размер виртуальной памяти станет совершенно неуместной статистикой.
размер резидентного набора - это та часть пространства виртуальной памяти, которая фактически находится в ОЗУ. Если ваш RSS растет, чтобы быть значительным часть вашей общей физической памяти, это может быть время, чтобы начать беспокоиться. Если RSS занимает всю вашу физическую память, а система начинает меняться местами, давно пора начать беспокоиться.
но RSS также вводит в заблуждение, особенно на слегка загруженной машине. Операционная система не тратит много усилий на восстановление страниц, используемых процессом. При этом мало пользы, и потенциал для дорогостоящей ошибки страницы, если процесс касается страницы в будущем. В результате статистика RSS может включать множество страниц, которые не используются активно.
если вы не меняетесь местами, не слишком беспокоиться о том, что различные статистики памяти говорят вам. С оговоркой, что постоянно растущий RSS может указывать на какую-то утечку памяти.
С Java-программой гораздо важнее обратить внимание на то, что происходит в куче. Общий объем пространства потребление важно, и есть некоторые шаги, которые вы можете предпринять, чтобы уменьшить это. Более важным является количество времени, которое вы потратите на сборку мусора, и какие части кучи собираются.
доступ к диску (т. е. базе данных) стоит дорого, а память дешевая. Если вы можете обменять одно на другое, сделайте это.
существует известная проблема с Java и glibc >= 2.10 (включая Ubuntu >= 10.04, RHEL >= 6).
лекарство это ОКР. переменная: export MALLOC_ARENA_MAX=4 Если вы используете Tomcat, вы можете добавить это .
резидентной памяти был известен ползучести аналогично утечка памяти или фрагментация памяти.
поиск MALLOC_ARENA_MAX на Гугле или более ссылок.
вы можете настроить и другие параметры malloc для оптимизации низкой фрагментации выделенной памяти:
объем памяти, выделенный для процесса Java, в значительной степени соответствует тому, что я ожидал бы. У меня были аналогичные проблемы с запуском Java на встроенных/ограниченных системах памяти. Бег!--1 - >любой приложение с произвольными ограничениями VM или на системах, которые не имеют достаточного количества свопа, как правило, ломаются. Похоже, это характер многих современных приложений, которые не предназначены для использования в системах с ограниченными ресурсами.
У вас есть несколько вариантов вы можете попробовать и ограничить След памяти JVM. Это может уменьшить объем виртуальной памяти:
- XX: ReservedCodeCacheSize=32M размер кэша зарезервированного кода (в байтах) - максимум размер кэша кода. [Solaris 64-разрядная версия, amd64, и-сервер x86: 48m; в 1.5.0_06 и более ранних версий, Solaris 64-бит и and64: 1024m.]
- XX: MaxPermSize=64m размер постоянного поколения. [5.0 и новее: 64-битные VMs масштабируются на 30% больше; 1.4 для amd64: 96М; 1.3.1 -клиент: 32m.]
кроме того, вы также должны установить-Xmx (максимальный размер кучи) как можно ближе к фактическое пиковое использование памяти приложения. Я считаю, что поведение по умолчанию JVM по-прежнему двойной размер кучи каждый раз, когда он расширяет его до максимального. Если вы начнете с кучи 32M и ваше приложение достигнет пика 65M, то куча будет расти 32M - > 64M- > 128M.
вы также можете попробовать это сделать ВМ менее агрессивно о росте кучи:
- XX: MinHeapFreeRatio=40 минимальный процент кучи свободной после GC к избегайте расширения.
кроме того, из того, что я помню из экспериментов с этим несколько лет назад, количество загруженных собственных библиотек оказало огромное влияние на минимальный след. Загрузка java.сеть.Сокет добавил более 15M, если я правильно помню (и я, вероятно, не).
Sun JVM требует много памяти для HotSpot, и она сопоставляется в библиотеках времени выполнения в общей памяти.
Если проблема с памятью, рассмотрите возможность использования другой JVM, подходящей для встраивания. IBM имеет j9, и есть открытый исходный код "jamvm", который использует библиотеки GNU classpath. Также У Sun есть писк JVM, работающий на солнечных пятнах, поэтому есть альтернативы.
просто мысль, но вы можете проверить влияние a ulimit -v опции.
это не фактическое решение, так как оно ограничит адресное пространство, доступное для все процесс, но это позволит вам проверить поведение вашего приложения с ограниченной виртуальной памятью.
одним из способов уменьшения кучи системы с ограниченными ресурсами может быть игра с переменной-XX: MaxHeapFreeRatio. Это обычно устанавливается в 70 и является максимальным процентом кучи, которая свободна до того, как GC сжимает ее. Установив его на более низкое значение, вы увидите, например, в профилировщике jvisualvm, что для вашей программы обычно используется меньшая куча.
EDIT: чтобы установить небольшие значения для-XX: MaxHeapFreeRatio, вы также должны установить-XX:MinHeapFreeRatio Например
EDIT2: добавлен пример для реального приложения, которое запускается и выполняет ту же задачу, один с параметрами по умолчанию и один с 10 и 25 в качестве параметров. Я не заметил никакой реальной разницы в скорости, хотя Java теоретически должна использовать больше времени для увеличения кучи в последнем примере.
в конце, максимальная куча 905, используемая куча 378
в конце, максимальная куча 722, используемая куча 378
это на самом деле имеет некоторые inpact, как наше приложение работает на сервере удаленного рабочего стола, и многие пользователи могут запустить его сразу.
java 1.4 Sun имеет следующие аргументы для управления размером памяти:
-Xmsn Укажите начальный размер пула распределения памяти в байтах. Это значение должно быть кратно 1024 больше чем 1МБ. Добавить букву k или K, чтобы указать килобайты, или m или M для обозначения мегабайт. Неисполнение значения 2Мб. Примеры:
- Xmxn Укажите максимальный размер пула распределения памяти в байтах. Это значение должно быть кратно 1024 больше чем 2Мб. Добавить букву k или K, чтобы указать килобайты, или m или M для обозначения мегабайт. Неисполнение значение 64Мб. Примеры:
нет, вы не можете настроить объем памяти, необходимый виртуальной машине. Однако обратите внимание, что это виртуальная память, а не резидент, поэтому она просто остается там без вреда, если не используется.
Alernatively, вы можете попробовать другую JVM, а затем Sun one, с меньшим объемом памяти, но я не могу посоветовать здесь.
Для работы любого приложения требуется память. Однако память компьютера ограничена. Поэтому важно ее очищать от старых неиспользуемых данных, чтобы освободить место для новых.
Кто занимается этой очисткой? Как и когда очищается память? Как выглядит структура памяти? Давайте разберем с этим подробнее.
Структура памяти Java
Память в Java состоит из следующих областей:
Структура памяти Java
Native Memory — вся доступная системная память.
Heap (куча) — часть native memory, выделенная для кучи. Здесь JVM хранит объекты. Это общее пространство для всех потоков приложения. Размер этой области памяти настраивается с помощью параметра -Xms (минимальный размер) и -Xmx (максимальный размер).
Stack (стек) — используется для хранения локальных переменных и стека вызовов метода. Для каждого потока выделяется свой стек.
Metaspace (метаданные) — в этой памяти хранятся метаданные классов и статические переменные. Это пространство также является общими для всех. Так как metaspace является частью native memory, то его размер зависит от платформы. Верхний предел объема памяти, используемой для metaspace, можно настроить с помощью флага MaxMetaspaceSize.
PermGen (Permanent Generation, постоянное поколение) присутствовало до Java 7. Начиная с Java 8 ему на смену пришла область Metaspace.
CodeCache (кэш кода) — JIT-компилятор компилирует часто исполняемый код, преобразует его в нативный машинный код и кеширует для более быстрого выполнения. Это тоже часть native memory.
Сборка мусора: введение
Что такое "мусор"? Мусором считается объект, который больше не может быть достигнут по ссылке из какого-либо объекта. Поскольку такие объекты больше не используются в приложении, то их можно удалить из памяти.
Например, на диаграмме ниже объект fruit2 может быть удален из памяти, поскольку на него нет ссылок.
Мусор
Что такое сборка мусора? Сборка мусора — это процесс автоматического управления памятью. Освобождение памяти (путем очистки мусора) выполняется автоматически специальным компонентом JVM — сборщиком мусора (Garbage Collector, GC). Нам, как программистам, нет необходимости вмешиваться в процесс сборки мусора.
Сборка мусора: процесс
Для сборки мусора используется алгоритм пометок (Mark & Sweep). Этот алгоритм состоит из трех этапов:
Mark (маркировка). На первом этапе GC сканирует все объекты и помечает живые (объекты, которые все еще используются). На этом шаге выполнение программы приостанавливается. Поэтому этот шаг также называется "Stop the World" .
Sweep (очистка). На этом шаге освобождается память, занятая объектами, не отмеченными на предыдущем шаге.
Compact (уплотнение). Объекты, пережившие очистку, перемещаются в единый непрерывный блок памяти. Это уменьшает фрагментацию кучи и позволяет проще и быстрее размещать новые объекты.
Поколения объектов
Что такое поколения объектов?
Для оптимизации сборки мусора память кучи дополнительно разделена на четыре области. В эти области объекты помещаются в зависимости от их возраста (как долго они используются в приложении).
Young Generation (молодое поколение). Здесь создаются новые объекты. Область young generation разделена на три части раздела: Eden (Эдем), S0 и S1 (Survivor Space — область для выживших).
Old Generation (старое поколение). Здесь хранятся давно живущие объекты.
Что такое Stop the World?
Когда запускается этап mark, работа приложения останавливается. После завершения mark приложение возобновляет свою работу. Любая сборка мусора — это "Stop the World".
Что такое гипотеза о поколениях?
Как уже упоминалось ранее, для оптимизации этапов mark и sweep используются поколения. Гипотеза о поколениях говорит о следующем:
Большинство объектов живут недолго.
Если объект выживает, то он, скорее всего, будет жить вечно.
Этапы mark и sweep занимают меньше времени при большом количестве мусора. То есть маркировка будет происходить быстрее, если анализируемая область небольшая и в ней много мертвых объектов.
Таким образом, алгоритм сборки мусора, использующий поколения, выглядит следующим образом:
Новые объекты создаются в области Eden. Области Survivor (S0, S1) на данный момент пустые.
Когда область Eden заполняется, происходит минорная сборка мусора (Minor GC). Minor GC — это процесс, при котором операции mark и sweep выполняются для young generation (молодого поколения).
После Minor GC живые объекты перемещаются в одну из областей Survivor (например, S0). Мертвые объекты полностью удаляются.
По мере работы приложения пространство Eden заполняется новыми объектами. При очередном Minor GC области young generation и S0 очищаются. На этот раз выжившие объекты перемещаются в область S1, и их возраст увеличивается (отметка о том, что они пережили сборку мусора).
При следующем Minor GC процесс повторяется. Однако на этот раз области Survivor меняются местами. Живые объекты перемещаются в S0 и у них увеличивается возраст. Области Eden и S1 очищаются.
Объекты между областями Survivor копируются определенное количество раз (пока не переживут определенное количество Minor GC) или пока там достаточно места. Затем эти объекты копируются в область Old.
Major GC. При Major GC этапы mark и sweep выполняются для Old Generation. Major GC работает медленнее по сравнению с Minor GC, поскольку старое поколение в основном состоит из живых объектов.
Преимущества использования поколений
Minor GC происходит в меньшей части кучи (~ 2/3 от кучи). Этап маркировки эффективен, потому что область небольшая и состоит в основном из мертвых объектов.
Недостатки использования поколений
В каждый момент времени одно из пространств Survivor (S0 или S1) пустое и не используется.
Сборка мусора: флаги
В этом разделе приведены некоторые важные флаги, которые можно использовать для настройки процесса сборки мусора.
Флаг
Описание
Первоначальный размер кучи
Максимальный размер куча
Отношение размера Old Generation к Young Generation
Отношение размера Eden к Survivor
Возраст объекта, когда объект перемещается из области Survivor в область Old Generation
Типы сборщиков мусора
Сборщик мусора
Описание
Преимущества
Когда использовать
Флаги для включения
Использует один поток.
Эффективный, т.к. нет накладных расходов на взаимодействие потоков.
Работа с небольшими наборами данных.
Использует несколько потоков.
Многопоточность ускоряет сборку мусора.
В приоритете пиковая производительность.
Допустимы паузы при GC в одну секунду и более.
Работа со средними и большими наборами данных.
Для приложений, работающих на многопроцессорном или многопоточном оборудовании.
Выполняет некоторую тяжелую работу параллельно с работой приложения.
Может использоваться как на небольших системах, так и на больших с большим количеством процессоров и большим количеством памяти.
Когда время отклика важнее пропускной способности.
Паузы GC должны быть меньше одной секунды.
Выполняет всю тяжелую работу параллельно с работой приложения.
В приоритете время отклика.
Сборщики мусора в Java
Инструменты мониторинга GC
Что мониторить?
Частота запуска сборки мусора. Так как GC вызывает "stop the world", поэтому чем время сборки мусора меньше, тем лучше.
Длительность одного цикла сборки мусора.
Как мониторить сборщик мусора?
Для мониторинга можно использовать следующие инструменты:
Для включения логирования событий сборщика мусора добавьте следующие параметры JVM:
Это глубокое погружение в управление памятью Java позволит расширить ваши знания о том, как работает куча, ссылочные типы и сборка мусора.
Вероятно, вы могли подумать, что если вы программируете на Java, то вам незачем знать о том, как работает память. В Java есть автоматическое управление памятью, красивый и тихий сборщик мусора, который работает в фоновом режиме для очистки неиспользуемых объектов и освобождения некоторой памяти.
Поэтому вам, как программисту на Java, не нужно беспокоиться о таких проблемах, как уничтожение объектов, поскольку они больше не используются. Однако, даже если в Java этот процесс выполняется автоматически, он ничего не гарантирует. Не зная, как устроен сборщик мусора и память Java, вы можете создать объекты, которые не подходят для сбора мусора, даже если вы их больше не используете.
Поэтому важно знать, как на самом деле работает память в Java, поскольку это дает вам преимущество в написании высокопроизводительных и оптимизированных приложений, которые никогда не будут аварийно завершены с ошибкой OutOfMemoryError . С другой стороны, когда вы окажетесь в плохой ситуации, вы сможете быстро найти утечку памяти.
Для начала давайте посмотрим, как обычно организована память в Java:
Структура памяти
Обычно память делится на две большие части: стек и куча. Имейте в виду, что размер типов памяти на этом рисунке не пропорционален реальному размеру памяти. Куча - это огромный объем памяти по сравнению со стеком.
Стек (Stack)
Стековая память отвечает за хранение ссылок на объекты кучи и за хранение типов значений (также известных в Java как примитивные типы), которые содержат само значение, а не ссылку на объект из кучи.
Кроме того, переменные в стеке имеют определенную видимость, также называемую областью видимости. Используются только объекты из активной области. Например, предполагая, что у нас нет никаких глобальных переменных (полей) области видимости, а только локальные переменные, если компилятор выполняет тело метода, он может получить доступ только к объектам из стека, которые находятся внутри тела метода. Он не может получить доступ к другим локальным переменным, так как они не выходят в область видимости. Когда метод завершается и возвращается, верхняя часть стека выталкивается, и активная область видимости изменяется.
Возможно, вы заметили, что на картинке выше отображено несколько стеков памяти. Это связано с тем, что стековая память в Java выделяется для каждого потока. Следовательно, каждый раз, когда поток создается и запускается, он имеет свою собственную стековую память и не может получить доступ к стековой памяти другого потока.
Куча (Heap)
Эта часть памяти хранит в памяти фактические объекты, на которые ссылаются переменные из стека. Например, давайте проанализируем, что происходит в следующей строке кода:
Ключевое слово new несет ответственность за обеспечение того, достаточно ли свободного места на куче, создавая объект типа StringBuilder в памяти и обращаясь к нему через «Builder» ссылки, которая попадает в стек.
Для каждого запущенного процесса JVM существует только одна область памяти в куче. Следовательно, это общая часть памяти независимо от того, сколько потоков выполняется. На самом деле структура кучи немного отличается от того, что показано на картинке выше. Сама куча разделена на несколько частей, что облегчает процесс сборки мусора.
Максимальные размеры стека и кучи не определены заранее - это зависит от работающей JVM машины. Позже в этой статье мы рассмотрим некоторые конфигурации JVM, которые позволят нам явно указать их размер для запускаемого приложения.
Типы ссылок
Если вы внимательно посмотрите на изображение структуры памяти, вы, вероятно, заметите, что стрелки, представляющие ссылки на объекты из кучи, на самом деле относятся к разным типам. Это потому, что в языке программирования Java используются разные типы ссылок: сильные, слабые, мягкие и фантомные ссылки. Разница между типами ссылок заключается в том, что объекты в куче, на которые они ссылаются, имеют право на сборку мусора по различным критериям. Рассмотрим подробнее каждую из них.
1. Сильная ссылка
Это самые популярные ссылочные типы, к которым мы все привыкли. В приведенном выше примере со StringBuilder мы фактически храним сильную ссылку на объект из кучи. Объект в куче не удаляется сборщиком мусора, пока на него указывает сильная ссылка или если он явно доступен через цепочку сильных ссылок.
2. Слабая ссылка
Попросту говоря, слабая ссылка на объект из кучи, скорее всего, не сохранится после следующего процесса сборки мусора. Слабая ссылка создается следующим образом:
Хорошим вариантом использования слабых ссылок являются сценарии кеширования. Представьте, что вы извлекаете некоторые данные и хотите, чтобы они также были сохранены в памяти - те же данные могут быть запрошены снова. С другой стороны, вы не уверены, когда и будут ли эти данные запрашиваться снова. Таким образом, вы можете сохранить слабую ссылку на него, и в случае запуска сборщика мусора, возможно, он уничтожит ваш объект в куче. Следовательно, через некоторое время, если вы захотите получить объект, на который вы ссылаетесь, вы можете внезапно получить null значение. Хорошей реализацией сценариев кеширования является коллекция WeakHashMap . Если мы откроем WeakHashMap класс в Java API, мы увидим, что его записи фактически расширяют WeakReference класс и используют его поле ref в качестве ключа отображения ( Map) :
После сбора мусора ключа из WeakHashMap вся запись удаляется из карты.
3. Мягкая ссылка
Эти типы ссылок используются для более чувствительных к памяти сценариев, поскольку они будут собираться сборщиком мусора только тогда, когда вашему приложению не хватает памяти. Следовательно, пока нет критической необходимости в освобождении некоторого места, сборщик мусора не будет касаться легко доступных объектов. Java гарантирует, что все объекты, на которые имеются мягкие ссылки, будут очищены до того, как будет выдано исключение OutOfMemoryError . В документации Javadocs говорится, что «все мягкие ссылки на мягко достижимые объекты гарантированно очищены до того, как виртуальная машина выдаст OutOfMemoryError».
Подобно слабым ссылкам, мягкая ссылка создается следующим образом:
4. Фантомная ссылка
Используется для планирования посмертных действий по очистке, поскольку мы точно знаем, что объекты больше не живы. Используется только с очередью ссылок, поскольку .get() метод таких ссылок всегда будет возвращаться null . Эти типы ссылок считаются предпочтительными для финализаторов.
Ссылки на String
Ссылки на тип String в Java обрабатываются немного по- другому. Строки неизменяемы, что означает, что каждый раз, когда вы делаете что-то со строкой, в куче фактически создается другой объект. Для строк Java управляет пулом строк в памяти. Это означает, что Java сохраняет и повторно использует строки, когда это возможно. В основном это верно для строковых литералов. Например:
При запуске этот код распечатывает следующее:
Strings are equal
Следовательно, оказывается, что две ссылки типа String на одинаковые строковые литералы фактически указывают на одни и те же объекты в куче. Однако это не действует для вычисляемых строк. Предположим, что у нас есть следующее изменение в строке // 1 приведенного выше кода.
Strings are different
В этом случае мы фактически видим, что у нас есть два разных объекта в куче. Если учесть, что вычисляемая строка будет использоваться довольно часто, мы можем заставить JVM добавить ее в пул строк, добавив .intern() метод в конец вычисляемой строки:
При добавлении вышеуказанного изменения создается следующий результат:
Процесс сборки мусора
Как обсуждалось ранее, в зависимости от типа ссылки, которую переменная из стека содержит на объект из кучи, в определенный момент времени этот объект становится подходящим для сборщика мусора.
Объекты, подходящие для сборки мусора
Например, все объекты, отмеченные красным цветом, могут быть собраны сборщиком мусора. Вы можете заметить, что в куче есть объект, который имеет строгие ссылки на другие объекты, которые также находятся в куче (например, это может быть список, который имеет ссылки на его элементы, или объект, имеющий два поля типа, на которые есть ссылки). Однако, поскольку ссылка из стека потеряна, к ней больше нельзя получить доступ, так что это тоже мусор.
Чтобы углубиться в детали, давайте сначала упомянем несколько вещей:
Этот процесс запускается автоматически Java, и Java решает, запускать или нет этот процесс.
На самом деле это дорогостоящий процесс. При запуске сборщика мусора все потоки в вашем приложении приостанавливаются (в зависимости от типа GC, который будет обсуждаться позже).
На самом деле это более сложный процесс, чем просто сбор мусора и освобождение памяти.
Несмотря на то, что Java решает, когда запускать сборщик мусора, вы можете явно вызвать System.gc() и ожидать, что сборщик мусора будет запускаться при выполнении этой строки кода, верно?
Это ошибочное предположение.
Вы только как бы просите Java запустить сборщик мусора, но, опять же, Java решать, делать это или нет. В любом случае явно вызывать System.gc() не рекомендуется.
Поскольку это довольно сложный процесс и может повлиять на вашу производительность, он реализован разумно. Для этого используется так называемый процесс «Mark and Sweep». Java анализирует переменные из стека и «отмечает» все объекты, которые необходимо поддерживать в рабочем состоянии. Затем все неиспользуемые объекты очищаются.
Так что на самом деле Java не собирает мусор. Фактически, чем больше мусора и чем меньше объектов помечены как живые, тем быстрее идет процесс. Чтобы сделать это еще более оптимизированным, память кучи на самом деле состоит из нескольких частей. Мы можем визуализировать использование памяти и другие полезные вещи с помощью JVisualVM, инструмента, поставляемого с Java JDK. Единственное, что вам нужно сделать, это установить плагин с именем Visual GC, который позволяет увидеть, как на самом деле структурирована память. Давайте немного увеличим масштаб и разберем общую картину:
Поколения памяти кучи
Когда объект создается, он размещается в пространстве Eden (1). Поскольку пространство Eden не такое уж большое, оно заполняется довольно быстро. Сборщик мусора работает в пространстве Eden и помечает объекты как живые.
Если объект выживает в процессе сборки мусора, он перемещается в так называемое пространство выжившего S0(2). Во второй раз, когда сборщик мусора запускается в пространстве Eden, он перемещает все уцелевшие объекты в пространство S1(3). Кроме того, все, что в настоящее время находится на S0(2), перемещается в пространство S1(3).
Если объект выживает в течение X раундов сборки мусора (X зависит от реализации JVM, в моем случае это 8), скорее всего, он выживет вечно и перемещается в пространство Old(4).
Принимая все сказанное выше, если вы посмотрите на график сборщика мусора (6), каждый раз, когда он запускается, вы можете увидеть, что объекты переключаются на пространство выживших и что пространство Эдема увеличивалось. И так далее. Старое поколение также может быть обработано сборщиком мусора, но, поскольку это большая часть памяти по сравнению с пространством Eden, это происходит не так часто. Метапространство (5) используется для хранения метаданных о ваших загруженных классах в JVM.
Представленное изображение на самом деле является приложением Java 8. До Java 8 структура памяти была немного другой. Метапространство на самом деле называется PermGen область. Например, в Java 6 это пространство также хранит память для пула строк. Поэтому, если в вашем приложении Java 6 слишком много строк, оно может аварийно завершить работу.
Типы сборщиков мусора
Фактически, JVM имеет три типа сборщиков мусора, и программист может выбрать, какой из них следует использовать. По умолчанию Java выбирает используемый тип сборщика мусора в зависимости от базового оборудования.
1. Serial GC (Последовательный сборщик мусора) - однониточный коллектор. В основном относится к небольшим приложениям с небольшим использованием данных. Можно включить, указав параметр командной строки: -XX:+UseSerialGC.
2. Parallel GC (Параллельный сборщик мусора) - даже по названию, разница между последовательным и параллельным будет заключаться в том, что параллельный сборщик мусора использует несколько потоков для выполнения процесса сбора мусора. Этот тип GC также известен как сборщик производительности. Его можно включить, явно указав параметр: -XX:+UseParallelGC.
3. Mostly concurrent GC (В основном параллельный сборщик мусора). Если вы помните, ранее в этой статье упоминалось, что процесс сбора мусора на самом деле довольно дорогостоящий, и когда он выполняется, все потоки приостанавливаются. Однако у нас есть в основном параллельный тип GC, который утверждает, что он работает одновременно с приложением. Однако есть причина, по которой он «в основном» параллелен. Он не работает на 100% одновременно с приложением. Есть период времени, на который цепочки приостанавливаются. Тем не менее, пауза делается как можно короче для достижения наилучшей производительности сборщика мусора. На самом деле существует 2 типа в основном параллельных сборщиков мусора:
3.1 Garbage First - высокая производительность с разумным временем паузы приложения. Включено с опцией: -XX:+UseG1GC.
3.2 Concurrent Mark Sweep (Параллельное сканирование отметок) - время паузы приложения сведено к минимуму. Он может быть использован с помощью опции: -XX:+UseConcMarkSweepGC . Начиная с JDK 9, этот тип GC объявлен устаревшим.
Советы и приемы
Чтобы минимизировать объем памяти, максимально ограничьте область видимости переменных. Помните, что каждый раз, когда выскакивает верхняя область видимости из стека, ссылки из этой области теряются, и это может сделать объекты пригодными для сбора мусора.
Явно устанавливайте в null устаревшие ссылки. Это сделает объекты, на которые ссылаются, подходящими для сбора мусора.
Избегайте финализаторов (finalizer). Они замедляют процесс и ничего не гарантируют. Фантомные ссылки предпочтительны для работы по очистке памяти.
Не используйте сильные ссылки там, где можно применить слабые или мягкие ссылки. Наиболее распространенные ошибки памяти - это сценарии кэширования, когда данные хранятся в памяти, даже если они могут не понадобиться.
JVisualVM также имеет функцию создания дампа кучи в определенный момент, чтобы вы могли анализировать для каждого класса, сколько памяти он занимает.
Настройте JVM в соответствии с требованиями вашего приложения. Явно укажите размер кучи для JVM при запуске приложения. Процесс выделения памяти также является дорогостоящим, поэтому выделите разумный начальный и максимальный объем памяти для кучи. Если вы знаете его, то не имеет смысла начинать с небольшого начального размера кучи с самого начала, JVM расширит это пространство памяти. Указание параметров памяти выполняется с помощью следующих параметров:
Начальный размер кучи -Xms512m - установите начальный размер кучи на 512 мегабайт.
Максимальный размер кучи -Xmx1024m - установите максимальный размер кучи 1024 мегабайта.
Размер стека потоков -Xss1m - установите размер стека потоков равным 1 мегабайту.
Размер поколения -Xmn256m - установите размер поколения 256 мегабайт.
Если приложение Java выдает ошибку OutOfMemoryError и вам нужна дополнительная информация для обнаружения утечки, запустите процесс с –XX:HeapDumpOnOutOfMemory параметром, который создаст файл дампа кучи, когда эта ошибка произойдет в следующий раз.
Используйте опцию -verbose:gc , чтобы получить вывод процесса сборки мусора. Каждый раз, когда происходит сборка мусора, будет генерироваться вывод.
Заключение
Знание того, как организована память, дает вам преимущество в написании хорошего и оптимизированного кода с точки зрения ресурсов памяти. Преимущество заключается в том, что вы можете настроить свою работающую JVM, предоставив различные конфигурации, наиболее подходящие для запуска вашего приложения. Выявление и устранение утечек памяти - это очень просто, если использовать правильные инструменты.
Наверняка вы уже читали не один обзор механизмов сборки мусора в Java и настройка таких опций, как Xmx и Xms, превратилась для вас в обычную рутину. Но действительно ли вы в деталях понимаете, что происходит под капотом вашей виртуальной машины в тот момент, когда приходит время избавиться от ненужных объектов в памяти и ваш идеально оптимизированный метод начинает выполняться в несколько раз дольше положенного? И знаете ли вы, какие возможности предоставляют вам последние версии Java для оптимизации ответственной работы по сборке мусора, зачастую сильно влияющей на производительность вашего приложения?
Попробуем в нескольких статьях пройти путь от описания базовых идей, лежащих в основе всех сборщиков мусора, до разбора алгоритмов работы и возможностей тонкой настройки различных сборщиков Java HotSpot VM (вы ведь знаете, что таких сборщиков четыре?). И самое главное, рассмотрим, каким образом эти знания можно использовать на практике.
Следует сразу оговориться, что все сказанное ниже относится к виртуальной машине HotSpot. Так что если вы встречаете в тексте упоминание JVM, то речь идет именно об этой реализации. Но базовые принципы распространяются и на виртуальные машины других поставщиков, хотя в некоторых деталях они могут отличаться.
Резонный вопрос. Далеко не любой программе для беспроблемной работы требуется тонкая настройка сборщика мусора. Очень часто выделения ей необходимого объема памяти оказывается достаточным. В конце концов, редкий пользователь заметит, что отклик программы время от времени занимает на сотню-другую миллисекунд дольше обычного.
Прежде чем приступить непосредственно к решению вопросов очистки наших Авгиевых конюшен, давайте разберемся с их общим устройством и определимся, на чем конкретно нам хотелось бы сосредоточиться.
JVM разделяет используемую ею память на две области: куча (heap), в которой хранятся данные приложения, и не-куча (non-heap), в которой хранится код программы и другие вспомогательные данные.
Если ваше приложение при работе самостоятельно не генерирует новые классы и не занимается постоянной подгрузкой / выгрузкой классов, то состояние non-heap в долгосрочной перспективе будет близким к статичному и мало поддающимся оптимизации. В связи с этим, механизмы функционирования области non-heap мы здесь рассматривать не будем, а сосредоточимся на той области, где наши усилия принесут наибольшую выгоду.
Все объекты, которые явно или неявно создаются Java-приложением, размещаются в куче. Над оптимизацией размещения объектов и алгоритмами их обработки разработчики языков с автоматической сборкой мусора бьются с первого дня их создания. И как минимум в ближайшем будущем эта битва будет продолжаться, ведь объемы обрабатываемых данных растут, а требования к сборке мусора у различных приложений сильно отличаются, что делает создание единого идеального сборщика не самым тривиальным делом. Наше же дело — следить за развитием ситуации и стараться извлекать из имеющихся инструментов как можно больше пользы.
Преследуя свои цели (которые могут варьироваться и которые мы обязательно рассмотрим ниже), различные сборщики мусора используют разные подходы к организации памяти и ее очистке, но их объединяет общая черта — все они опираются на слабую гипотезу о поколениях. В общем виде, гипотеза о поколениях гласит, что вероятность смерти как функция от возраста снижается очень быстро. Ее приложение к сборке мусора в частности означает, что подавляющее большинство объектов живут крайне недолго. По людским меркам, большинство даже в детский сад не пойдут. Также это означает, что чем дольше прожил объект, тем выше вероятность того, что он будет жить и дальше.
Большинство приложений имеют распределение времен жизни объектов, схематично описываемое примерно такой кривой:
Подавляющее большинство объектов создаются на очень короткое время, они становятся ненужными практически сразу после их первого использования. Итераторы, локальные переменные методов, результаты боксинга и прочие временные объекты, которые зачастую создаются неявно, попадают именно в эту категорию, образуя пик в самом начале графика.
Далее идут объекты, создаваемые для выполнения более-менее долгих вычислений. Их жизнь чуть разнообразнее — они обычно гуляют по различным методам, трансформируясь и обогащаясь в процессе, но после этого становятся ненужными и превращаются в мусор. Благодаря таким объектам возникает небольшой бугорок на графике следом за пиком временных объектов.
И, наконец, объекты-старожилы, переживающие почти всех — это постоянные данные программы, загружаемые часто в самом начале и проживающие долгую и счастливую жизнь до остановки приложения.
Конечно, каждое приложение по-своему уникально, поэтому в каждом конкретном случае этот график будет варьироваться, изменять пропорции, на нем будут появляться аномалии, но чаще всего форма именно такая. Запомните этот график, он нам еще пригодится при выполнении оптимизаций.
Все это навело разработчиков на мысль, что в первую очередь необходимо сосредотачиваться на очистке тех объектов, которые были созданы совсем недавно. Именно среди них чаще всего находится число тех, кто уже отжил свое, и именно здесь можно получить максимум эффекта при минимуме трудозатрат.
Вот тут и возникает идея разделения объектов на младшее поколение (young generation) и старшее поколение (old generation). В соответствии с этим разделением и процессы сборки мусора разделяются на малую сборку (minor GC), затрагивающую только младшее поколение, и полную сборку (full GC), которая может затрагивать оба поколения. Малые сборки выполняются достаточно часто и удаляют основную часть мертвых объектов. Полные сборки выполняются тогда, когда текущий объем выделенной программе памяти близок к исчерпанию и малой сборкой уже не обойтись.
При этом разделение объектов по поколениям не просто условное, они физически размещаются в разных регионах памяти. Объекты из младшего поколения по мере выживания в сборках мусора переходят в старшее поколение. В старшем поколении объект может прожить до окончания работы приложения, либо будет удален в процессе одной из полных сборок мусора.
Интуитивно понятно, что желательно иметь сборщик мусора, который как можно быстрее избавлялся бы от ненужных объектов, расчищая дорогу молодым и обеспечивая тихое и спокойное существование долгожителям. Но работа сборщика мусора не бесплатная, она оплачивается ресурсами компьютера и задержками в выполнении программы. Поэтому прежде чем двигаться дальше, давайте разберемся с критериями, используемыми при оценке сборщиков.
Традиционно, при определении эффективности работы сборщика мусора учитываются следующие факторы:
- Максимальная задержка — максимальное время, на которое сборщик приостанавливает выполнение программы для выполнения одной сборки. Такие остановки называются stop-the-world (или STW).
- Пропускная способность — отношение общего времени работы программы к общему времени простоя, вызванного сборкой мусора, на длительном промежутке времени.
- Потребляемые ресурсы — объем ресурсов процессора и/или дополнительной памяти, потребляемых сборщиком.
Поэтому при настройке сборщиков мусора разработчики обычно фокусируются на оптимизации одного или двух параметров, стараясь сильно не ухудшать остальные, но жертвуя ими в случае необходимости.
Господи, дай мне места для размещения того, что пока еще нужно,
Дай мне смелости удалить то, что больше не пригодится,
И дай мне мудрости, чтобы отличить одно от другого.
— Молитва сборщиков мусора
Еще один важный вопрос, который хотелось бы разобрать прежде, чем двигаться дальше, это определение самого понятия мусора, то есть мертвых объектов.
Как мы уже выяснили выше, путь большинства объектов от момента создания и исполнения своего предназначения до момента превращения в мусор, достаточно короток. Но существуют факторы, которые могут задержать его в мире живых чуть дольше, чем нам того хотелось бы.
Все мы знаем, что считать объект живым просто по факту наличия на него ссылок из других объектов нельзя. В противном случае рецепт бессмертия в JVM был бы до безобразия прост и заключался бы в наличии взаимных ссылок хотя бы у двух объектов друг на друга, а в общем случае — в наличии цикла в графе связанности объектов. При таком подходе и ограниченном объеме памяти более-менее серьезная программа долго не проработала бы, поэтому с отслеживанием циклов в графах объектов JVM справляется хорошо.
Но и просто сказать, что объект мертв и может быть удален только на основании того, что в программе не осталось ссылающихся на него (напрямую или опосредованно) еще используемых объектов, нельзя, так как разделение объектов на поколения вносит свои коррективы.
Рассмотрим такую ситуацию: У нас есть молодой объект A и ссылающийся на него объект B, уже заслуживший место в старшем поколении. В какой-то момент времени оба этих объекта стали нам не нужны и мы обнулили все имеющиеся у нас ссылки на них. Очевидно, объект A можно было бы удалить в ближайшую малую сборку мусора, но для того, чтобы получить это знание, сборщику пришлось бы просмотреть всё старшее поколение и понять, что объект B ссылающийся на A, тоже является мусором, а следовательно их оба можно утилизировать. Но анализ старшего поколения не входит в план малой сборки, так как является относительно дорогой процедурой, поэтому объект А во время малой сборки будет считаться живым.
Таким образом, чаще всего для целей малой сборки мусора объект считается мертвым и подлежащим утилизации, если до него невозможно добраться по ссылкам ни из объектов старшего поколения, ни из так называемых корней (roots), к каковым относятся ссылки из стеков потоков, статические члены классов При полной сборке мусора могут анализироваться оба поколения, поэтому здесь сборщик может плясать только от корней.
Кстати, время от момента, когда объект стал нам не нужен, до момента его фактического удаления из памяти называется проворством (promptness) и иногда рассматривается как дополнительный фактор оценки эффективности сборщика.
Итак, мы уже получили базовые представления о том, чем занимаются сборщики мусора и по каким критериям их можно оценивать. Теперь хотелось бы разобраться, каким образом можно заглянуть внутрь виртуальной машины, чтобы у нас была возможность наблюдать за работой ее скрытых механизмов.
- внутренние, являющиеся частью той программы, которую мы мониторим,
- внешние, подключаемые к процессу исследуемой программы извне.
Внутренние инструменты
Что касается внутренних инструментов мониторинга, то здесь мы можем либо попросить JVM выводить информацию о производимых сборках с различным уровнем детализации (в stdout или в лог-файл), либо самостоятельно обращаться к MXBean’ам, возвращающим информацию о состоянии памяти и о выполняемых сборках мусора, и обрабатывать ее как нам вздумается.
В JVM HotSpot доступны следующие опции, управляющие выводом информации о сборках мусора (это основные опции, работающие для всех сборщиков):
Включает режим логирования сборок мусора в stdout. |
Указывает имя файла, в который должна логироваться информация о сборках мусора. Имеет приоритет над -verbose:gc. |
Добавляет к информации о сборках временные метки (в виде количества секунд, прошедших с начала работы программы). |
Включает расширенный вывод информации о сборках мусора. |
При старте приложения выводит в stdout значения всех опций, заданных явно или установленных самой JVM. Сюда же попадают опции, относящиеся к сборке мусора. Часто бывает полезно посмотреть на присвоенные им значения. |
Если вы хотите собирать данные из своего приложения самостоятельно, то для этого можно использовать соответствующие MXBean’ы. Вот пример простого класса, который позволяет выводить текущее состояние различных регионов памяти, а также информацию о сборках мусора, его можно взять за основу, если хотите разработать свой собственный мониторинг:
Внешние инструменты
В природе существует огромное количество инструментов, позволяющих подключиться к процессу Java и в удобном виде получить информацию о состоянии памяти и процессах сборки мусора. Это и входящие в поставку JVM HotSpot утилиты VisualVM (с плагином VisualGC) и Java Mission Control и различные инструменты/плагины для IDE и отдельные программы вроде JProfiler или YourKit и еще много чего.
Вы можете выбрать то, чем вам удобнее пользоваться, но как уже было сказано выше, обязательно проверьте, какое влияние оказывает ваш инструмент и его настройки на подопытное приложение. Вот пример того, как VisualVM влияет на поведение программы, весь исполняемый код которой состоит из приостановки выполнения основного потока:
Видите этот растущий график в верхней части? Это почти 8 МБ мусорных данных в минуту, привносимых мониторингом. Если вам нужно общее представление о том, как работает сборщик, либо если десяток мегабайт данных в минуту для вашей программы меньше допустимой погрешности измерений, то такое поведение инструменту можно простить. Но если вы проводите тонкую настройку и у вас каждый мегабайт на счету, то лучше выбрать что-нибудь менее прожорливое.
В идеале, ваш инструмент должен отображать график использования памяти коматозной программой как-нибудь так:
Как вариант, обратите внимание на описанные выше внутренние инструменты мониторинга, они изначально легковесные, а при необходимости добавления расширенных функций вы можете сами влиять на их прожорливость.
Ну что ж, раз вы добрались до этого места и вас не остановило даже долгое перечисление очевидных вещей в предыдущих параграфах, то вам и впрямь должно быть интересно. Давайте тогда уже взглянем на то, что же нам предоставляет HotSpot из коробки.
Как уже было сказано, описанные выше принципы сборки мусора являются общими для всех сборщиков. Но при этом между сборщиками существуют и заметные различия, проявляющиеся в ответах на следующие вопросы:
- Какое количество регионов кучи используется, каково их назначение и размеры? Как эти размеры изменяются динамически?
- Как устроен перевод объектов из младшего поколения в старшее?
- Какие из работ по сборке мусора выполняются параллельно с работой основной программы, а какие приводят к ее остановке?
- Каким образом сборщик мусора автоматически подстраивается под требуемые параметры производительности? Каким из них отдает приоритет?
- Какие существуют возможности по настройке сборщика?
Java HotSpot VM предоставляет разработчикам на выбор четыре различных сборщика мусора:
Serial (последовательный) — самый простой вариант для приложений с небольшим объемом данных и не требовательных к задержкам. Редко когда используется, но на слабых компьютерах может быть выбран виртуальной машиной в качестве сборщика по умолчанию.
Parallel (параллельный) — наследует подходы к сборке от последовательного сборщика, но добавляет параллелизм в некоторые операции, а также возможности по автоматической подстройке под требуемые параметры производительности.
Concurrent Mark Sweep (CMS) — нацелен на снижение максимальных задержек путем выполнения части работ по сборке мусора параллельно с основными потоками приложения. Подходит для работы с относительно большими объемами данных в памяти.
Garbage-First (G1) — создан для постепенной замены CMS, особенно в серверных приложениях, работающих на многопроцессорных серверах и оперирующих большими объемами данных.
В следующих статьях мы детально рассмотрим каждый из этих сборщиков, стараясь придерживаться общего плана: краткое описание, принципы работы, ситуации STW (это stop the world, если успели забыть), способы настройки, достоинства и недостатки. Получив эти знания, мы посмотрим, что с ними делать в реальной жизни.
Читайте также: