После обновления до Spring 5.3.x количество GC резко возросло, я туплю

Spring Boot задняя часть Spring
После обновления до Spring 5.3.x количество GC резко возросло, я туплю

Сегодня шестой день моего августовского испытания обновления.

image

image

Недавно наш проект обновился до Spring Boot 2.4.6 + Spring Cloud 2020.0.x, что видно по другой моей серии:Путь обновления Spring Cloud. Однако после обновления мы обнаружили, что YoungGC значительно увеличился, и скорость выделения объектов значительно увеличилась, но продвигаемые объекты не увеличились, что доказывает, что все они являются вновь созданными объектами и могут быть скоро переработаны. Посмотрим на мониторинг одного из процессов, в это время скорость http запросов около 100:

image

Это очень странно, скорость запросов не такая уж и большая, но по мониторингу видно, что в секунду выделяется почти два гигабайта памяти. До обновления эта скорость выделения составляла около 100–200 МБ при той же скорости запросов. Так где же потребляется дополнительная память?

image

нам нужно взглянутьСтатистика различных объектов в памяти,即使用 jmap 命令。 в то же времяНельзя просто просмотреть статистику уцелевших объектов, т.к. по мониторингу видно, что объектов в старости не так уж и много, т.к. раскрученных объектов не прибавилось.Наоборот, было бы лучше, если бы мы могли исключить те объекты, которые еще живы. В то же время, поскольку GC довольно частый, будет один раз около 1 с. Таким образом, в основном мы не можем рассчитывать на получение нужного jmap за один раз. В то же время, jmap заставит все потоки войти в safepoint и, таким образом, STW, что окажет определенное влияние на линию, поэтому jmap не должен быть слишком частым. Поэтому мы придерживаемся следующей стратегии:

  1. Расширить инстанс, а затем вдвое сократить трафик инстанса через центр регистрации и текущий ограничитель;
  2. В этом случае выполняйте непрерывноjmap -histo(для подсчета всех предметов) иjmap -histo:live(Считать только уцелевшие объекты);
  3. Повторите второй шаг 5 раз, каждый интервал 100 мс, 300 мс, 500 мс, 700 мс;
  4. Удалите текущее ограничение этого экземпляра и закройте вновь развернутый экземпляр.

Благодаря этим сравнениям 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Весь этот анализ кучи не очень применим, потому что:

  1. объект больше не жив, MAT больше подходит для анализа утечек памяти.Здесь мы создаем много неожиданных объектов, которые занимают много памяти, и эти объекты скоро перестанут существовать.
  2. MAT для объектов, которые уже не живы, создатель не может быть точно проанализирован, в основном потому, что он не уверен, сможем ли мы захватить нужную информацию при дампе, или имеется много информационного шума.

Хотя эта проблема не может быть локализована таким образом, я все же помещаю сюда результаты дампа jmap, которые я собрал, и использую результаты анализа MAT, чтобы показать их на всеобщее обозрение:

Так какой следующий анализ? И это вернемся к нашим старым друзьям, JFR + JMC. Как известно старым читателям, я часто использую JFR для поиска онлайн-проблем, как мне его использовать здесь?Нет прямой статистики событий JFR, какие объекты часто создаются, однако есть косвенные события, которые могут косвенно отражать, кто создал столько объектов. Я обычно позиционирую так:

  1. Проверьте, какой поток выделяет слишком много объектов с помощью события статистики объекта выделения потока.(Статистика распределения потоков).
  2. Проанализируйте, какой горячий код может генерировать эти объекты с помощью анализа горячего кода.(Пример профилирования метода). Для такого большого количества созданных объектов код для захвата Runnable, скорее всего, будет перехвачен, и на него приходится высокая доля событий.

Сначала проверьте событие статистики распределения потоков и обнаружите, что в основном все потоки сервлета (то есть поток, который обрабатывает запросы Http, мы используем Undertow, поэтому имя потока начинается с XNIO) имеют много выделенных объектов, которые не могут определить местонахождение проблемы. :

image

Затем давайте посмотрим статистику кода точки доступа, нажмите «Образец события профилирования метода», чтобы просмотреть статистику отслеживания стека, чтобы увидеть, какие учетные записи относительно высоки.

image

Обнаружено, что пропорция наибольшей пропорции кажется такой же, как этаResolvableTypeДля дальнейшего позиционирования дважды щелкните первый метод, чтобы просмотреть статистику стека вызовов:

image

Мы обнаружили, что вызовBeanUtils.copyProperties. Посмотреть другиеResolvableTypeсвязанные звонки, как иBeanUtils.copyPropertiesСвязанный. Этот метод является часто используемым методом в нашем проекте для копирования собственности одного и того же типа или между различными типами. Почему этот метод создает так многоResolvableTypeШерстяная ткань?

image

Изучив исходный код, мы обнаружили, что, начиная с 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.RELEASE444489 раз,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, подписывайтесь на официальный аккаунт, чистите каждый день, легко улучшайте свои технологии и получайте различные предложения.:

image