Recordar un proceso de solución de problemas de pérdida de memoria nativa | Equipo técnico de JD Cloud

1 Problema fenómeno

El servicio de cálculo de rutas es el servicio central del sistema de rutas, responsable del cálculo del plan de ruta de la carta de porte y la correspondencia entre la operación real y el plan. En el proceso de operación y mantenimiento, se encuentra que el TP99 sube lentamente la pendiente cuando no se reinicia durante mucho tiempo. Además, durante el cálculo de prueba de la programación de la rutina semanal, se puede ver claramente el aumento en la memoria. Las siguientes capturas de pantalla son el seguimiento de estas dos situaciones anormales.

Escalada TP99

rampa de memoria

La configuración de la máquina es la siguiente

CPU: 16C RAM: 32G

La configuración de JVM es la siguiente:

-Xms20480m (luego cambiado a 8GB) -Xmx20480m (luego cambiado a 8GB) -XX:MaxPermSize=2048m -XX:MaxGCPauseMillis=200 -XX:+ParallelRefProcEnabled -XX:+PrintReferenceGC -XX:+UseG1GC -Xss256k -XX:ParallelGCThread s = 16 -XX:ConcGCThreads=4 -XX:MaxDirectMemorySize=2g -Dsun.net.inetaddr.ttl=600 -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dlog4j2.asyncQueueFullPolicy=Discard -XX : MetaspaceSize=1024M -XX:G1NewSizePercent=35 -XX:G1MaxNewSizePercent=35

Programación de tareas de rutina:

La ejecución se activa todos los lunes a las 2:00 a. m. La captura de pantalla anterior contiene un total de dos ciclos de tareas. Se puede ver que en la primera ejecución, la memoria subió directamente del 33% al 75%. En la segunda ejecución, después de subir al 88 %, OOM salió de forma anormal.

2 Solución de problemas

Como hay dos fenómenos, hay dos líneas principales de investigación. El primero es verificar el uso de la memoria con el fin de rastrear la causa de OOM, lo que se conoce como solución de problemas de memoria. El segundo elemento es la investigación de las razones del lento crecimiento de TP99, denominado solución de problemas de degradación del rendimiento.

2.1 Solución de problemas de degradación del rendimiento

Dado que es un ascenso lento y el ciclo de rampa está directamente relacionado con el reinicio del servicio, se puede descartar la posibilidad de problemas de rendimiento de la interfaz externa. Primero encuentre la razón de su propio programa. Por lo tanto, primero verifique la situación del GC y la situación de la memoria. El siguiente es el registro de GC que no se ha reiniciado durante mucho tiempo. Este es un YGC y toma 1.16 segundos en total. Entre ellos, el enlace Ref Proc consume 1150,3 ms y la recuperación de JNI Weak Reference consume 1,1420596 segundos. En la máquina recién reiniciada, el tiempo de recuperación de JNI Weak Reference es de 0,0000162 segundos. Por lo tanto, se puede ubicar que el aumento en TP99 es causado por el aumento en el ciclo de reciclaje de JNI Weak Reference.

JNI Weak Reference, como sugiere el nombre, debe estar relacionado con el uso de la memoria nativa. Sin embargo, debido a la dificultad de la solución de problemas de memoria nativa. Por lo tanto, es mejor comenzar la investigación desde el uso del montón y aprovechar la oportunidad para ver si puede encontrar alguna pista.

2.2 Solución de problemas de memoria

Volviendo a la memoria, después del recordatorio de Jiange, el problema debe reproducirse primero. Y las tareas activadas cada semana reproducirán de manera estable el aumento de memoria, por lo que es más fácil de verificar desde la dirección de programación de tareas. Con la ayuda de @柳岩, tengo la capacidad de reproducir problemas en cualquier momento en el entorno de cálculo de prueba.

La resolución de problemas de memoria aún comienza desde la memoria en el montón. Después de varios volcados, aunque el uso total de la memoria del proceso Java siguió aumentando, el uso de la memoria del montón no aumentó significativamente. Después de solicitar el permiso de root y desplegar arthas, a través de la función dashbord de arthas, se puede ver claramente que el montón (heap) y el no montón (nonheap) permanecen estables.

tablero arthas

El uso de la memoria se ha duplicado.

A partir de esto, se puede concluir que el aumento en el uso de la memoria nativa conduce a un aumento en el uso de la memoria de toda la aplicación Java. El primer paso para analizar nativos es habilitar jvm -XX:NativeMemoryTracking=detail.

2.2.1 Use jcmd para ver la situación general de la memoria

jcmd puede imprimir toda la asignación de memoria del proceso Java. Cuando el parámetro NativeMemoryTracking=detail está habilitado, puede ver la información de la pila de llamadas del método nativo. Después de solicitar el permiso de root, puede instalarlo directamente usando yum.

安装好后,执行如下命令。

jcmd <pid> VM.native_memory detail

visualización de resultados jcmd

En la figura anterior, hay dos partes: la primera parte es un resumen de la situación general de la memoria, incluido el uso total de la memoria y el uso de la clasificación. Las categorías incluyen: Montón de Java, Clase, Subproceso, Código, GC, Compilador, Interno, Símbolo, Seguimiento de memoria nativa, Fragmento de arena, Desconocido. Para la introducción de cada categoría, puede leer este documento; la segunda parte son detalles, incluidos cada La dirección inicial y la dirección final de la asignación de memoria del segmento, el tamaño específico y la categoría a la que pertenece. Por ejemplo, la parte de la captura de pantalla describe que se asignan 8 GB de memoria para el almacenamiento dinámico de Java (más adelante, para reproducir rápidamente el problema, el tamaño del almacenamiento dinámico se ajusta de 20 GB a 8 GB). Las líneas sangradas detrás representan la asignación específica de memoria.

Use jcmd dump dos veces en un intervalo de 2 horas para comparar. Puedes ver que la parte Interna ha crecido significativamente. ¿Qué es Internal y por qué está creciendo? Después de Google, descubrí que hay muy pocas introducciones en esta área, básicamente se trata de análisis de línea de comandos, JVMTI y otras llamadas. Después de consultar a @崔立园, aprendí que JVMTI puede estar relacionado con el agente de Java. En el cálculo de enrutamiento, solo pfinder debería estar relacionado con el agente de Java, pero el problema del middleware subyacente no solo debería afectar el enrutamiento, así que solo pregunté sobre la investigación de pfinder y desarrollo y no siguió invirtiendo en el seguimiento.

2.2.2 Usando pmap y gdb para analizar la memoria

En primer lugar, se da la conclusión de este método. Dado que este análisis contiene conjeturas relativamente grandes, no se recomienda probarlo primero. La idea general es usar pmap para generar toda la memoria asignada por el proceso java, seleccionar rangos de memoria sospechosos, usar gdb para volcar y codificar y visualizar su contenido para el análisis.
Hay muchos blogs relacionados en Internet, todos los cuales ubican el caso de fuga de enlace analizando la existencia de una gran cantidad de bloques de asignación de memoria de 64 MB. Así que también revisé nuestro proceso, y contiene una gran cantidad de uso de memoria de aproximadamente 64 MB. De acuerdo con la introducción en el blog, después de codificar la memoria, la mayor parte del contenido está relacionado con JSF, que se puede inferir que es el grupo de memoria utilizado por JSF netty. La versión 1.7.4 de JSF que estamos usando no tiene fugas en el grupo de memoria, por lo que no debería estar relacionado.
pmap: https://docs.oracle.com/cd/E56344_01/html/E54075/pmap-1.html
gdb: https://segmentfault.com/a/1190000024435739

2.2.3 Usar strace para analizar llamadas al sistema

Esto debe considerarse como una especie de método de análisis de la suerte. La idea es usar strace para generar la llamada al sistema para cada asignación de memoria y luego hacerla coincidir con los subprocesos en jstack. Para determinar qué subproceso de Java asignó la memoria nativa. Este tipo de eficiencia es la más baja.En primer lugar, las llamadas al sistema son muy frecuentes, especialmente en los servicios con más RPC. Por lo tanto, a excepción de las fugas de memoria más obvias, es fácil solucionar los problemas de esta manera. Las fugas de memoria lentas como las de este artículo básicamente quedarán ocultas por las llamadas normales, lo que dificultará su observación.

2.3 Ubicación del problema

Después de una serie de intentos, no se localizó la causa raíz. Entonces solo podemos comenzar con el fenómeno del crecimiento de la memoria interna detectado por jcmd nuevamente. Hasta ahora, todavía existe la pista de los detalles de asignación de memoria que no se ha analizado. Aunque hay 1.2w líneas de registros, solo puedo revisarlo, con la esperanza de encontrar pistas relacionadas con Interno.

A través del siguiente párrafo, puede ver que después de asignar 32k de espacio de memoria interna, hay dos asignaciones de memoria relacionadas con JNIHandleBlock, a saber, 4 GB y 2 GB, y las llamadas relacionadas con MemberNameTable asignan 7 GB de memoria.

[0x00007fa4aa9a1000 - 0x00007fa4aa9a9000] reserved and committed 32KB for Internal from
    [0x00007fa4a97be272] PerfMemory::create_memory_region(unsigned long)+0xaf2
    [0x00007fa4a97bcf24] PerfMemory::initialize()+0x44
    [0x00007fa4a98c5ead] Threads::create_vm(JavaVMInitArgs*, bool*)+0x1ad
    [0x00007fa4a952bde4] JNI_CreateJavaVM+0x74

[0x00007fa4aa9de000 - 0x00007fa4aaa1f000] reserved and committed 260KB for Thread Stack from
    [0x00007fa4a98c5ee6] Threads::create_vm(JavaVMInitArgs*, bool*)+0x1e6
    [0x00007fa4a952bde4] JNI_CreateJavaVM+0x74
    [0x00007fa4aa3df45e] JavaMain+0x9e
Details:

[0x00007fa4a946d1bd] GenericGrowableArray::raw_allocate(int)+0x17d
[0x00007fa4a971b836] MemberNameTable::add_member_name(_jobject*)+0x66
[0x00007fa4a9499ae4] InstanceKlass::add_member_name(Handle)+0x84
[0x00007fa4a971cb5d] MethodHandles::init_method_MemberName(Handle, CallInfo&)+0x28d
                             (malloc=7036942KB #10)

[0x00007fa4a9568d51] JNIHandleBlock::allocate_handle(oopDesc*)+0x2f1
[0x00007fa4a9568db1] JNIHandles::make_weak_global(Handle)+0x41
[0x00007fa4a9499a8a] InstanceKlass::add_member_name(Handle)+0x2a
[0x00007fa4a971cb5d] MethodHandles::init_method_MemberName(Handle, CallInfo&)+0x28d
                             (malloc=4371507KB #14347509)

[0x00007fa4a956821a] JNIHandleBlock::allocate_block(Thread*)+0xaa
[0x00007fa4a94e952b] JavaCallWrapper::JavaCallWrapper(methodHandle, Handle, JavaValue*, Thread*)+0x6b
[0x00007fa4a94ea3f4] JavaCalls::call_helper(JavaValue*, methodHandle*, JavaCallArguments*, Thread*)+0x884
[0x00007fa4a949dea1] InstanceKlass::register_finalizer(instanceOopDesc*, Thread*)+0xf1
                             (malloc=2626130KB #8619093)

[0x00007fa4a98e4473] Unsafe_AllocateMemory+0xc3
[0x00007fa496a89868]
                             (malloc=239454KB #723)

[0x00007fa4a91933d5] ArrayAllocator<unsigned long, (MemoryType)7>::allocate(unsigned long)+0x175
[0x00007fa4a9191cbb] BitMap::resize(unsigned long, bool)+0x6b
[0x00007fa4a9488339] OtherRegionsTable::add_reference(void*, int)+0x1c9
[0x00007fa4a94a45c4] InstanceKlass::oop_oop_iterate_nv(oopDesc*, FilterOutOfRegionClosure*)+0xb4
                             (malloc=157411KB #157411)

[0x00007fa4a956821a] JNIHandleBlock::allocate_block(Thread*)+0xaa
[0x00007fa4a94e952b] JavaCallWrapper::JavaCallWrapper(methodHandle, Handle, JavaValue*, Thread*)+0x6b
[0x00007fa4a94ea3f4] JavaCalls::call_helper(JavaValue*, methodHandle*, JavaCallArguments*, Thread*)+0x884
[0x00007fa4a94eb0d1] JavaCalls::call_virtual(JavaValue*, KlassHandle, Symbol*, Symbol*, JavaCallArguments*, Thread*)+0x321
                             (malloc=140557KB #461314)

Al comparar la salida de jcmd en los dos períodos de tiempo, podemos ver que la asignación de memoria relacionada con JNIHandleBlock continúa creciendo. Por lo tanto, se puede concluir que es la asignación de memoria de JNIHandles::make_weak_global lo que provoca la fuga. Entonces, ¿qué está haciendo esta lógica y qué está causando la fuga?

A través de Google, encontré el artículo de Jvm God, que nos respondió toda la pregunta. El fenómeno del problema es básicamente consistente con el nuestro. Blog: https://blog.csdn.net/weixin_45583158/article/details/100143231

Entre ellos, Han Quanzi dio un código para reproducir el problema. Hay una sección casi idéntica de nuestro código, que implica suerte.

// 博客中的代码
public static void main(String args[]){

        while(true){

            MethodType type = MethodType.methodType(double.class, double.class);

            try {

                MethodHandle mh = lookup.findStatic(Math.class, "log", type);

            } catch (NoSuchMethodException e) {

                e.printStackTrace();

            } catch (IllegalAccessException e) {

                e.printStackTrace();

            }

        }

    }
}

Error jvm: https://bugs.openjdk.org/browse/JDK-8152271

Es el error anterior, el uso frecuente de reflejos relacionados con MethodHandles hará que los objetos caducados no se reciclen y también hará que aumente el tiempo de exploración de YGC, lo que provocará una degradación del rendimiento.

3 resolución de problemas

Dado que jvm 1.8 ha dejado en claro que este problema no se solucionará en 1.8, se refactorizará en java. Pero no podemos actualizar a Java en poco tiempo. Por lo tanto, no hay forma de solucionarlo actualizando directamente la JVM. Dado que el problema es el uso frecuente de la reflexión, se considera agregar un caché para reducir la frecuencia, a fin de resolver los problemas de degradación del rendimiento y fugas de memoria. Teniendo en cuenta el problema de la seguridad de subprocesos, el caché se coloca en ThreadLocal y se agrega la regla de eliminación de LRU para evitar fugas nuevamente.

El efecto final de la reparación es el siguiente: el crecimiento de la memoria se controla dentro del rango de configuración normal de la memoria en montón (8 GB) y la tasa de crecimiento es relativamente moderada. Después de reiniciar durante 2 días, el tiempo de referencia débil de JNI es de 0,0001583 segundos, como se esperaba.

4 Resumen

La idea de resolución de problemas de la fuga de memoria nativa es similar a la de la memoria en montón, principalmente basada en la comparación y el volcado de tiempo compartido. Determine la causa del problema observando los valores atípicos o el crecimiento anormal. Debido a las diferencias en las herramientas y el proceso de solución de problemas de la memoria nativa, es difícil asociar directamente las fugas de memoria con los subprocesos. Puede probar suerte a través de strace. Además, de acuerdo con las pistas limitadas, busque en el motor de búsqueda, puede encontrar el proceso de investigación relevante y recibirá sorpresas inesperadas. Después de todo, jvm sigue siendo un software muy confiable, por lo que si hay problemas serios, debería ser fácil encontrar soluciones relevantes en Internet. Si hay menos contenido en Internet, es posible que aún deba considerar si está confiando en un software que es demasiado especializado.

En términos de desarrollo, intente utilizar patrones de diseño y desarrollo convencionales. Aunque no hay distinción entre buenas y malas tecnologías, los métodos de implementación como la reflexión y AOP deben limitar el alcance del uso. Debido a que estas tecnologías afectarán la legibilidad del código, y el rendimiento empeorará gradualmente en el AOP cada vez mayor. Además, en términos de probar nuevas tecnologías, intente comenzar desde el borde del negocio. En la aplicación principal, lo primero que se debe considerar es el problema de la estabilidad. Este tipo de conciencia puede evitar pisar fosos que son difíciles de encontrar para otros, lo que reduce los problemas innecesarios.

Autor: JD Logística Chen Haolong

Fuente: Comunidad de desarrolladores de JD Cloud

{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/10085734
Recomendado
Clasificación