Solución de problemas de YGC, (del intercambio de grandes)

  Reimpreso de: https://mp.weixin.qq.com/s/O0l-d928hr994OpSNw3oow Luo Junwu IT People  's lugar de trabajo

 

En condiciones de alta concurrencia, el problema de GC de los programas Java es un tipo de problema muy típico, y el impacto a menudo se magnificará aún más. Independientemente de que "la frecuencia del GC es demasiado rápida" o "el GC tarda demasiado", debido al problema de Stop The World durante el GC, es fácil provocar un tiempo de espera del servicio y problemas de rendimiento.

El sistema de publicidad del que es responsable nuestro equipo realizó un tráfico relativamente grande en el lado C. El volumen de solicitudes durante el período pico alcanzó básicamente miles de QPS. En el pasado, también encontramos muchos problemas en línea relacionados con GC.

En este artículo de mayo, presenté un caso en el que la GC completa es demasiado frecuente y resumí sistemáticamente la estructura de la memoria del montón de JVM y los principios de GC.

En este artículo, compartiré un caso en línea más complicado de Young GC que lleva demasiado tiempo. Al mismo tiempo, resolveré los puntos de conocimiento relacionados con YGC, espero que puedas ganar algo. El contenido se divide en las siguientes 2 partes:

  • Comencemos con un caso en el que YGC lleva demasiado tiempo

  • Resumen de los puntos de conocimiento relacionados con YGC

 

01 Partiendo de un caso en el que YGC tarda demasiado

En abril de este año, después del lanzamiento de la nueva versión de nuestro servicio de publicidad, recibimos una gran cantidad de alarmas de tiempo de espera del servicio. Puede ver en el siguiente cuadro de monitoreo: La cantidad de tiempo de espera aumentó repentinamente en un área grande, e incluso miles de los tiempos de espera de la interfaz se alcanzaron en 1 minuto. El proceso de solución de problemas de este problema se describe en detalle a continuación.

1. Verificar y monitorear

Después de recibir la alerta, verificamos el sistema de monitoreo por primera vez e inmediatamente descubrimos la anormalidad de que YoungGC tardó demasiado. Nuestro programa se lanzó aproximadamente a las 21:50. La siguiente figura muestra que YGC se completó básicamente en decenas de milisegundos antes de que se pusiera en línea, pero el tiempo que tardó YGC en ser significativamente más largo después de estar en línea, el más largo incluso llegó a más de 3 segundos.

Dado que el programa detendrá el mundo durante YGC, y el tiempo de espera del servicio establecido por nuestro sistema ascendente es de varios cientos de milisegundos, se infiere que se debe a que YGC tarda demasiado en provocar una gran área de tiempo de espera del servicio.

De acuerdo con el proceso regular de resolución de problemas de los problemas de GC, inmediatamente eliminamos un nodo y luego volcamos el archivo de memoria de pila para mantener la escena a través del siguiente comando.

jmap -dump: formato = b, archivo = pid del montón

Finalmente, el servicio en línea se retiró. Después de la restauración, el servicio volvió inmediatamente a la normalidad. El siguiente paso es un proceso de resolución de problemas y reparación de un día.

2. Confirme la configuración de JVM

Con el siguiente comando, verificamos nuevamente los parámetros de JVM

ps aux | grep "applicationName = adsearch"

-Xms4g -Xmx4g -Xmn2g -Xss1024K 

-XX: ParallelGCThreads = 5 

-XX: + UseConcMarkSweepGC 

-XX: + UseParNewGC 

-XX: + UseCMSCompactAtFullCollection 

-XX: CMSInitiatingOccupancyFraction = 80

Se puede ver que la memoria del montón es 4G, la nueva generación y la vieja generación son 2G y la nueva generación usa el colector ParNew.

Luego use el comando jmap -heap pid para averiguar: El área de Eden de la nueva generación es 1.6G, y las áreas S0 y S1 son ambas 0.2G.

Este lanzamiento no modificó ningún parámetro relacionado con la JVM, y el número de solicitudes de nuestros servicios fue básicamente el mismo que de costumbre. Así que adivina: es probable que este problema esté relacionado con el código en línea.

3. Verifique el código

Volviendo al principio de YGC para pensar en este problema, el proceso de un YGC incluye principalmente los siguientes dos pasos:

1. Escanee objetos de GC Root y marque los objetos supervivientes

2. Copie el objeto superviviente en el área S1 o asciéndalo al área Antigua

De acuerdo con el cuadro de seguimiento a continuación, se puede ver que, en circunstancias normales, la tasa de uso del área de Supervivientes se ha mantenido en un nivel muy bajo (alrededor de 30 millones), pero después de conectarse, la tasa de uso del área de Supervivientes comenzó a disminuir. fluctuar, y como mucho, casi ocupó 0.2 G arriba. Además, el tiempo consumido por YGC está básicamente correlacionado positivamente con la tasa de uso de la zona Survivor. Por lo tanto, especulamos que debería haber cada vez más objetos de larga duración, lo que lleva a un aumento en el proceso de etiquetado y copia que consume mucho tiempo.

Volviendo al rendimiento general del servicio: el tráfico ascendente no ha cambiado significativamente. En circunstancias normales, el tiempo de respuesta de la interfaz principal es básicamente de 200 ms, y la frecuencia de YGC es aproximadamente una vez cada 8 segundos.

Obviamente, para las variables locales, se pueden recuperar inmediatamente después de cada YGC. Entonces, ¿por qué tantos objetos siguen vivos después de YGC?

Además, bloqueamos el objeto sospechoso en: las variables globales del programa o las variables estáticas de clase. Sin embargo, diferimos el código que se lanzó esta vez y no encontramos que tales variables se introdujeran en el código.

4. Analice el archivo de memoria del montón de volcado

Después de que la investigación del código no progresó, comenzamos a buscar pistas en el archivo de memoria del montón, usamos la herramienta MAT para importar el archivo del montón descargado en el paso 1 y luego vimos todos los objetos grandes en el montón actual a través del árbol Dominator. vista.

Inmediatamente descubrí que la clase NewOldMappingService ocupa mucho espacio. Se ubica a través del código: Esta clase está ubicada en el paquete de cliente de terceros y es proporcionada por el equipo de productos de nuestra empresa para lograr la conversión entre categorías nuevas y antiguas. ( Recientemente, el equipo de producto está en la categoría El sistema se transforma, para que sea compatible con el negocio anterior, es necesario mapear las categorías nuevas y antiguas).

Mirando más en el código, se encuentra que hay una gran cantidad de HashMaps estáticos en esta clase, que se utilizan para almacenar en caché los diversos datos que deben usarse cuando se convierten las categorías nuevas y antiguas, a fin de reducir las llamadas RPC. y mejorar el rendimiento de conversión.

Originalmente pensé que estaba muy cerca de la verdad del problema, pero una investigación en profundidad descubrió que todas las variables estáticas de esta clase se inicializan cuando se carga la clase. Aunque ocupará más de 100M de memoria, no lo hará. se agregarán más tarde. Además, esta clase se lanzó ya en marzo y la versión del paquete del cliente no ha cambiado.

Después del análisis anterior, este tipo de HashMap estático siempre sobrevivirá. Después de múltiples rondas de YGC, eventualmente será promovido a la vejez. No debería ser la razón por la que YGC continúa demorando demasiado. Por lo tanto, descartamos temporalmente este punto sospechoso.

5. Analizar el consumo de tiempo de YGC para procesar la Referencia

El equipo tiene muy poca experiencia en la resolución de problemas de YGC y no sé cómo analizarlo más. Básicamente, analicé todos los casos disponibles en Internet y descubrí que las razones se concentraban en estas dos categorías:

1. Se tarda demasiado en etiquetar objetos activos: por ejemplo, el método Finalize de la clase Object está sobrecargado, lo que hace que el etiquetado de la Referencia final tarde demasiado; o el método String.intern se utiliza incorrectamente, lo que provoca que YGC para escanear StringTable durante demasiado tiempo.

2. Acumulación excesiva de objetos a largo plazo: por ejemplo, el uso inadecuado de las memorias caché locales acumula demasiados objetos supervivientes, o la competencia de bloqueos grave provoca un bloqueo de subprocesos y el ciclo de vida de las variables locales se alarga.

Para el primer tipo de problema, la referencia de procesamiento de GC que consume mucho tiempo se puede mostrar a través de los siguientes parámetros -XX: + PrintReferenceGC. Después de agregar este parámetro, puede ver que el tiempo de procesamiento para diferentes tipos de referencias es muy corto, por lo que se excluye este factor.

6. Volver al objeto de análisis a largo plazo

Más tarde, agregamos varios parámetros de GC e intentamos encontrar pistas en vano. A partir de un seguimiento exhaustivo y varios análisis: solo los objetos a largo plazo deberían causarnos este problema.

Después de lanzar durante varias horas, al final, un pequeño compañero encontró el segundo punto de sospecha en la memoria del montón de MAT.

En la captura de pantalla anterior, podemos ver que la clase ConfigService que ocupa el tercer lugar entre los objetos grandes ha entrado en nuestro campo de visión. Una variable ArrayList de esta clase en realidad contiene objetos de 270 W, y la mayoría de ellos son los mismos elementos.

La clase ConfigService está en el paquete Apollo de terceros, pero el departamento de arquitectura de la empresa ha vuelto a modificar el código fuente. En el código, se puede ver que el problema está en la línea 11 y se agregan elementos a la lista. cada vez que se llama al método getConfig., Y no realizó el procesamiento de deduplicación.

Nuestro servicio de publicidad almacena una gran cantidad de configuraciones de estrategias publicitarias en apollo, y la mayoría de las solicitudes llamarán al método getConfig de ConfigService para obtener la configuración, por lo que constantemente se agregan nuevos objetos a los espacios de nombres de variables estáticas, lo que causa este problema.

En este punto, todo el problema finalmente salió a la luz. Este ERROR fue introducido accidentalmente por el departamento de arquitectura durante el desarrollo personalizado del paquete de cliente apollo. Obviamente, no se ha probado cuidadosamente y se lanzó al almacén central justo el día antes de que nos pusiéramos en línea. La versión básica de la empresa biblioteca de componentes aprobada El método super-pom se mantiene uniformemente y no hay percepción comercial.

7. Solución

Para verificar rápidamente que YGC tardó demasiado en ser causado por este problema, reemplazamos directamente la versión anterior del paquete del cliente apollo en un servidor y luego reiniciamos el servicio. Después de observar durante casi 20 minutos, YGC volvió a la normalidad.

Finalmente, notificamos al Departamento de Arquitectura para corregir el ERROR y relanzamos el super-pom, que resolvió completamente el problema.

02 Resumen de los puntos de conocimiento relevantes de YGC

A través del caso anterior, puede ver que los problemas de YGC son en realidad más difíciles de solucionar. Comparado con FGC u OOM, el registro de YGC es muy simple. Solo conoce los cambios y el tiempo que consume la nueva generación de memoria. Al mismo tiempo, la memoria del montón descargada debe ser revisada cuidadosamente.

Además, si no conoce el proceso de YGC, será más difícil solucionarlo. Aquí, ordenaré los puntos de conocimiento relacionados con YGC, para que todos puedan entender YGC de manera más completa.

1. 5 preguntas para volver a comprender la nueva generación

YGC se lleva a cabo en el Cenozoico Primero, la división de la estructura del montón del Cenozoico debe ser clara. La nueva generación está dividida en un área de Edén y dos áreas de Supervivientes, donde Edén: de: a = 8: 1: 1 (la proporción se puede establecer mediante el parámetro -XX: Razón de Superviviente) Este es el conocimiento más básico.

¿Por qué hay una nueva generación?

Si no hay generación, todos los objetos están en un área y cada GC necesita escanear todo el montón, lo que causa problemas de eficiencia. Después de generaciones, la frecuencia de recuperación se puede controlar por separado y se pueden utilizar diferentes algoritmos de recuperación para garantizar el rendimiento óptimo global del GC.

¿Por qué la nueva generación adopta el algoritmo de replicación?

Los objetos de la nueva generación están vivos y muriendo, alrededor del 90% de los objetos recién creados se pueden reciclar rápidamente, el costo del algoritmo de copia es bajo y se garantiza que el espacio está libre de fragmentación. Aunque el algoritmo de marcado y clasificación también puede garantizar que no haya fragmentación, debido a la gran cantidad de objetos a limpiar en la nueva generación, se requiere una gran cantidad de operaciones de movimiento antes de que los objetos supervivientes se clasifiquen en los objetos a limpiar. , y la complejidad del tiempo es mayor que la del algoritmo de copia.

¿Por qué la nueva generación necesita dos áreas de Supervivientes?

Para ahorrar espacio, si se usa el algoritmo de copia tradicional y solo hay un área de Superviviente, el tamaño del área de Superviviente debe ser igual al tamaño del área de Edén. En este momento, el consumo de espacio es 8 * 2 , y dos Supervivientes pueden mantener nuevos objetos siempre creados en el área del Edén. Los objetos supervivientes se pueden transferir entre Supervivientes, y el consumo de espacio es 8 + 1 + 1. Obviamente, la tasa de utilización del espacio de este último es mayor.

¿Cuál es el espacio real disponible para la nueva generación?

Después de YGC, siempre hay un área de Superviviente que está libre, por lo que el espacio de memoria disponible de la nueva generación es del 90%. Al ver el espacio de la nueva generación en el registro de YGC o mediante el comando jmap -heap pid, no se sorprenda si encuentra que la capacidad es solo del 90%.

¿Cómo acelera el área de Eden la asignación de memoria?

La máquina virtual HotSpot utiliza dos técnicas para acelerar la asignación de memoria. Son bump-the-pointer y TLAB (Thread Local Allocation Buffers).

Dado que el área del Edén es continua, el puntero solo necesita verificar si hay suficiente memoria detrás del último objeto cuando se crea el objeto, lo que acelera la asignación de memoria.

La tecnología TLAB es para múltiples subprocesos. En Eden, se asigna una región para cada subproceso para reducir los conflictos de bloqueo durante la asignación de memoria, acelerar la asignación de memoria y mejorar el rendimiento.

2. Cuatro tipos de recolectores de la nueva generación

SerialGC (Serial Collector), el más antiguo, ejecución de un solo subproceso, adecuado para escenarios de una sola CPU.

ParNew (Parallel Collector), el recopilador en serie es multiproceso, adecuado para escenarios de múltiples CPU y debe usarse con el antiguo recopilador CMS.

ParallelGC (Parallel Collector) se diferencia de ParNew en que se centra en el rendimiento y puede establecer el tiempo de pausa esperado, ajusta automáticamente el tamaño del montón y otros parámetros cuando funciona.

G1 (Garage-First Collector), el recopilador predeterminado de JDK 9 y versiones posteriores, tiene en cuenta la nueva generación y la generación anterior. Divide el montón en una serie de regiones y no requiere bloques de memoria continuos. La nueva generación es todavía recogidos en paralelo.

Todos los recopiladores mencionados anteriormente utilizan el algoritmo de replicación, son exclusivos y detendrán el mundo durante la ejecución.

3. Tiempo de activación de YGC

Cuando el área del Edén es insuficiente, se activará YGC. Veamos el proceso detallado junto con la asignación de memoria de los objetos de nueva generación:

1. El nuevo objeto primero intentará asignarlo en la pila. Si no funciona, intente asignarlo en el TLAB. De lo contrario, verá si se cumple la condición del objeto grande. Debe asignarse en el vejez, y finalmente considere solicitar un espacio en el área de Eden.

2. Si no hay un espacio adecuado en el área de Eden, se activa YGC.

3. Durante YGC, se procesan los objetos supervivientes en el área de Edén y el área De superviviente. Si se cumplen las condiciones para la determinación dinámica de la edad o el espacio en el área A superviviente es insuficiente, la vejez se ingresará directamente. El espacio de la vejez no es suficiente, se producirá una promoción fallida., Desencadenando el reciclaje de la vejez. De lo contrario, el objeto superviviente se copia en el área Al superviviente.

4. En este momento, los objetos restantes en el área de Eden y From Survivor son objetos de basura, que se pueden borrar y reciclar directamente.

Además, si el colector CMS se utiliza en la vejez, para reducir el consumo de tiempo de la fase de Observación CMS, también puede desencadenar un YGC, que no se ampliará aquí.

4. El proceso de ejecución de YGC

El algoritmo de replicación adoptado por YGC se divide principalmente en los siguientes dos pasos:

1. Busque GC Roots y copie los objetos a los que se hace referencia en el área S1

2. Recorra el objeto de forma recursiva en el paso 1, copie el objeto al que se hace referencia en el área S1 o promuévalo al área Antigua

Todo el proceso mencionado anteriormente requiere la suspensión de subprocesos comerciales (STW), pero los recopiladores de nueva generación como ParNew se pueden ejecutar en paralelo en múltiples subprocesos para mejorar la eficiencia del procesamiento.

YGC utiliza el algoritmo de análisis de accesibilidad para buscar hacia abajo desde la raíz de GC (el punto de partida del objeto alcanzable) para marcar los objetos que sobreviven actualmente, y luego los objetos sin marcar restantes son los objetos que deben reciclarse.

Los objetos de GC Root que se pueden usar como YGC incluyen los siguientes:

1. Objetos a los que se hace referencia en la pila de máquinas virtuales

2. Objetos referenciados por propiedades estáticas y constantes en el área de métodos.

3. Objetos a los que se hace referencia en la pila de métodos locales

4. Objetos retenidos por cerraduras sincronizadas

5. Registre el SystemDictionary de la clase cargada actualmente

6. Registre la StringTable a la que hace referencia la constante de cadena

7. Objetos con referencias de generaciones cruzadas

8. Objetos en la misma CardTable que GC Root

Entre ellos, es fácil pensar en 1-3 y 4-8 es fácil de pasar por alto, pero es muy probable que sea una entrada clave al analizar los problemas de YGC.

Además, debe tenerse en cuenta que para la referencia intergeneracional en la siguiente figura, el objeto A de la vejez también debe ser parte de la Raíz de GC, pero si la vejez se escanea cada YGC, debe haber una eficiencia. problema. En HotSpot JVM, se introduce la tabla de tarjetas para acelerar el marcado de referencias de generaciones cruzadas.

Card Table, una comprensión simple es una forma de espacio-por-tiempo, porque probablemente hay menos del 1% de los objetos referenciados entre generaciones, por lo que el espacio del montón se puede dividir en páginas de tarjetas con un tamaño de 512 bytes. Si un objeto tiene una referencia de generación cruzada, se puede usar 1 byte para identificar que la página de la tarjeta está en un estado sucio, y el estado de la página de la tarjeta se mantiene aún más a través de la tecnología de barrera de escritura.

Después de atravesar las raíces de GC, puede encontrar el primer lote de objetos supervivientes y luego copiarlos en el área S1. A continuación, hay un proceso de búsqueda y copia recursiva de objetos supervivientes.

Para facilitar el mantenimiento del área de memoria en el área S1, se introducen dos variables de puntero: _saved_mark_word y _top, donde _saved_mark_word representa la ubicación del objeto atravesado actual, y _top representa la ubicación de la memoria asignable actual. objeto entre _saved_mark_word y _top Son todos los objetos que se han copiado pero no escaneado.

Como se muestra en la figura anterior, cada vez que se escanea un objeto, _saved_mark_word avanzará. Durante este período, si hay nuevos objetos, también se copiará al área S1, y _top también avanzará hasta que _saved_mark_word alcance a _top , indicando todos los objetos en el área S1 El recorrido se ha completado.

Hay un detalle que debe tenerse en cuenta: el espacio de destino del objeto de copia no es necesariamente el área S1, pero también puede ser la vejez. Si la edad de un objeto (el número de YGC experimentados) cumple con la condición de determinación de edad dinámica, se promueve directamente a la vejez. La antigüedad del objeto se almacena en la estructura de datos de la palabra de marca del encabezado del objeto Java (si está familiarizado con los bloqueos concurrentes de Java, debe conocer esta estructura de datos. Si no está familiarizado, se recomienda verificar la información para comprender, y no lo ampliaré aquí).

Supongo que te gusta

Origin blog.csdn.net/qq_39809613/article/details/107354568
Recomendado
Clasificación