Исключения java нехватка памяти
Я разрабатываю программу, которая требует огромного количества памяти, и я хочу поймать, когда происходит исключение нехватки памяти. Я слышал, что это невозможно сделать, но мне любопытно, есть ли какие-нибудь разработки в этом направлении.
9 ответы
Ты может поймать его, когда он спускается с Throwable:
Однако, если вы не делаете какие-то довольно специфические вещи (например, выделяете тонны вещей в определенном разделе кода), вы, вероятно, не сможете его поймать, поскольку вы не будете знать, откуда он будет брошен.
@matt b - в конкретном случае, когда вы в состоянии чтобы поймать это, вы, вероятно, пытаетесь контролировать потребление памяти и, таким образом, сможете выпустить часть / все из нее. Но в целом вы, конечно, правы. - ЧссПлы76
@ ChssPly76 (и другие) - прочтите, пожалуйста, мой ответ, чтобы понять, почему попытка управлять потреблением памяти путем перехвата OOME была бы непростой задачей. ПЛОХАЯ ИДЕЯ. - Стивен С
спасибо @ ChssPly76, но Стивен, где нет выбора, поэтому мы используем эту попытку - Лысый bcs из IT
@BaldbcsofIT - Ваше утверждение об отсутствии альтернативы неверно. Всегда есть альтернатива: перезапустить JVM. - Стивен С
@uckelman Это неправильный подход, если ваше приложение не однопоточное. Дело в том, что даже если ваш поток загрузки изображений ловит OOME, но другие потоки не знают об этом и также могут получить OOME - Сурадж Чандран
@SurajChandran: OOME, вызванный попыткой загрузить очень большое изображение, будет происходить из-за попытки выделить очень большой byte[] or int[] . Вы получаете OOME из-за сбоя выделения, а не из-за того, что вам действительно не хватает памяти - следовательно, это не вызовет проблемы для других потоков. - Uckelman
Вы можете поймать и попытаться восстановить исключение OutOfMemoryError (OOM), НО ЭТО ВЕРОЯТНО ПЛОХАЯ ИДЕЯ . особенно если ваша цель состоит в том, чтобы приложение продолжало работать.
Есть ряд причин для этого:
Как отмечали другие, есть лучшие способы управления ресурсами памяти, чем явное освобождение вещей; т.е. использование SoftReference и WeakReference для объектов, которые могут быть освобождены при нехватке памяти.
Если вы дождетесь, пока действительно не закончится память, прежде чем освобождать вещи, ваше приложение, вероятно, потратит больше времени на запуск сборщика мусора. В зависимости от вашей версии JVM и параметров настройки вашего GC, JVM может в конечном итоге запускать GC все чаще и чаще по мере приближения к точке, в которой будет генерироваться OOM. Замедление (с точки зрения приложения, выполняющего полезную работу) может быть значительным. Вероятно, вы захотите этого избежать.
Если основной причиной вашей проблемы является утечка памяти, есть вероятность, что перехват и восстановление из OOM не восстановят утечку памяти. Ваше приложение будет продолжать работать некоторое время, затем снова и снова OOM, снова и снова с уменьшающимися интервалами.
Так что мой совет - НЕ пытаться уходить от OOM . если не являетесь знать:
- где и почему произошло ООМ,
- что не будет никакого "побочного ущерба", и
- что ваше восстановление освободит достаточно памяти для продолжения.
ответ дан 06 мая '20, 15:05
просто бросить это всем тем, кто размышляет, почему кому-то может не хватить памяти: я работаю над проектом, в котором часто не хватает памяти, и мне пришлось реализовать решение для этого.
проект является компонентом приложения для криминалистики и расследования. после сбора данных в полевых условиях (с использованием очень малого объема памяти, кстати) данные открываются в нашем приложении для расследования. одной из функций является выполнение CFG-обхода любого произвольного двоичного изображения, которое было захвачено в полевых условиях (приложения из физической памяти). эти обходы могут занять много времени, но дают очень полезные визуальные представления пройденного двоичного файла.
Чтобы ускорить процесс обхода, мы стараемся хранить как можно больше данных в физической памяти, но структуры данных растут по мере роста двоичного файла, и мы не можем хранить их ВСЕ в памяти (цель состоит в том, чтобы использовать кучу java размером менее 256 м). так что мне делать?
Я создал дисковые версии LinkedLists, Hashtables и т. д., они являются заменой своих аналогов и реализуют все те же интерфейсы, поэтому внешне они выглядят одинаково.
различия? эти замещающие структуры взаимодействуют друг с другом, улавливая ошибки памяти и запрашивая освобождение из памяти наименее недавно использованных элементов из наименее недавно использованной коллекции. освобождение элемента выгружает его на диск во временном файле (в системном временном каталоге) и помечает объекты-заполнители как «выгружаемые» в соответствующей коллекции.
существует МНОЖЕСТВО причин, по которым вам может не хватить памяти в java-приложении - основная причина большинства из этих причин - одна или обе из следующих: 1. Приложение работает на машине с ограниченными ресурсами (или пытается ограничить использование ресурсов путем ограничения размера кучи. ) 2. Приложению просто требуется большой объем памяти (предлагалось редактирование изображений, но как насчет аудио и видео? Как насчет компиляторов, как в моем случае? Как насчет долгосрочных сборщиков данных без энергонезависимого хранилища?)
Если вы словили OutOfMemoryError, то это вовсе не значит, что ваше приложение создает много объектов, которые не могут почиститься сборщиком мусора и заполняют всю память, выделенную вами с помощью параметра -Xmx. Я, как минимум, могу придумать два других случая, когда вы можете увидеть эту ошибку. Дело в том, что память java процесса не ограничивается областью -Xmx, где ваше приложение программно создает объекты.
Область памяти, занимаемая java процессом, состоит из нескольких частей. Тип OutOfMemoryError зависит от того, в какой из них не хватило места.
1. java.lang.OutOfMemoryError: Java heap space
Не хватает место в куче, а именно, в области памяти в которую помещаются объекты, создаваемые программно в вашем приложении. Размер задается параметрами -Xms и -Xmx. Если вы пытаетесь создать объект, а места в куче не осталось, то получаете эту ошибку. Обычно проблема кроется в утечке памяти, коих бывает великое множество, и интернет просто пестрит статьями на эту тему.
2. java.lang.OutOfMemoryError: PermGen space
Данная ошибка возникает при нехватке места в Permanent области, размер которой задается параметрами -XX:PermSize и -XX:MaxPermSize. Что там лежит и как бороться с OutOfMemoryError возникающей там, я уже описал подробнейшим образом тут.
3. java.lang.OutOfMemoryError: GC overhead limit exceeded
Данная ошибка может возникнуть как при переполнении первой, так и второй областей. Связана она с тем, что памяти осталось мало и GC постоянно работает, пытаясь высвободить немного места. Данную ошибку можно отключить с помощью параметра -XX:-UseGCOverheadLimit, но, конечно же, её надо не отключать, а либо решать проблему утечки памяти, либо выделять больше объема, либо менять настройки GC.
4. java.lang.OutOfMemoryError: unable to create new native thread
На самом деле это очень просто воспроизвести на windows на 32-битной машине, так как там процессу выделяется не больше 2Гб.
Допустим у вас есть приложение с большим количеством одновременно работающих пользователей, которое запускается с параметрами -Xmx1024M -XX:MaxPermSize=256M -Xss512K. Если всего процессу доступно 2G, то остается свободным еще коло 768M. Именно в данном остатке памяти и создаются стеки потоков. Таким образом, примерно вы можете создать не больше 768*(1024/512)=1536 (у меня при таких параметрах получилось создать 1316) нитей (см. рисунок в начале статьи), после чего вы получите OutOfMemoryError. Если вы увеличиваете -Xmx, то количество потоков, которые вы можете создать соответственно уменьшается. Вариант с уменьшением -Xss, для возможности создания большего количества потоков, не всегда выход, так как, возможно, у вас существуют в системе потоки требующие довольно больших стеков. Например, поток инициализации или какие-нибудь фоновые задачи. Но все же выход есть. Оказывается при программном создании потока, можно указать размер стека: Thread(ThreadGroup group, Runnable target, String name,long stackSize). Таким образом вы можете выставить -Xss довольно маленьким, а действия требующие больших стеков, выполнять в отдельных потоках, созданных с помощью упомянутого выше конструктора.
Более подробно, что же лежит в стеке потока, и куда уходит эта память, можно прочитать тут.
Конечно, вам может показаться данная проблема слегка надуманной, так как большинство серверов нынче крутиться на 64-битной архитектуре, но все же считаю данный пример весьма полезным, так как он помогает разобраться из каких частей состоит память java-процесса.
Одним из основных преимуществ Java является автоматизированное управление памятью с помощью встроенного сборщика мусора (или сокращенно GC). GC неявно заботится о выделении и освобождении памяти и, таким образом, способен решать большинство проблем, связанных с ее утечкой.
Хотя GC эффективно обрабатывает значительную часть памяти, он не гарантирует надежного решения проблемы с ее утечкой. GC достаточно умен, но не безупречен. Утечки памяти все еще могут закрасться даже в приложения, созданные добросовестным разработчиком.
По-прежнему возможны ситуации, когда приложение создает значительное количество лишних объектов, расходуя ресурсы памяти, что иногда приводит к его полному отказу.
Стек (Stack)
Стековая память отвечает за хранение ссылок на объекты кучи и за хранение типов значений (также известных в Java как примитивные типы), которые содержат само значение, а не ссылку на объект из кучи.
Кроме того, переменные в стеке имеют определенную видимость, также называемую областью видимости. Используются только объекты из активной области. Например, предполагая, что у нас нет никаких глобальных переменных (полей) области видимости, а только локальные переменные, если компилятор выполняет тело метода, он может получить доступ только к объектам из стека, которые находятся внутри тела метода. Он не может получить доступ к другим локальным переменным, так как они не выходят в область видимости. Когда метод завершается и возвращается, верхняя часть стека выталкивается, и активная область видимости изменяется.
Возможно, вы заметили, что на картинке выше отображено несколько стеков памяти. Это связано с тем, что стековая память в Java выделяется для каждого потока. Следовательно, каждый раз, когда поток создается и запускается, он имеет свою собственную стековую память и не может получить доступ к стековой памяти другого потока.
Типы ссылок
Если вы внимательно посмотрите на изображение структуры памяти, вы, вероятно, заметите, что стрелки, представляющие ссылки на объекты из кучи, на самом деле относятся к разным типам. Это потому, что в языке программирования Java используются разные типы ссылок: сильные, слабые, мягкие и фантомные ссылки. Разница между типами ссылок заключается в том, что объекты в куче, на которые они ссылаются, имеют право на сборку мусора по различным критериям. Рассмотрим подробнее каждую из них.
1. Сильная ссылка
Это самые популярные ссылочные типы, к которым мы все привыкли. В приведенном выше примере со StringBuilder мы фактически храним сильную ссылку на объект из кучи. Объект в куче не удаляется сборщиком мусора, пока на него указывает сильная ссылка или если он явно доступен через цепочку сильных ссылок.
2. Слабая ссылка
Попросту говоря, слабая ссылка на объект из кучи, скорее всего, не сохранится после следующего процесса сборки мусора. Слабая ссылка создается следующим образом:
Хорошим вариантом использования слабых ссылок являются сценарии кеширования. Представьте, что вы извлекаете некоторые данные и хотите, чтобы они также были сохранены в памяти - те же данные могут быть запрошены снова. С другой стороны, вы не уверены, когда и будут ли эти данные запрашиваться снова. Таким образом, вы можете сохранить слабую ссылку на него, и в случае запуска сборщика мусора, возможно, он уничтожит ваш объект в куче. Следовательно, через некоторое время, если вы захотите получить объект, на который вы ссылаетесь, вы можете внезапно получить null значение. Хорошей реализацией сценариев кеширования является коллекция WeakHashMap . Если мы откроем WeakHashMap класс в Java API, мы увидим, что его записи фактически расширяют WeakReference класс и используют его поле ref в качестве ключа отображения ( Map) :
После сбора мусора ключа из WeakHashMap вся запись удаляется из карты.
3. Мягкая ссылка
Эти типы ссылок используются для более чувствительных к памяти сценариев, поскольку они будут собираться сборщиком мусора только тогда, когда вашему приложению не хватает памяти. Следовательно, пока нет критической необходимости в освобождении некоторого места, сборщик мусора не будет касаться легко доступных объектов. Java гарантирует, что все объекты, на которые имеются мягкие ссылки, будут очищены до того, как будет выдано исключение OutOfMemoryError . В документации Javadocs говорится, что «все мягкие ссылки на мягко достижимые объекты гарантированно очищены до того, как виртуальная машина выдаст OutOfMemoryError».
Подобно слабым ссылкам, мягкая ссылка создается следующим образом:
4. Фантомная ссылка
Используется для планирования посмертных действий по очистке, поскольку мы точно знаем, что объекты больше не живы. Используется только с очередью ссылок, поскольку .get() метод таких ссылок всегда будет возвращаться null . Эти типы ссылок считаются предпочтительными для финализаторов.
Куча (Heap)
Эта часть памяти хранит в памяти фактические объекты, на которые ссылаются переменные из стека. Например, давайте проанализируем, что происходит в следующей строке кода:
Ключевое слово new несет ответственность за обеспечение того, достаточно ли свободного места на куче, создавая объект типа StringBuilder в памяти и обращаясь к нему через «Builder» ссылки, которая попадает в стек.
Для каждого запущенного процесса JVM существует только одна область памяти в куче. Следовательно, это общая часть памяти независимо от того, сколько потоков выполняется. На самом деле структура кучи немного отличается от того, что показано на картинке выше. Сама куча разделена на несколько частей, что облегчает процесс сборки мусора.
Максимальные размеры стека и кучи не определены заранее - это зависит от работающей JVM машины. Позже в этой статье мы рассмотрим некоторые конфигурации JVM, которые позволят нам явно указать их размер для запускаемого приложения.
Советы и приемы
Чтобы минимизировать объем памяти, максимально ограничьте область видимости переменных. Помните, что каждый раз, когда выскакивает верхняя область видимости из стека, ссылки из этой области теряются, и это может сделать объекты пригодными для сбора мусора.
Явно устанавливайте в null устаревшие ссылки. Это сделает объекты, на которые ссылаются, подходящими для сбора мусора.
Избегайте финализаторов (finalizer). Они замедляют процесс и ничего не гарантируют. Фантомные ссылки предпочтительны для работы по очистке памяти.
Не используйте сильные ссылки там, где можно применить слабые или мягкие ссылки. Наиболее распространенные ошибки памяти - это сценарии кэширования, когда данные хранятся в памяти, даже если они могут не понадобиться.
JVisualVM также имеет функцию создания дампа кучи в определенный момент, чтобы вы могли анализировать для каждого класса, сколько памяти он занимает.
Настройте JVM в соответствии с требованиями вашего приложения. Явно укажите размер кучи для JVM при запуске приложения. Процесс выделения памяти также является дорогостоящим, поэтому выделите разумный начальный и максимальный объем памяти для кучи. Если вы знаете его, то не имеет смысла начинать с небольшого начального размера кучи с самого начала, JVM расширит это пространство памяти. Указание параметров памяти выполняется с помощью следующих параметров:
Начальный размер кучи -Xms512m - установите начальный размер кучи на 512 мегабайт.
Максимальный размер кучи -Xmx1024m - установите максимальный размер кучи 1024 мегабайта.
Размер стека потоков -Xss1m - установите размер стека потоков равным 1 мегабайту.
Размер поколения -Xmn256m - установите размер поколения 256 мегабайт.
Если приложение Java выдает ошибку OutOfMemoryError и вам нужна дополнительная информация для обнаружения утечки, запустите процесс с –XX:HeapDumpOnOutOfMemory параметром, который создаст файл дампа кучи, когда эта ошибка произойдет в следующий раз.
Используйте опцию -verbose:gc , чтобы получить вывод процесса сборки мусора. Каждый раз, когда происходит сборка мусора, будет генерироваться вывод.
5. Заключение
Говоря простым языком, мы можем рассматривать утечку памяти как болезнь, которая снижает производительность нашего приложения, блокируя жизненно важные ресурсы памяти. И, как и все другие болезни, если ее не лечить, со временем она может привести к фатальным сбоям приложения.
Решить проблему утечки памяти непросто, и ее обнаружение требует высокого мастерства и владения языком Java. При борьбе с утечками памяти не существует универсального решения, поскольку они могут возникать из-за множества разнообразных событий.
Однако, если мы будем использовать лучшие практики и регулярно проводить тщательный анализ кода и профилирование, то сможем свести к минимуму риск утечки памяти в нашем приложении.
Это глубокое погружение в управление памятью Java позволит расширить ваши знания о том, как работает куча, ссылочные типы и сборка мусора.
Вероятно, вы могли подумать, что если вы программируете на Java, то вам незачем знать о том, как работает память. В Java есть автоматическое управление памятью, красивый и тихий сборщик мусора, который работает в фоновом режиме для очистки неиспользуемых объектов и освобождения некоторой памяти.
Поэтому вам, как программисту на Java, не нужно беспокоиться о таких проблемах, как уничтожение объектов, поскольку они больше не используются. Однако, даже если в Java этот процесс выполняется автоматически, он ничего не гарантирует. Не зная, как устроен сборщик мусора и память Java, вы можете создать объекты, которые не подходят для сбора мусора, даже если вы их больше не используете.
Поэтому важно знать, как на самом деле работает память в Java, поскольку это дает вам преимущество в написании высокопроизводительных и оптимизированных приложений, которые никогда не будут аварийно завершены с ошибкой OutOfMemoryError . С другой стороны, когда вы окажетесь в плохой ситуации, вы сможете быстро найти утечку памяти.
Для начала давайте посмотрим, как обычно организована память в Java:
Структура памяти
Обычно память делится на две большие части: стек и куча. Имейте в виду, что размер типов памяти на этом рисунке не пропорционален реальному размеру памяти. Куча - это огромный объем памяти по сравнению со стеком.
2. Что такое утечка памяти
Утечка памяти — это ситуация, когда в куче присутствуют объекты, которые больше не используются, но сборщик мусора не может удалить их из памяти и, таким образом, они сохраняются там без необходимости.
Утечка памяти плоха тем, что она блокирует ресурсы памяти и со временем снижает производительность системы. Если с ней не бороться, приложение в конечном итоге исчерпает свои ресурсы и завершится с фатальной ошибкой java.lang.OutOfMemoryError .
Существует два различных типа объектов, которые находятся в Heap-памяти (куче) — со ссылками и без них. Объекты со ссылками — это те, на которые имеются активные ссылки внутри приложения, в то время как на другие нет таких ссылок.
Сборщик мусора периодически удаляет объекты без ссылок, но он никогда не собирает объекты, на которые все еще ссылаются. В таких случаях могут возникать утечки памяти:
Признаки утечки памяти
Серьезное снижение производительности при длительной непрерывной работе приложения
Ошибка кучи OutOfMemoryError в приложении
Спонтанные и странные сбои приложения
В приложении время от времени заканчиваются объекты подключения
Давайте подробнее рассмотрим несколько таких сценариев и как с ними бороться.
Заключение
Знание того, как организована память, дает вам преимущество в написании хорошего и оптимизированного кода с точки зрения ресурсов памяти. Преимущество заключается в том, что вы можете настроить свою работающую JVM, предоставив различные конфигурации, наиболее подходящие для запуска вашего приложения. Выявление и устранение утечек памяти - это очень просто, если использовать правильные инструменты.
Основные симптомы утечек памяти Java
Встречаются несколько симптомов указывающих, что приложение имеет проблемы утечки памяти. Небольшое снижение производительности, а не внезапный отказ работы приложения, указывает только на утечку памяти. Проблема может случаться каждый раз во время работы или только тогда, когда приложение начинает работать с большим объемом данных или, напротив, вы начинаете масштабировать приложение. Приложение, возможно, покажет ошибку нехватки памяти, как только утечка сожрет все доступные ресурсы памяти. Перезапустив приложение и надеясь на лучшее, натолкнетесь неоднократно на аварийные завершения до того момента, пока утечка не будет устранена. В общем, утечки памяти случаются когда ссылки на объекты накапливаются, вместо того, чтобы освобождать память. Они занимают всю доступную память и делают для приложения невозможным доступ до нужных ему ресурсов.
Ошибки конфигурации выглядящие как утечки памяти
Перед тем, как заглянете в ситуации вызывающие проблемы с памятью Java и проведете анализ, необходимо убедиться, что исследования не имеют отношения к абсолютно другой задаче. Часть ошибок out-of-memory возникают из-за различных ошибок, например ошибок конфигурации. У приложения, возможно, недостаток памяти в куче или оно конфликтует в системе с другими приложениями. Если начинаете говорить о проблемах нехватки памяти, но не можете определить что вызывает утечку, взгляните на приложение по-другому. Обнаружится, что нужно сделать изменения в потоке финализации или увеличить объем permanent generation пространства, являющегося областью памяти JVM для хранения описания классов Java и некоторых дополнительных данных.
Преимущества инструментов мониторинга памяти
Инструменты мониторинга памяти дают бОльшую видимость использования доступных ресурсов приложением Java. Используя данное ПО, вы делаете шаг для сужения поиска корня проблемы утечки памяти и прочих инцидентов связанных с производительностью. Инструменты идут в нескольких категориях, и вам, возможно, нужно использовать множество приложений, чтобы разобраться как начать правильно обозначать проблему и что пошло не так, даже если вы имеете дело с утечками памяти. Heap dump (дампа кучи) файлы дают необходимые сведения для анализа Java-памяти. В этом случае вам нужно использовать два инструмента: один для генерации дамп-файла и другой для подробного анализа. Такое решение дает детализированную информацию о том, что происходит с приложением. Один раз инструмент указывает места возможных проблем и работает над сужением площади, чтобы обнаружить точное место возникновения инцидента. И этот период времени - время самой длинной и портящей настроение части проб и ошибок. Анализатор памяти указывает несколько проблем в коде, но вы не уверены абсолютно, с какими проблемами столкнулось ваше приложение. Если всё ещё сталкиваетесь с прежней ошибкой, начните сначала и поработайте над другой возможной ошибкой. Сделайте одно изменение за раз и попытайтесь продублировать ошибку. Нужно будет дать приложению поработать некоторое время, чтобы продублировать условия возникновения ошибки. Если при первом тесте происходит утечка памяти, не забудьте протестировать приложение под нагрузкой. Приложение может работать отлично с небольшим количеством данных, но может снова выбросить прежние ошибки при работе с большим объемом данных. Если еще возникает всё та же самая ошибка, нужно начать сначала и разобрать другую возможную причину. Инструменты мониторинга памяти доказывают свою пользу после того, когда приложение стало полностью работающим. Можно удаленно наблюдать за производительностью JVM и проактивным обнаружением сбойных ситуаций перед тем, как разработчик погрузится в проблему и будет собирать исторические данные производительности, чтобы помочь себе в будущем улучшить техники программирования и наблюдать как Java работает под тяжелой нагрузкой. Многие решения включают режимы оповещения "опасность" или другие подобные режимы и разработчик сразу может знать, что происходит не так, как хотелось. Каждый разработчик не хочет, чтобы критическое приложение, будучи в промэксплуатации, падало и являлось причиной потери десятков или сотен тысяч долларов во время простоя приложения, поэтому инструменты мониторинга памяти уменьшают время реагирования разработчика. Приложения мониторинга памяти дают начать процесс диагностики мгновенно, вместо того, чтобы попросить вас пойти к заказчику, где никто не скажет какая именно ошибка случилось или какой код ошибки выдало приложение. Если часто погружаетесь в проблемы памяти и производительности вашего Java-приложения, плотно возьмитесь за процесс тестирования. Обозначьте каждую слабую область в процессе разработки и измените стратегии тестирования. Посоветуйтесь с коллегами и сравните свои подходы тестирования с существующими лучшими практиками. Иногда вам надо пересмотреть маленький фрагмент кода и далее обеспечить длительное воздействие на все приложение.
Роль Garbage Collector на память Java и утечки памяти
Garbage Collector (cборщик мусора) в Java играет ключевую роль в производительности приложения и использования памяти. Он ищет неиспользуемые (мертвые) объекты и удаляет их. Эти объекты больше не занимают память, так что ваше приложение продолжает обеспечивать доступность ресурсов. Иногда приложение не дает GC достаточно времени или ресурсов для удаления мертвых объектов и они накапливаются. Можно столкнуться с такой ситуацией когда идет активное обращение к объектам, которые, вы полагаете, мертвы. Сборщик мусора не может сделать ничего c этим, т.к. его механизм автоматизированного управления памяти обходит активные объекты. Обычно сборщик мусора работает автономно, но необходимо настроить его поведение на реагирование тяжелых проблем с памятью. Однако, GC может сам приводить к проблемам производительности.
Области GC
Сборщик мусора для оптимизации сборки разделяет объекты на разные области. В Young Generation представлены объекты, которые отмирают быстро. Сборщик мусора часто работает в этой области, с того момента, когда он должен проводить очистку. Объекты оставшиеся живыми по достижению определенного периода переходят в Old Generation. В области Old Generation объекты остаются долгое время, и они не удаляются сборщиком так часто. Однако, когда сборщик работает в области, приложение проходит проходит через большую операцию, где сборщик смотрит сквозь живые объекты для очистки мусора. В итоге объекты приложения находятся в конечной области permanent generation. Обычно, эти объекты включают нужные метаданные JVM. Приложение не генерирует много мусора в Permanent Generation, но нуждается в сборщике для удаления классов когда классы больше не нужны.
Связь между Garbage Collector и временем отклика
Сборщик мусора, независимо от приоритета исполнения потоков приложения, останавливает их не дожидаясь завершения. Такое явление называется событием "Stop the World". Область Young Generation сборщика мусора незначительно влияет на производительность, но проблемы заметны, если GC выполняет интенсивную очистку. В конечном итоге вы оказываетесь в ситуации, когда минорная сборка мусора Young Generation постоянно запущена или Old Generation переходит в неконтролируемое состояние. В такой ситуации нужно сбалансировать частоту Young Generation с производительностью, которая требует увеличение размера этой области сборщика. Области Permanent Generation и Old Generation сборщика мусора значительно влияют на производительность приложения и использования памяти. Эта операция major очистки мусора проходит сквозь heap, чтобы вытолкнуть отмершие объекты. Процесс длится дольше чем minor сборка и влияние на производительность может идти дольше. Когда высокая интенсивность очистки и большой размер области Old Generation, производительность всего приложения увязывает из-за событий "Stop the world". Оптимизация сборки мусора требует мониторинга как часто программа запущена, влияния на всю производительность и способов настройки параметров приложения для уменьшения частоты мониторинга. Возможно нужно будет идентифицировать один и тот же объект, размещенный больше, чем один раз, причем приложению не нужно отгораживаться от размещения или вам надо найти точки сжатия, сдерживающие всю систему. Получение правильного баланса требует уделения близкого внимания ко всему от нагрузки на CPU до циклов вашего сборщика мусора, особенно если Young и Old Generation несбалансированы. Адресация утечек памяти и оптимизация сборки мусора помогает увеличить производительность Java-приложения. Вы буквально жонглируете множеством движущихся частей. Но с правильным подходом устранения проблем и инструментами анализа, спроектированных чтобы дать строгую видимость, вы достигнете света в конце туннеля. В противном случае замучаетесь с возникающими неполадками связанных с произодительностью. Тщательное размещение памяти и её мониторинг играют критическую роль в Java-приложении. Необходимо полностью взять в свои руки взаимодействие между сборкой мусора, удалением объектов и производительностью, чтобы оптимизировать приложение и избежать ошибок упирающихся в нехватку памяти. Инструменты мониторинга дают оставаться на высоте, чтобы обнаружить возможные проблемы и обозначить тенденции утилизации памяти так, что вы принимаете проактивный подход к исправлению неисправностей. Утечки памяти часто показывают неэффективность устранения неисправностей обычным путем, особенно если вы сталкиваетесь с неверными значениями параметров конфигурации, но решения вопросов связанных с памятью помогают быстро избежать инцидентов стоящих у вас на пути. Совершенство настройки памяти Java и GC делают ваш процесс разработки намного легче.
3. Типы утечек памяти в Java
В любом приложении утечка памяти может произойти по множеству причин. В этом разделе мы обсудим наиболее распространенные из них.
3.1. Утечка памяти через статические поля
Первый сценарий, который может привести к потенциальной утечке памяти, — это интенсивное использование статических переменных.
В Java статические поля имеют срок жизни, который обычно соответствует полному жизненному циклу запущенного приложения (за исключением случаев, когда ClassLoader получает право на сборку мусора).
Давайте создадим простую Java-программу, которая заполняет статический список:
Теперь, если мы проанализируем кучу во время выполнения этой программы, то увидим, что она увеличилась между точками отладки 1 и 2.
Но когда мы оставляем метод populateList() в точке отладки 3, куча еще не убрана сборщиком, как это видно в ответе VisualVM:
Однако в приведенной выше программе, в строке номер 2, если мы просто отбросим ключевое слово static, то это приведет к резкому изменению использования памяти, как показывает отклик:
Первая часть до точки отладки почти не отличается от того, что мы получили в случае static. Но на этот раз после выхода из метода populateList() вся память списка очищается, поскольку у нас нет на него ссылок.
Следовательно, нам нужно очень внимательно следить за использованием статических переменных. Если коллекции или большие объекты объявлены как статические, то они остаются в памяти на протяжении всего времени работы приложения, тем самым блокируя жизненно важную память, которую можно было бы использовать в другом месте.
Как предотвратить это?
Минимизируйте использование статических переменных
При использовании синглтонов полагайтесь на имплементацию, которая лениво, а не жадно загружает объект.
3.2. Через незакрытые ресурсы
Всякий раз, когда мы создаем новое соединение или открываем поток, JVM выделяет память для этих ресурсов. В качестве примера можно привести соединения с базой данных, входные потоки и объекты сессий.
Забыв закрыть эти ресурсы, можно заблокировать память, что сделает их недоступными для GC. Это может произойти даже в случае исключения, которое не позволяет программному процессу достичь оператора, выполняющего код для закрытия этих ресурсов.
В любом случае, открытые соединения, оставшиеся от ресурсов, потребляют память, и если с ними не разобраться, они могут ухудшить производительность и даже привести к ошибке OutOfMemoryError .
Как предотвратить это?
Всегда используйте блок finally для закрытия ресурсов
Код (даже в блоке finally ), закрывающий ресурсы, сам не должен содержать исключений.
При использовании Java 7+ можно использовать блок try-with-resources .
3.3. Неправильная имплементация equals() и hashCode()
При определении новых классов очень распространенной ошибкой является отсутствие надлежащих переопределенных методов для equals() и hashCode() .
HashSet и HashMap используют эти методы во многих операциях, и если они переопределены неправильно, то могут стать источником потенциальных проблем с утечкой памяти.
Давайте рассмотрим как пример тривиальный класс Person и используем его в качестве ключа в HashMap
Теперь мы вставим дубликаты объектов Person в Map , использующую этот ключ.
Помните, что Map не может содержать дубликаты ключей:
Здесь мы используем Person в качестве ключа. В связи с тем, что Map не допускает дублирования ключей, то мы вставили в качестве ключа дубликаты объектов Person , что не должно увеличивать память.
Но поскольку мы не определили правильный метод equals() , дубликаты объектов накапливаются и увеличивают память, поэтому в памяти мы видим больше одного объекта. Куча в VisualVM в этом случае выглядит следующим образом:
Однако, если бы мы правильно переопределили методы equals() и hashCode(), то в этой Map существовал бы только один объект Person.
Давайте рассмотрим правильную имплементацию equals() и hashCode() для нашего класса Person :
В этом случае будут верны следующие утверждения:
После правильного переопределения equals() и hashCode() куча для той же программы выглядит следующим образом:
Другой пример — использование инструмента ORM, такого как Hibernate, который применяет методы equals() и hashCode() для анализа объектов и сохраняет их в кэше.
Вероятность утечки памяти довольно высока, если эти методы не переопределены, поскольку Hibernate не сможет сравнивать объекты и заполнит свой кэш их копиями.
Как предотвратить это?
Как правило, на практике, при определении новых сущностей всегда переопределяйте методы equals() и hashCode() .
Недостаточно их просто переопределить, это необходимо сделать оптимальным образом. Для получения дополнительной информации ознакомьтесь с нашими учебными пособиями Generate equals() and hashCode() with Eclipse и Guide to hashCode() in Java.
3.4. Внутренние классы, которые ссылаются на внешние
Это происходит в случае нестатических внутренних классов (анонимных классов). Для инициализации они всегда требуют экземпляр внешнего класса.
Каждый нестатический внутренний класс по умолчанию имеет неявную ссылку на содержащий его класс. Если мы используем объект этого внутреннего класса в приложении, то даже после того, как объект нашего содержащего внешнего класса покинет область видимости, он не будет убран в мусор.
Рассмотрим класс, который содержит ссылки на множество громоздких объектов и имеет нестатический внутренний класс. При создании объекта только внутреннего класса, модель памяти выглядит следующим образом:
Однако, если мы просто объявим внутренний класс как статический, то память уже будет выглядеть так:
Как предотвратить это?
Если внутреннему классу не нужен доступ к членам внешнего класса, подумайте о том, чтобы превратить его в статический.
3.5. Через методы finalize()
Использование финализаторов - еще один источник потенциальных проблем с утечкой памяти. Когда метод finalize() класса переопределяется, то объекты этого класса не сразу убирают в мусор. Вместо этого GC ставит их в очередь на финализацию, которая происходит позже.
Кроме того, если код метода finalize() , не является оптимальным, а также очередь финализации не успевает за сборщиком мусора Java, то рано или поздно приложение столкнется с ошибкой OutOfMemoryError .
Для демонстрации возьмем класс, в котором мы переопределили метод finalize() , и его выполнение занимает немного времени. Когда большое количество объектов данного класса собирается в мусор, то в VisualVM это выглядит так:
Однако если мы просто удалим переопределенный метод finalize() , то та же программа даст следующий ответ:
Как предотвратить это?
Мы всегда должны избегать финализаторов
Более подробно о finalize() читайте в разделе 3 (Как избежать использования финализаторов) нашего руководства по методу finalize в Java.
3.6. Интернированные строки
Пул строк Java претерпел значительные изменения в Java 7, когда он был перенесен из PermGen в HeapSpace. Однако для приложений, работающих на версии 6 и ниже, мы должны быть более внимательны при работе с большими строками.
Если мы считываем огромный объект-массив String и вызываем для него intern(), то он попадает в пул строк, который находится в PermGen (постоянной памяти) и будет оставаться там до тех пор, пока работает наше приложение. Это блокирует память и создает большую ее утечку в нашем приложении.
PermGen для этого случая в JVM 1.6 выглядит в VisualVM следующим образом :
В отличие от этого, если мы просто читаем строку из файла и не интернируем ее, PermGen выглядит так:
Как предотвратить это?
Самый простой способ решить эту проблему — обновить Java до последней версии, так как начиная с Java версии 7 пул строк перемещен в HeapSpace.
При работе с большими строками увеличьте размер пространства PermGen, чтобы избежать возможных ошибок OutOfMemoryErrors :
3.7. Использование ThreadLocals
ThreadLocal (подробно рассматривается в учебнике "Введение в ThreadLocal в Java") - это конструкция, которая дает нам возможность изолировать состояние для конкретного потока, тем самым позволяя достичь его безопасности.
В этой конструкции каждый поток хранит неявную ссылку на копию переменной ThreadLocal и будет поддерживать только свою собственную независимую копию, вместо совместного использования ресурса несколькими потоками, в течение всего времени, пока поток активен.
Несмотря на все преимущества, переменные ThreadLocal являются спорными, поскольку они могут приводить к утечкам памяти при неправильном использовании. Joshua Bloch однажды прокомментировал применение локальных переменных потоков:
Неаккуратное использование пулов потоков в сочетании с небрежным применением локальных переменных потоков может привести к непреднамеренному удержанию объектов, как было отмечено во многих местах. Но возлагать вину на локальные переменные неоправданно.
Процесс сборки мусора
Как обсуждалось ранее, в зависимости от типа ссылки, которую переменная из стека содержит на объект из кучи, в определенный момент времени этот объект становится подходящим для сборщика мусора.
Объекты, подходящие для сборки мусора
Например, все объекты, отмеченные красным цветом, могут быть собраны сборщиком мусора. Вы можете заметить, что в куче есть объект, который имеет строгие ссылки на другие объекты, которые также находятся в куче (например, это может быть список, который имеет ссылки на его элементы, или объект, имеющий два поля типа, на которые есть ссылки). Однако, поскольку ссылка из стека потеряна, к ней больше нельзя получить доступ, так что это тоже мусор.
Чтобы углубиться в детали, давайте сначала упомянем несколько вещей:
Этот процесс запускается автоматически 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 объявлен устаревшим.
Утечки памяти при использовании ThreadLocals
Предполагается, что ThreadLocals будут утилизироваться, как только удерживающий их поток перестанет существовать. Но проблема возникает, когда ThreadLocals применяются вместе с современными серверами приложений.
Современные серверы приложений используют пул потоков для обработки запросов вместо создания новых (например, Executor в Apache Tomcat). Более того, они также используют отдельный загрузчик классов.
Поскольку пулы потоков в серверах приложений работают на основе концепции повторного использования, они никогда не утилизируются - их используют повторно для обслуживания другого запроса.
Теперь, если какой-либо класс создает переменную ThreadLocal , но явно не удаляет ее, то копия этого объекта останется в воркере Thread даже после остановки веб-приложения, тем самым препятствуя утилизации объекта.
Как предотвратить это?
Хорошей практикой является очистка ThreadLocals , когда они больше не используются — ThreadLocals предоставляет метод remove(), который удаляет значение текущего потока для этой переменной.
Не используйте ThreadLocal.set(null) для очистки значения — он в действительности не очищает, а вместо этого ищет Map , связанную с текущим потоком, и устанавливает пару ключ-значение как текущий поток и null соответственно
Еще лучше рассматривать ThreadLocal как ресурс, который должен быть закрыт в блоке finally , чтобы быть уверенным в его закрытии во всех случаях, даже при исключении:
4. Другие стратегии борьбы с утечками памяти
Хотя универсального решения при борьбе с утечками памяти не существует, есть некоторые способы, с помощью которых их можно минимизировать.
4.1. Включить профилирование
Профилировщики Java — это инструменты, которые отслеживают и диагностируют утечки памяти в приложении. Они анализируют, что происходит внутри нашего приложения — например, как выделяется память.
Используя профилировщики, можно сравнить различные подходы и найти области, где оптимально используются наши ресурсы.
В разделе 3 этого руководства мы использовали Java VisualVM. Пожалуйста, ознакомьтесь с нашим руководством по профилировщикам Java, чтобы узнать о различных типах профилировщиков, таких как Mission Control, JProfiler, YourKit, Java VisualVM и Netbeans Profiler.
4.2. Подробная сборка мусора
При активации подробной сборки мусора мы отслеживаем детальную трассировку GC. Чтобы включить эту функцию, нам нужно добавить следующее в конфигурацию JVM:
Добавив этот параметр, мы сможем увидеть подробности того, что происходит внутри GC:
4.3. Использование ссылочных объектов для предотвращения утечек памяти
Для борьбы с утечками памяти можно также воспользоваться ссылочными объектами в Java, которые поставляются с пакетом java.lang.ref. С помощью пакета java.lang.ref вместо прямых ссылок на объекты мы используем специальные ссылки, которые позволяют легко собирать мусор.
Очереди ссылок предназначены для того, чтобы мы знали о действиях, выполняемых сборщиком мусора. Для получения дополнительной информации прочитайте Baeldung-учебник "Мягкие ссылки в Java", а именно раздел 4.
4.4. Предупреждения об утечке памяти в Eclipse
Для проектов на JDK 1.5 и выше Eclipse выдает предупреждения и ошибки всякий раз, когда сталкивается с очевидными случаями утечки памяти. Поэтому при разработке в Eclipse мы можем регулярно посещать вкладку "Проблемы" и быть более бдительными в отношении предупреждений об утечке памяти (если таковые имеются):
4.5. Бенчмаркинг
Мы можем измерить и проанализировать производительность Java-кода, выполняя эталонные тесты. Таким образом, мы можем сравнить производительность альтернативных подходов к выполнению одной и той же задачи. Это поможет нам выбрать лучший из них и поможет сэкономить память.
Для получения более подробной информации о бенчмаркинге, ознакомьтесь с нашим учебным пособием "Микробенчмаркинг с Java".
4.6. Обзоры кода
Наконец, у нас всегда есть классический, старый добрый способ — сделать простой обзор кода.
В некоторых случаях даже этот тривиальный на первый взгляд метод может помочь в устранении некоторых распространенных проблем утечки памяти.
Ссылки на String
Ссылки на тип String в Java обрабатываются немного по- другому. Строки неизменяемы, что означает, что каждый раз, когда вы делаете что-то со строкой, в куче фактически создается другой объект. Для строк Java управляет пулом строк в памяти. Это означает, что Java сохраняет и повторно использует строки, когда это возможно. В основном это верно для строковых литералов. Например:
При запуске этот код распечатывает следующее:
Strings are equal
Следовательно, оказывается, что две ссылки типа String на одинаковые строковые литералы фактически указывают на одни и те же объекты в куче. Однако это не действует для вычисляемых строк. Предположим, что у нас есть следующее изменение в строке // 1 приведенного выше кода.
Strings are different
В этом случае мы фактически видим, что у нас есть два разных объекта в куче. Если учесть, что вычисляемая строка будет использоваться довольно часто, мы можем заставить JVM добавить ее в пул строк, добавив .intern() метод в конец вычисляемой строки:
При добавлении вышеуказанного изменения создается следующий результат:
Читайте также: