Después de que el proyecto de la compañía se actualizó a Spring 5.3.x, la cantidad de GC aumentó considerablemente y yo era un estúpido.

fondo del problema

Recientemente, nuestro proyecto se actualizó a Spring Boot 2.4.6 + Spring Cloud 2020.0.x, pero después de la actualización, descubrimos que YoungGC aumentó significativamente y la tasa de asignación de objetos aumentó significativamente, pero la cantidad de objetos promocionados ha aumentado significativamente. no aumentada, lo que prueba que son todos objetos de nueva creación y que serán reciclados próximamente. Veamos el monitoreo de uno de los procesos, en este momento, la tasa de solicitud de http es de aproximadamente 100:

Esto es muy extraño, la tasa de solicitud no es tan grande, pero a través del monitoreo se puede ver que se asignan casi dos gigabytes de memoria por segundo. Antes de la actualización, esta tasa de asignación era de alrededor de 100 a 200 MB, con la misma tasa de solicitud. Entonces, ¿de dónde viene esta memoria extra?

Orientación

Necesitamos echar un vistazo a las estadísticas de varios objetos en la memoria , es decir, usando el comando jmap. Al mismo tiempo, no puede simplemente mirar las estadísticas de objetos supervivientes , porque se puede ver en el seguimiento que no hay demasiados objetos en la vejez, porque los objetos promocionados no han aumentado. sería mejor si pudiéramos excluir los objetos que todavía están vivos ahora. Al mismo tiempo, debido a que GC es bastante frecuente, habrá una vez alrededor de 1s. Entonces, básicamente, no podemos esperar capturar el jmap que queremos al mismo tiempo. Al mismo tiempo, jmap hace que todos los subprocesos entren en punto seguro y, por lo tanto, en STW, lo que tiene cierto impacto en la línea, por lo que jmap no debería ser demasiado frecuente. Por lo tanto, adoptamos la siguiente estrategia:

  1. Expanda una instancia y luego reduzca el tráfico de una instancia a la mitad a través del centro de registro y el limitador actual;
  2. Para esta instancia, ejecute jmap -histo (para contar todos los objetos) y jmap -histo:live (para contar solo objetos vivos) continuamente;
  3. Repita el segundo paso 5 veces, cada intervalo es de 100ms, 300ms, 500ms, 700ms;
  4. Elimine el límite actual de esta instancia y cierre la instancia recién expandida.

A través de estas comparaciones de jmap, encontramos que los principales tipos de objetos en las estadísticas de jmap tienen un marco de resorte:

 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:       7993252      601860528  [B ([email protected])
   2:        360025      296261160  [C ([email protected])
   3:      10338806      246557984  [Ljava.lang.Object; ([email protected])
   4:       6314471      151547304  java.lang.String ([email protected])
   5:         48170      135607088  [J ([email protected])
   6:        314420      126487344  [I ([email protected])
   7:       4591109      110100264  [Ljava.lang.Class; ([email protected])
   8:        245542       55001408  org.springframework.core.ResolvableType
   9:        205234       29042280  [Ljava.util.HashMap$Node; ([email protected])
  10:        386252       24720128  [org.springframework.core.ResolvableType;
  11:        699929       22397728  java.sql.Timestamp ([email protected])
  12:         89150       21281256  [Ljava.beans.PropertyDescriptor; ([email protected])
  13:        519029       16608928  java.util.HashMap$Node ([email protected])
  14:        598728       14369472  java.util.ArrayList ([email protected])

¿Cómo se crea este objeto? ¿Cómo ubicar un objeto creado con frecuencia que ya no está vivo y este tipo de objeto es interno al marco ?

En primer lugar, todo el análisis de montón de MAT (Eclipse Memory Analyzer) + jmap dump no es muy aplicable, porque:

  1. El objeto ya no está vivo . MAT es más adecuado para el análisis de fugas de memoria. Hemos creado muchos objetos inesperados aquí, ocupando mucha memoria, y estos objetos pronto ya no sobrevivirán.
  2. Para los objetos que ya no están vivos, MAT no puede analizar con precisión al creador, principalmente porque no está claro si podemos capturar la información que queremos al descargar, o hay mucho ruido de información.

Aunque este problema no se puede ubicar de esta manera, todavía pongo los resultados del volcado de jmap que recopilé aquí y uso los resultados del análisis MAT para mostrarlos a todos:

Entonces, ¿cuál es el próximo análisis? Y eso es volver a nuestros viejos amigos, JFR + JMC. Como saben los viejos lectores, a menudo uso JFR para localizar problemas en línea. ¿Cómo lo uso aquí? No hay estadísticas de eventos JFR directos sobre qué objetos se crean a menudo , pero hay eventos indirectos, que pueden reflejar indirectamente quién creó tantos objetos. Normalmente lo coloco así:

  1. Compruebe qué subproceso asigna demasiados objetos (Estadísticas de asignación de subprocesos) a través del evento Estadísticas de objetos de asignación de subprocesos.
  2. Analice qué código de punto de acceso puede producir estos objetos mediante el código de punto de acceso (Ejemplo de creación de perfiles de método). Para una cantidad tan grande de objetos, el código para capturar Runnable tiene una alta probabilidad de ser capturado y representa una alta proporción de eventos.

Primero verifique el evento Estadísticas de asignación de subprocesos y descubra que básicamente todos los subprocesos de servlet (es decir, el subproceso que maneja las solicitudes Http, usamos Undertow, por lo que el nombre del subproceso comienza con XNIO), tienen muchos objetos asignados, que no pueden ubicar el problema:

Luego, veamos las estadísticas del código activo, haga clic en el evento de ejemplo de creación de perfiles de métodos y vea las estadísticas de seguimiento de la pila para ver qué cuentas son relativamente altas.

Se encuentra que la proporción superior parece estar relacionada con este ResolvableType. Para localizar más, haga doble clic en el primer método para ver las estadísticas de la pila de llamadas:

Descubrimos que se llama BeanUtils.copyProperties. Vea otras llamadas relacionadas con ResolvableType, todas relacionadas con BeanUtils.copyProperties. Este método es un método de uso frecuente en nuestro proyecto para la copia de propiedades del mismo tipo o entre diferentes tipos. ¿Por qué este método crea tantos ResolvableTypes?

Ver el código fuente y la ubicación del problema

Al observar el código fuente, encontramos que a partir de Spring 5.3.x, BeanUtils comenzó a replicar propiedades al crear una encapsulación de información de clase unificada de 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 {
}

El código fuente interno crea un nuevo ResolvableType para cada método de atributo del tipo del objeto de origen y el objeto de destino cada vez, y no realiza el almacenamiento en caché . Esto da como resultado una copia, que crea una gran cantidad de ResolvableTypes. Experimentemos:

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);
        }
    }
}

Use spring-beans 5.2.16.RELEASE y spring-beans 5.3.9 para ejecutar este código respectivamente. Los parámetros de JVM usan -XX:+
UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx512m. Estos parámetros significan, use EpsilonGC , es decir, cuando la memoria de almacenamiento dinámico está llena, no se realiza GC, se lanza directamente una excepción OutofMemory y el programa finaliza, y la memoria de almacenamiento dinámico máxima es de 512 m. De esta manera, el programa realmente ve: cuántas versiones diferentes de BeanUtils.copyProperties se pueden ejecutar antes de que se agote la memoria .

Los resultados de la prueba son: spring-beans 5.2.16.RELEASE es 444489 veces, spring-beans 5.3.9 es 27456 veces. Esa es una gran diferencia .

Entonces, en respuesta a este problema, planteé un Problema en spring-framework github.

Luego, para los lugares donde BeanUtils.copyProperties se usa a menudo en el proyecto, reemplácelo con BeanCopier y encapsule una clase simple:

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);
    }
}

Sin embargo, debe tenerse en cuenta que el problema más directo de que BeanCopier reemplace a BeanUtils.copyProperties es que no se puede copiar para propiedades con nombres diferentes pero con nombres diferentes. Por ejemplo, uno es un int y el otro es un entero. Al mismo tiempo, existen algunas diferencias en la copia profunda, lo que requiere que hagamos pruebas unitarias.

Después de la modificación, el problema está resuelto.

Enlace original:
https://www.cnblogs.com/zhxdick/p/15110071.html

Autor: Zhang Hash lleno de productos secos

Si cree que este artículo es útil para usted, puede retuitear, seguir y apoyar

Supongo que te gusta

Origin blog.csdn.net/m0_67645544/article/details/124435674
Recomendado
Clasificación