Убийство ГК складывается из принципа отражения

Java

предисловие

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

текст

Обзор

Компания имеет большую систему памяти (более 70 ГБ), которая использует CMS GC, но поскольку система очень чувствительна ко времени, иногда замечание бывает очень длинным из-за gclocker (хотя параметр -XX:+CMSScavReengeBeforeRemark добавлен, но gclocker вызовет замечание перед замечанием).YGC задержался), я не выдержал такой длинной паузы, поэтому пришлось мигрировать на G1.После ряда настроек он был относительно стабилен, и этот набор параметров был запихал на все машины.

Однако буквально на прошлой неделе неожиданно появилась машина с Full GC.Изначально G1 создавался в расчете на то, что Full GC не появится.Полный сборщик мусора вообще ненормальный.Лог GC выглядит следующим образом:

gc.jpg

Из приведенного выше лога нетрудно обнаружить, что Perm запускает Full GC, а Perm падает после Full GC, но следует отметить, что обычный G1 GC под JDK7 не будет выполнять выгрузку классов, только когда Full GC будет удален, но в JDK8 соответствующие параметры предоставляются, и выгрузка класса может быть сделана на определенных этапах G1 GC

Таким образом, бизнес-сторона должна сначала сделать дамп ядра, сохранить сайт, перезапустить систему, а затем сделать дамп кучи для дампа ядра, но дамп кучи достигает 40 ГБ, вы можете использовать jmap -permstat core.xxx, чтобы увидеть, что находится в перми

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

Что в Перми?

Так как Пермь заполнена, мы должны посмотреть, что находится в Перми.Мы знаем, что Пермь в основном хранит исходные данные класса.Например, если мы загружаем класс, информация этого класса будет размещена в Перми для хранения.Некоторые из его структур данных, поэтому в большинстве случаев использование Perm тесно связано с количеством загружаемых классов.Конечно, есть и другие данные, хранящиеся в Perm в более низкой версии, такие как String(String.intern ()Случай).

Кроме того, опыт говорит нам, что если Perm переполняется, существует высокая вероятность динамического создания загрузчика классов для загрузки класса.С помощью приведенной выше команды jmap мы можем подсчитать количество sun.reflect.DelegatingClassLoaders, чтобы достичь 415 737.

То, что в принципе может быть заблокировано, является причиной того, что загрузчик классов отражения вызывает переполнение Perm. Тогда почему существует так много загрузчиков классов отражения? Что такое загрузчик классов отражения? Давайте кратко поговорим о принципе отражения.

Принцип отражения

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

Method method = XXX.class.getDeclaredMethod(xx,xx);method.invoke(target,params)

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

получить метод

Для вызова необходимо сначала получить Метод, а логика получения Метода — через класс Class, а ключевые методы и атрибуты следующие:

class.jpg

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

3.jpg

Этот атрибут в основном является SoftReference, то есть он может быть перезапущен при некоторых неблагоприятных условиях памяти, но в нормальных условиях время перезапуска можно контролировать с помощью параметра -XX:SoftRefLRUPolicyMSPerMB. происходит Он будет переработан.После переработки значит, что такой объект нужно пересоздать, когда возникнет необходимость.При этом также необходимо повторно получить копию данных из JVM.Тогда Метод и Поля полей, связанные с этой структурой данных, регенерируются. Если это регенерированный объект, в чем может быть проблема? я пойму позже

Метод getDeclaredMethod на самом деле очень прост, то есть скопируйте объект Method из списка методов, возвращаемого privateGetDeclaredMethods, и верните его. И этот процесс репликации достигается с помощью searchMethods

Если manifestMethods свойства ReflectData не пуст, то privateGetDeclaredMethods может вернуть его напрямую, в противном случае он будет взят из JVM и назначен на поле ReflectData, чтобы кешированные данные можно было использовать при следующем вызове privateGetDeclaredMethods. не нужно каждый раз передавать в JVM, чтобы получить данные, т.к. ReflectData — это Softreference, поэтому есть риск не получить значение, а раз не получится — снова лезешь в JVM

searchMethods найдет соответствующий метод с тем же именем из списка методов, возвращенного privateGetDeclaredMethods, а затем скопирует объект метода. Конкретной реализацией этой копии является метод Method.copy:

4.jpg

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

Вызов метода

После того, как у вас есть метод, вы можете вызвать его метод вызова.Давайте сначала рассмотрим некоторую ключевую информацию о методе.

5.jpg

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

The methodAccessor is very important. In fact, the Method.invoke method is to call the invoke method of the methodAccessor. If the methodAccessor attribute already exists in the root itself, then assign it directly with the methodAccessor of the root. Otherwise, create a новый.

Реализация метода доступа

MethodAccessor сам по себе является интерфейсом

6.jpg

Существует три основных реализации

  • DelegatingMethodAccessorImpl
  • NativeMethodAccessorImpl
  • GeneratedMethodAccessorXXX

Среди них DelegatingMethodAccessorImpl — это методAccessor, который, наконец, внедряется в метод, то есть все методы вызова метода будут вызывать этот DelegatingMethodAccessorImpl.invoke, как его имя, это прокси, то есть реальная реализация может быть следующие два вида

7.jpg
Если это NativeMethodAccessorImpl, как следует из названия, реализация в основном является собственной реализацией, а GeneratedMethodAccessorXXX — это класс, динамически генерируемый для каждого метода, который необходимо вызывать путем отражения, а последний XXX — это число, которое продолжает увеличиваться. И все отражения методов должны сначала использовать NativeMethodAccessorImpl.После 15 вызовов по умолчанию создается класс GeneratedMethodAccessorXXX.После генерации он перейдет к методу вызова сгенерированного класса. Итак, как перейти от NativeMethodAccessorImpl к GeneratedMethodAccessorXXX, давайте посмотрим на метод вызова NativeMethodAccessorImpl.
8.jpg
Среди них то, что я сказал выше, это то, что метод ReflectionFactory.inflationThreshold() возвращает 15 раз.Конечно, это 15 не является неизменным.Мы можем указать это с помощью -Dsun.reflect.inflationThreshold=xxx, а также мы можем используйте -Dsun .reflect.noInflation=true, чтобы обойти 15 вызовов NativeMethodAccessorImpl выше, так же, как -Dsun.reflect.inflationThreshold=0 GeneratedMethodAccessorXXX создается с помощью new MethodAccessorGenerator().generateMethod. После создания ему присваивается значение DelegatingMethodAccessorImpl, так что Method.invoke будет передан этому вновь созданному MethodAccessor в следующий раз.

Так что выглядит генерируемый Methodaccessorxxxx, вероятно, понравится это

9.jpg
По сути, это прямой вызов конкретного метода целевого объекта, который ничем не отличается от обычного вызова метода.

Загрузчик классов для GeneratedMethodAccessorXXX

Какой загрузчик классов загружает GeneratedMethodAccessorXXX? После создания байт-кода будет вызван следующий метод для определения класса

10.jpg

Таким образом, загрузчик класса GeneratedMethodAccessorXXX на самом деле является загрузчиком класса DelegatingClassLoader.

Причина использования нового загрузчика классов связана с соображениями производительности. В некоторых случаях эти сгенерированные классы могут быть выгружены, поскольку выгрузка класса повторно используется только в том случае, если загрузчик классов может быть повторно использован. Если использование исходного загрузчика классов может вызвать эти новые созданные классы не могут быть выгружены все время.С точки зрения дизайна не ожидается, что они всегда будут существовать в памяти.Достаточно иметь их по мере необходимости.При нехватке памяти память можно освободить

Параллелизм приводит к созданию класса мусора

Я не знаю, нашли ли вы здесь проблему. Вышеприведенный NativeMethodAccessorImpl.invoke на самом деле разблокирован. Что это значит? Если параллелизм высокий, значит ли это, что может быть много потоков, входящих в логику создания класса GeneratedMethodAccessorXXX одновременно, хотя в итоге будет использоваться только один, но эти накладные расходы уже существуют?Если потоков 1000 Все входят в логику создания GeneratedMethodAccessorXXX, а это значит, что было создано 999 бесполезных классов, и эти классы всегда будут занимать память, пока не произойдет GC, способный переработать Perm.

Так что же это за метод, который продолжает размышлять?

При вышеприведенном понимании принципа отражения мы знаем, что после того, как отражение будет выполнено определенное количество раз, будет динамически построен класс, и соответствующий метод целевого объекта будет вызываться непосредственно в этом классе, как мы можем см. из дампа кучи.Существует большое количество загрузчиков классов DelegatingClassLoader, которые загружают классы GeneratedMethodAccessorXXX, так что какие методы вызывают эти классы, поэтому мы должны сделать одно, то есть сбросить все эти классы в память, а затем сделать байт-код Статистический анализ

Дамп байт-кода класса во время выполнения

Мы можем использовать интерфейс SA для дампа соответствующего класса из coredump или живого процесса.Чтобы вывести наш конкретный класс, сначала мы пишем класс фильтра

11.jpg

После компиляции класса с использованием JAR-файла SA ($JAVA_HOME/lib/sa-jdi.jar) мы вызываем следующую команду в каталоге скомпилированного класса для создания дампа

12.jpg

Таким образом мы можем сбросить все GeneratedMethodAccessor.В это время мы можем посмотреть байт-код класса через javap -verbose GeneratedMethodAccessor9

12.jpg

См. Линию над ключом BCI 36, метод здесь является метод нашего вызова размышлений, например, метод вызова об отражении выше, является ORG / CodeHaus / xfire / util / parmrreader.readcode

Найдите определенные классы и методы отражения

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

13.jpg
Получив эту подсказку, я вижу, где в коде есть логика отражения для вызова этих методов модели, но, к сожалению, я ее не нашел, но этот объект модели, скорее всего, появится при определенных обстоятельствах, то есть при десериализации rpc. , Наконец, я спросил у бизнес-партии, какой сервис Xfire использовался, и с моим многолетним опытом разработки фреймворков я определил, что Xfire десериализует объекты посредством отражения.Конкретный код выглядит следующим образом (org.codehaus.xfire.aegis.type. базовый .BeanType.writeProperty):
14.jpg
Метод get/set в PropertyDeor javabean фактически упакован самим SoftReference.
14.jpg

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

G1 Recycles Пермь

Обратите внимание, что бизнес-система использует G1 из JDK7, а G1 из JDK7 не будет перезапускать perm при нормальных обстоятельствах. Perm будет восстановлен только во время полного GC. Эти объекты Softreference будут переработаны, но вновь созданные классы фактически не будут переработаны, поэтому чем чаще G1 GC, тем легче перерабатывать объекты SoftReference (хотя при нормальных обстоятельствах время истекло, но если GC не является частым, даже если время истекло, оно останется в памяти) , чем проще его переработать, тем проще генерировать новые классы, пока не произойдет Full GC

решение

  • Обновление до jdk8, классы могут быть выгружены во время G1 GC
  • Переход на протокол сериализации без отражения метода, например гессиана.
  • Настройте параметр SoftRefLRUPolicyMSPerMB на увеличение, но это не может устранить основную причину.

Суммировать

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

В этой системе используется G1 под JDK7, а эта версия G1 будет выгружать классы в Перми только при выполнении Full GC.Система вызывает частое выполнение G1 GC из-за большого количества запросов.При этом система также устанавливает -XX :SoftRefLRUPolicyMSPerMB=0, что означает, что жизненный цикл SoftReference не будет пересекать цикл GC и может быть быстро перезапущен.В этой системе большое количество вызовов RPC.Используется протокол Xfire, и возвращаемый результат является десериализованным. Логика Method.invoke и связанный с ним метод поэтому ссылаются на SoftReference, поэтому его легко переработать. После повторного использования создается новый объект метода, а затем вызывается его метод вызова. После определенного числа вызовов (15 раз), построить новый класс байт-кода, наряду с ходом GC, класс байт-кода одного и того же метода непрерывно создается до тех пор, пока Perm не заполнится и не будет запущен полный GC.

Рекомендуемое чтение

Одноразовая производственная практика оптимизации ЦП 100% устранения неполадок

Разговор об оптимизации GC Java-приложений из практических кейсов