Сегодня шестой день моего августовского испытания обновления.
Недавно наш проект обновился до Spring Boot 2.4.6 + Spring Cloud 2020.0.x, что видно по другой моей серии:Путь обновления Spring Cloud. Однако после обновления мы обнаружили, что YoungGC значительно увеличился, и скорость выделения объектов значительно увеличилась, но продвигаемые объекты не увеличились, что доказывает, что все они являются вновь созданными объектами и могут быть скоро переработаны. Посмотрим на мониторинг одного из процессов, в это время скорость http запросов около 100:
Это очень странно, скорость запросов не такая уж и большая, но по мониторингу видно, что в секунду выделяется почти два гигабайта памяти. До обновления эта скорость выделения составляла около 100–200 МБ при той же скорости запросов. Так где же потребляется дополнительная память?
нам нужно взглянутьСтатистика различных объектов в памяти,即使用 jmap 命令。 в то же времяНельзя просто просмотреть статистику уцелевших объектов, т.к. по мониторингу видно, что объектов в старости не так уж и много, т.к. раскрученных объектов не прибавилось.Наоборот, было бы лучше, если бы мы могли исключить те объекты, которые еще живы. В то же время, поскольку GC довольно частый, будет один раз около 1 с. Таким образом, в основном мы не можем рассчитывать на получение нужного jmap за один раз. В то же время, jmap заставит все потоки войти в safepoint и, таким образом, STW, что окажет определенное влияние на линию, поэтому jmap не должен быть слишком частым. Поэтому мы придерживаемся следующей стратегии:
- Расширить инстанс, а затем вдвое сократить трафик инстанса через центр регистрации и текущий ограничитель;
- В этом случае выполняйте непрерывно
jmap -histo
(для подсчета всех предметов) иjmap -histo:live
(Считать только уцелевшие объекты); - Повторите второй шаг 5 раз, каждый интервал 100 мс, 300 мс, 500 мс, 700 мс;
- Удалите текущее ограничение этого экземпляра и закройте вновь развернутый экземпляр.
Благодаря этим сравнениям jmap мы обнаружили, что основные типы объектов в статистике jmap имеют структуру Spring:
num #instances #bytes class name (module)
-------------------------------------------------------
1: 7993252 601860528 [B (java.base@11.0.8)
2: 360025 296261160 [C (java.base@11.0.8)
3: 10338806 246557984 [Ljava.lang.Object; (java.base@11.0.8)
4: 6314471 151547304 java.lang.String (java.base@11.0.8)
5: 48170 135607088 [J (java.base@11.0.8)
6: 314420 126487344 [I (java.base@11.0.8)
7: 4591109 110100264 [Ljava.lang.Class; (java.base@11.0.8)
8: 245542 55001408 org.springframework.core.ResolvableType
9: 205234 29042280 [Ljava.util.HashMap$Node; (java.base@11.0.8)
10: 386252 24720128 [org.springframework.core.ResolvableType;
11: 699929 22397728 java.sql.Timestamp (java.sql@11.0.8)
12: 89150 21281256 [Ljava.beans.PropertyDescriptor; (java.desktop@11.0.8)
13: 519029 16608928 java.util.HashMap$Node (java.base@11.0.8)
14: 598728 14369472 java.util.ArrayList (java.base@11.0.8)
Как создается этот объект?Как найти часто создаваемый объект, который больше не существует, а тип объекта является внутренним для фреймворка?
Во-первых, MAT (анализатор памяти Eclipse) +jmap dump
Весь этот анализ кучи не очень применим, потому что:
- объект больше не жив, MAT больше подходит для анализа утечек памяти.Здесь мы создаем много неожиданных объектов, которые занимают много памяти, и эти объекты скоро перестанут существовать.
- MAT для объектов, которые уже не живы, создатель не может быть точно проанализирован, в основном потому, что он не уверен, сможем ли мы захватить нужную информацию при дампе, или имеется много информационного шума.
Хотя эта проблема не может быть локализована таким образом, я все же помещаю сюда результаты дампа jmap, которые я собрал, и использую результаты анализа MAT, чтобы показать их на всеобщее обозрение:
Так какой следующий анализ? И это вернемся к нашим старым друзьям, JFR + JMC. Как известно старым читателям, я часто использую JFR для поиска онлайн-проблем, как мне его использовать здесь?Нет прямой статистики событий JFR, какие объекты часто создаются, однако есть косвенные события, которые могут косвенно отражать, кто создал столько объектов. Я обычно позиционирую так:
- Проверьте, какой поток выделяет слишком много объектов с помощью события статистики объекта выделения потока.(Статистика распределения потоков).
- Проанализируйте, какой горячий код может генерировать эти объекты с помощью анализа горячего кода.(Пример профилирования метода). Для такого большого количества созданных объектов код для захвата Runnable, скорее всего, будет перехвачен, и на него приходится высокая доля событий.
Сначала проверьте событие статистики распределения потоков и обнаружите, что в основном все потоки сервлета (то есть поток, который обрабатывает запросы Http, мы используем Undertow, поэтому имя потока начинается с XNIO) имеют много выделенных объектов, которые не могут определить местонахождение проблемы. :
Затем давайте посмотрим статистику кода точки доступа, нажмите «Образец события профилирования метода», чтобы просмотреть статистику отслеживания стека, чтобы увидеть, какие учетные записи относительно высоки.
Обнаружено, что пропорция наибольшей пропорции кажется такой же, как этаResolvableType
Для дальнейшего позиционирования дважды щелкните первый метод, чтобы просмотреть статистику стека вызовов:
Мы обнаружили, что вызовBeanUtils.copyProperties
. Посмотреть другиеResolvableType
связанные звонки, как иBeanUtils.copyProperties
Связанный. Этот метод является часто используемым методом в нашем проекте для копирования собственности одного и того же типа или между различными типами. Почему этот метод создает так многоResolvableType
Шерстяная ткань?
Изучив исходный код, мы обнаружили, что, начиная с Spring 5.3.x,BeanUtils
Начните с созданияResolvableType
Эта унифицированная информация о классе инкапсулируется для репликации атрибутов:
/**
*
* <p>As of Spring Framework 5.3, this method honors generic type information
*/
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
@Nullable String... ignoreProperties) throws BeansException {
}
Внутри исходного кода каждый раз создается новый метод для каждого метода атрибута типа исходного объекта и целевого объекта.ResolvableType
,а такженет кеша. В результате получается одна копия, которая создает большое количествоResolvableType
, Проведем эксперимент:
public class Test {
public static void main(String[] args) {
TestBean testBean1 = new TestBean("1", "2", "3", "4", "5", "6", "7", "8", "1", "2", "3", "4", "5", "6", "7", "8");
TestBean testBean2 = new TestBean();
for (int i = 0; i > -1; i++) {
BeanUtils.copyProperties(testBean1, testBean2);
System.out.println(i);
}
}
}
использовать отдельноspring-beans 5.2.16.RELEASE
а такжеspring-beans 5.3.9
Эти две зависимости используются для выполнения этого кода, используется параметр JVM-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx512m
.Эти параметры означают, что при использовании EpsilonGC, то есть при заполнении памяти кучи, сборка мусора не выполняется, сразу выбрасывается исключение OutofMemory и программа завершается, а максимальный объем памяти кучи составляет 512м. Таким образом, программа фактически видит: до исчерпания памяти,разные версииBeanUtils.copyProperties
Сколько раз каждый может быть выполнен.
Результаты теста:spring-beans 5.2.16.RELEASE
444489 раз,spring-beans 5.3.9
составляет 27456 раз. Это довольно большая разница.
Итак, в ответ на эту проблему я поднял вопрос к гитхабу spring-frameworkIssue.
Тогда, для использования в проектеBeanUtils.copyProperties
место, замените его наBeanCopier
и инкапсулирует простой класс:
public class BeanUtils {
private static final Cache<String, BeanCopier> CACHE = Caffeine.newBuilder().build();
public static void copyProperties(Object source, Object target) {
Class<?> sourceClass = source.getClass();
Class<?> targetClass = target.getClass();
BeanCopier beanCopier = CACHE.get(sourceClass.getName() + " to " + targetClass.getName(), k -> {
return BeanCopier.create(sourceClass, targetClass, false);
});
beanCopier.copy(source, target, null);
}
}
Но следует отметить, что,BeanCopier
заменятьBeanUtils.copyProperties
Одна из самых прямых проблем заключается в том, что его нельзя скопировать для свойств с разными свойствами, но с тем же именем. Например, один тип — int, а другой — Integer. В то же время есть некоторые отличия в глубоком копировании, что требует от нас проведения модульного тестирования.
После модификации проблема решена.
Ищите «My Programming Meow» в WeChat, подписывайтесь на официальный аккаунт, чистите каждый день, легко улучшайте свои технологии и получайте различные предложения.: