Вопросы интервью Baidu: Могут ли после OOM продолжаться другие темы?

задняя часть

Поскольку интервьюер упомянул только OOM, в Java есть много типов OOM:

  • Переполнение кучи ("java.lang.OutOfMemoryError: пространство кучи Java")

  • Переполнение Permgen ("java.lang.OutOfMemoryError: Permgen space")

  • Невозможно создать поток ("java.lang.OutOfMemoryError: невозможно создать новый собственный поток")

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

  • Проверяет содержимое каждой области среды выполнения, описанной в «Спецификации виртуальной машины Java», по коду.

  • При столкновении с реальными исключениями переполнения памяти на работе вы можете быстро узнать, какая область переполнения памяти основана на аномальной информации подсказки, узнать, какой код может вызвать переполнение памяти в этих областях, и как бороться с этими исключениями.

Код в этой статье был протестирован автором на виртуальной машине HotSpot на базе OpenJDK 8.

1 переполнение кучи Java

Куча Java используется для хранения экземпляров объектов. Пока объекты создаются непрерывно и существует доступный путь между корнями GC и объектами, чтобы механизм GC не очищал эти объекты, по мере увеличения количества объектов общая емкость будет достигать ограничение емкости максимальной кучи. Возникнет исключение переполнения памяти.

Ограничьте размер кучи Java до 20 МБ, не масштабируйте

-XX:+HeapDumpOnOutOf-MemoryError

Вы можете сделать дамп виртуальной машины текущим снимком дампа кучи памяти, когда возникает исключение переполнения памяти.

Дело 1

  • сообщить об ошибке

OOM памяти кучи Java является наиболее распространенным сценарием исключения переполнения памяти в практических приложениях. Когда происходит переполнение памяти кучи Java, информация о стеке исключений «java.lang.OutOfMemoryError» будет сопровождаться дополнительным приглашением «Пространство кучи Java».

Теперь, когда это случилось,Как решить исключение этой области памяти?? Как правило, моментальный снимок дампа кучи из Dump сначала анализируется с помощью инструмента анализа образа памяти (например, jprofile). Первый шаг заключается в том, чтобы сначала подтвердить, является ли объект в памяти, вызывающий OOM, необходимым, то есть сначала различить, является ли он

  • Утечка памяти

  • Или переполнение памяти (Memory Overflow)

  • Изображение ниже представляет собой файл моментального снимка дампа кучи (java_pid44526.hprof), открытый с помощью jprofile.

Если есть утечка памяти, вы можете проверить цепочку ссылок от утекшего объекта до корней GC, чтобы узнать, через какой путь ссылок проходит утекший объект и с какими корнями GC он связан, чтобы сборщик мусора не смог их восстановить. Информация цепочки ссылок GC Roots обычно может более точно определить место, где эти объекты создаются, а затем узнать конкретное местонахождение кода, вызывающего утечку памяти.

Если это не утечка памяти, то есть если объекты в памяти действительно должны жить, то следует:

  1. Проверьте настройки параметров кучи JVM (-Xmx и -Xms) и сравните с памятью машины, чтобы увидеть, есть ли место для регулировки вверх.
  2. Затем проверьте код, чтобы убедиться, что жизненный цикл некоторых объектов слишком длинный, время удержания состояния слишком велико, конструкция структуры хранения неразумна и т. д., и минимизируете потребление памяти во время выполнения программы.

Выше приведено краткое представление о решении проблем с памятью кучи в Java.

Случай 2

Настройки параметров запуска JVM:

-Xms5m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError

  • Изменения в куче JVM

Используемый размер кучи внезапно дрожит! Это означает, что когда поток бросает OOM, все ресурсы памяти, которые он занимает, будут освобождены, не влияя на нормальную работу других потоков! Таким образом, после переполнения потока другие потоки в этом процессе могут работать как обычно. Поток, в котором происходит OOM, вообще умрет, то есть будет завершен, а куча, занятая удерживаемым потоком объектом, будет занята GC, а память будет освобождена. Поскольку gc выполняется до того, как произойдет OOM, даже если другие потоки могут работать нормально, это будет иметь большее влияние из-за частого gc.

Переполнение кучи и переполнение стека, вывод один и тот же.

2 стека виртуальных машин/переполнение стека собственных методов

так какHotSpot JVM не различает стек виртуальной машины и собственный стек методов., так что HotSpot-XossХотя параметр (устанавливающий размер стека нативных методов) существует, он не имеет никакого эффекта, емкость стека может быть определена только-XssУстановка параметров.

Что касается стека виртуальной машины и собственного стека методов, Спецификация виртуальной машины Java описывает следующие исключения:

  1. Если глубина стека, запрошенная потоком, больше максимальной глубины, разрешенной виртуальной машиной, будет выдано исключение StackOverflowError.
  2. Если стековая память виртуальной машины допускает динамическое расширение, будет выдано исключение OutOfMemoryError, когда расширенная емкость стека не может применяться для достаточного объема памяти.

«Спецификация виртуальной машины Java» явно позволяет реализации JVM выбирать, поддерживать ли динамическое расширение стека, и выбор виртуальной машины HotSpotРасширение не поддерживается, поэтому, если OOM не возникает из-за невозможности получить достаточно памяти при создании потока для подачи заявки на память, переполнение памяти не будет вызвано расширением во время выполнения потока, но StackOverflowError будет вызван только тем, что емкость стека не может вместить новый стек. кадры.

Как это проверить?

Проведите два эксперимента, сначала в операции с одним потоком, попробуйте следующие два поведения, чтобы сделать HotSpot OOM:

использовать-XssУменьшить объем памяти стека

  • Пример

  • результат

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

Различные версии виртуальной машины Java и разные операционные системы могут ограничивать минимальный размер стека, который в основном зависит от размера подкачки памяти операционной системы. Например, параметр -Xss160k в приведенном выше методе можно использовать в обычном режиме для JDK 8 в 62-разрядной системе macOS, но если он используется для JDK 11 в 64-разрядной системе Windows, появится сообщение о том, что минимальный размер стека не может быть меньше 180 КБ, а под Linux это значение может быть 228 КБ.Если оно ниже этого минимального предела, виртуализатор HotSpot при запуске выдаст следующее приглашение:

The stack size specified is too small, Specify at

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

  • Пример

  • результат

Так что, будь то из-за того, что кадр стека слишком велик или емкость стека виртуальной машины слишком мала, когда новая память кадра стека не может быть выделена, HotSpot выдаст SOF. Но на виртуальной машине, допускающей динамическое расширение размера стека, один и тот же код может привести к разным ситуациям.

Если тест не ограничивается одним потоком, а постоянно создает новые потоки, OOM также будет происходить на HotSpot. Однако прямой связи между генерацией OOM и достаточным пространством стека нет, что в основном зависит от состояния использования памяти самой ОС. Даже в этом случае, чем больше памяти выделяется стеку каждого потока, тем проще генерировать OOM. Нетрудно понять, что память, выделяемая ОС каждому процессу, ограничена, например, максимальный лимит памяти одного процесса 32-битной Windows составляет 2Гб. HotSpot предоставляет параметры для управления максимальной памятью кучи Java и области метода.Оставшаяся память составляет 2G (лимит ОС) минус максимальный размер кучи, а затем минус максимальный размер области метода.Поскольку счетчик программ потребляет много памяти Он небольшой и может быть проигнорирован.Если также удалить прямую память и память, потребляемую самим процессом виртуальной машины, оставшаяся память будет выделена стеком виртуальной машины и стеком собственных методов. Следовательно, чем больше стековая память, выделенная каждому потоку, тем меньшее количество потоков может быть создано и тем проще исчерпать оставшуюся память при создании потока:

  • Пример

  • результат
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread

Когда возникает SOF, будет четкий стек ошибок для анализа, и найти проблему будет относительно легко. Если вы используете параметры виртуальной машины HotSpot по умолчанию, глубина стека в большинстве случаев будет достигать 1000~2000 (поскольку размер кадра, помещаемый в стек каждым методом, не одинаков), и нет проблем с достижением 1000~ 2000. Для обычных вызовов методов (включая невозможность оптимизации хвостовой рекурсии) рекурсивных вызовов этой глубины должно быть вполне достаточно. Однако, если переполнение памяти вызвано созданием слишком большого количества потоков, если количество потоков нельзя уменьшить или нельзя заменить 64-битную виртуальную машину, ее можно обменять на большее количество потоков только за счет уменьшения максимальной кучи и уменьшения емкость стека. Такой метод решения переполнения памяти путем "уменьшения памяти" вообще сложно придумать, если нет опыта в этой области. Это также связано с тем, что эта проблема относительно скрыта.Начиная с JDK 7, после «невозможно создать собственный поток» в приведенном выше подсказке виртуальная машина конкретно указывает, что причина может быть «возможно

#define OS_NATIVE_THREAD_CREATION_FAILED_MSG 	
	"unable to create native thread: possibly out of memory or process/resource limits reached"

3 Область метода и постоянное переполнение пула времени выполнения

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

Начиная с JDK 7, в HotSpot постепенно происходит «постоянная генерация».

String::intern() — это нативный метод: если пул строковых констант уже содержит строку, равную этому объекту String, он возвращает ссылку на объект String, представляющий строку в пуле; в противном случае этот объект String содержит добавляет строку в постоянный пул и возвращает ссылку на этот объект String.

В JDK6 или до виртуальной машины HotSpot постоянный пул выделяется в постоянном поколении, которому можно передать следующие два параметра:Ограничение размера постоянной генерации может косвенно ограничить емкость постоянного пула.

  • пример

  • результат
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 
	at java.lang.String.intern(Native Method) 
	at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)

Можно видеть, что когда пул констант времени выполнения переполняется, после исключения OutOfMemoryError появляется подсказка «PermGen space», указывающая, что пул констант времени выполнения действительно является частью области методов (то есть постоянное создание в виртуальной машине HotSpot). JDK 6).

Запуск этой программы с JDK 7 или более поздней версии не даст такого же результата, будь то продолжение использования параметра -XX:MaxPermSize в JDK 7 или использование -XX:MaxMeta-spaceSize в JDK 8 и выше. Параметры также ограничивают область метода до 6 МБ, и не будет воспроизводиться исключение переполнения в JDK 6. Цикл будет продолжаться и никогда не останавливаться. Это изменение связано с тем, что начиная с JDK 7 пул строковых констант, изначально хранившийся в постоянном поколении, был перемещен в кучу Java, поэтому в JDK 7 и более поздних версиях ограничение емкости области метода не имеет смысла для этого тестового примера.

В настоящее время, используя параметр -Xmx для ограничения максимальной кучи до 6 МБ, вы можете увидеть один из следующих двух результатов, в зависимости от того, где произошло переполнение при выделении объекта:

// OOM异常一: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 
at java.base/java.lang.Integer.toString(Integer.java:440) 
at java.base/java.lang.String.valueOf(String.java:3058) 
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12) 

// OOM异常二: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/java.util.HashMap.resize(HashMap.java:699) 
at java.base/java.util.HashMap.putVal(HashMap.java:658) 
at java.base/java.util.HashMap.put(HashMap.java:607) 
at java.base/java.util.HashSet.add(HashSet.java:220) 
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)

Есть много интересного о том, где реализован пул строковых констант:Запуск в JDK 6, результат два ложных Запуск в JDK 7, одно верное и одно ложноеПоскольку intern() JDK6 скопирует экземпляр строки, обнаруженный впервые, в пул строковых констант постоянного поколения и вернет ссылку на экземпляр строки в постоянном поколении, а экземпляр строкового объекта, созданный StringBuilder, находится в Куча Java, поэтому это не может быть одна и та же ссылка, результат вернет false.

В JDK 7 и более поздних версиях intern() не нужно копировать экземпляр строки в постоянное поколение, пул строковых констант был перемещен в кучу Java, просто запишите ссылку на первый экземпляр в пуле констант, поэтому ссылка, возвращаемая intern() Это тот же экземпляр строки, созданный StringBuilder.

Сравнение str2 возвращает false, так как строка "java" уже появилась до выполнения String-Builder.toString(), и ссылка на нее уже есть в пуле строковых констант, что не соответствует требованиям стажера( ). Метод Encounter", а строка "computer software" появляется впервые, поэтому результат возвращает true!

Для теста области метода основная идея состоит в том, чтобы сгенерировать большое количество классов во время выполнения, чтобы заполнить область метода до тех пор, пока она не переполнится. Хотя непосредственное использование API Java SE также может динамически генерировать классы (такие как GeneratedConstructorAccessor и динамический прокси во время отражения), эта операция сопряжена с трудностями. С помощью CGLib большое количество динамических классов генерируется во время выполнения путем прямого манипулирования байт-кодом. Многие современные основные фреймворки, такие как Spring и Hibernate, используют расширение байт-кода CGLib при улучшении классов.Когда расширяется больше классов, требуется большая область методов, чтобы гарантировать, что динамически сгенерированные новые типы могут быть загружены в память. Многие динамические языки, работающие на JVM (например, Groovy), обычно продолжают создавать новые типы для поддержки динамической природы языка.С ростом популярности таких динамических языков все более и более вероятными становятся сценарии переполнения, подобные следующему коду. столкнуться.

Результат работы в JDK 7:

Caused by: java.lang.OutOfMemoryError: PermGen space 
	at java.lang.ClassLoader.defineClass1(Native Method) 
	at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) 
	at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

JDK8 и более поздние версии: можно использовать

-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M

Устанавливает начальный размер метапространства и максимальный выделяемый размер. 1. Если размер метапространства не указан, по умолчанию максимальный размер метапространства равен размеру системной памяти.Метапространство постоянно увеличивается, и виртуальная машина может использовать всю доступную системную память. 2. Если памяти метапространства недостаточно, будет сообщено об OOM. 3. По умолчанию для 64-разрядной серверной JVM значение по умолчанию -XX:MetaspaceSize равно 21 МБ, что является начальным верхним пределом. неиспользуемые классы, то значение верхней отметки будет сброшено. 4. Из пункта 3 мы можем знать, что если инициализированная верхняя отметка установлена ​​слишком низко, полный сборщик мусора будет часто запускаться, и верхняя отметка будет корректироваться много раз. Поэтому, чтобы избежать частого GC и настроить верхнюю отметку, рекомендуется установить -XX:MetaspaceSize на более высокое значение, а -XX:MaxMetaspaceSize не устанавливать.

Результаты работы JDK8:Если класс должен быть gc, условия, которые должны быть выполнены, более строгие. В сценариях, где большое количество динамических классов генерируется при частом выполнении, особое внимание следует уделить статусу повторного использования этих классов. В дополнение к вышеупомянутым программам, использующим расширение байт-кода CGLib и динамические языки, распространены такие сценарии:

  • Большое количество JSP или приложений, которые динамически генерируют файлы JSP (JSP должны быть скомпилированы в классы Java при первом запуске)
  • Приложения на основе OSGi (даже один и тот же файл класса, загруженный разными загрузчиками, будет рассматриваться как разные классы)

После JDK8 постоянное поколение полностью устарело, и вместо него используется метапространство. При настройках по умолчанию обычное динамическое создание новых типов тестовых случаев, перечисленных выше, трудно заставить виртуальную машину генерировать OOM области методов. Чтобы позволить пользователям предотвращать разрушительные операции, подобные приведенному выше коду, в практических приложениях, HotSpot по-прежнему предоставляет некоторые параметры в качестве мер защиты для метапространства:

  • -XX:MetaspaceSize

Задает начальный размер метапространства в байтах. При достижении этого значения сборщик мусора запускает выгрузку типа, а сборщик корректирует значение. Если освободилось много места, уменьшите значение соответствующим образом, если освободилось мало места, увеличьте значение соответствующим образом, не превышая -XX:MaxMetaspaceSize (если установлено)

  • -XX:MaxMetaspaceSize

Установите максимальное значение метапространства, по умолчанию -1, то есть без ограничений или ограничено только размером локальной памяти

  • -XX:MinMetaspaceFreeRatio

Управление процентом минимальной оставшейся емкости метапространства после сборки мусора, что может снизить частоту сборки мусора, вызванную недостаточным количеством метапространства.

  • -XX:Max-MetaspaceFreeRatio

Управляет процентом максимальной оставшейся емкости метапространства.

Собственное прямое переполнение памяти

Емкость прямой памяти может быть определена-XX:MaxDirectMemorySizeУкажите, если не указано, значение по умолчанию совпадает с максимальным значением кучи Java (-Xmx) соответствуют.

Здесь класс DirectByteBuffer обходится, а экземпляр Unsafe получается напрямую через отражение для выделения памяти. getUnsafe() класса Unsafe указывает, что только загрузчик класса начальной загрузки вернет экземпляр, что отражает надежду разработчика на то, что только классы в стандартной библиотеке классов виртуальной машины могут использовать Unsafe.В JDK10 некоторые функции Unsafe открыт наружу через VarHandle. Поскольку, хотя использование DirectByteBuffer для выделения памяти также вызовет OOM, он на самом деле не применяется для выделения памяти для os, когда генерирует исключение.Вместо этого посредством вычислений известно, что память не может быть выделена, и OOM вручную выбрасывается в коде для действительно применяются для выделения памяти ДаUnsafe::allocateMemory()

  • Выделить родную память с небезопасным

  • результат

Очевидной особенностью переполнения памяти, вызванного прямой памятью, является отсутствие явных аномалий в файле дампа кучи. например, использование NIO), то пришло время рассмотреть прямую память.