Habla sobre Metaspace mediante una pregunta en línea de GC Overhead

Al migrar un determinado servicio, observamos los siguientes datos. Diferentes colores representan diferentes máquinas. Se puede encontrar que muchas máquinas tienen una sobrecarga de GC que alcanza el 100% en diferentes períodos de tiempo. Esto significa que durante este período, la máquina no puede proporcionar servicios externos. Esta es una situación peligrosa y no un accidente.

Análisis de causa

Encontramos el registro de GC de una de las máquinas cuando la sobrecarga de GC alcanzó el 100%, de la siguiente manera:

Desde el registro, podemos Descubra que estos dos Todos los GC son GC completos. En circunstancias normales, solo hay GC jóvenes y GC mixto en G1. Cuando G1 GC no puede cumplir con los requisitos de asignación de memoria, cambiará a GC antiguo en serie para recopilar toda la memoria del montón. Estrictamente hablando, el GC completo no pertenece a G1, sino que es una estrategia de encubrimiento que se utiliza cuando G1 no puede satisfacer la demanda. Además, también podemos encontrar en la marca de tiempo y el tiempo de ejecución que estos dos GC son consecutivos y llevan mucho tiempo. Es por eso que la sobrecarga de GC aumentará al 100%.

El motivo del primer GC es el Umbral de GC de metadatos, lo que significa que es causado por un espacio de MetaSpace insuficiente. Después del primer GC, el espacio de Metaspace no se ha reducido, por lo que se produce el segundo GC. El GC secundario intentará borrar las referencias suaves, pero el espacio de MetaSpace aún no se reduce. Cuando vi esto, mi primera reacción fue que algo andaba mal con MetaSpace. En JDK 1.8, para administrar la memoria de manera más flexible, la generación permanente fue eliminada y reemplazada por Metaspace.

MetaSpace ya no se considera memoria JVM, por lo que debemos usar metaspace + memoria jvm al calcular el uso de memoria. La configuración de los parámetros relacionados con PermSize y MaxPermSize de la generación permanente ya no tendrá efecto. Después de verificar los parámetros de inicio, el metaespacio actual es de solo 512 MB. Cuando se utilizan ampliamente la reflexión, el proxy dinámico y la generación dinámica de funciones JSP, el espacio de Metaspace será insuficiente y no será posible el reciclaje normal.

Solución

Después de comprender la causa, la solución es simple: configure maxMetaSpaceSize en 1024 MB en los parámetros de inicio. Después del cambio, la sobrecarga del GC es como se muestra en la siguiente figura y la situación del 100% ya no ocurre.

¿Qué se coloca exactamente en Metaspace?

Dado que Metaspace está lleno, tenemos que ver qué se coloca exactamente en Metaspace. Sabemos que Metaspace almacena principalmente los datos originales de las clases. como Cuando cargamos una clase, la información sobre esta clase asignará memoria en Metaspace para almacenar algunas de sus estructuras de datos, por lo que en la mayoría de los casos, el uso de Metaspace está estrechamente relacionado con la cantidad de clases cargadas. Lo anterior es el espacio insuficiente de Metaspace causado por una gran cantidad de clases.

Existe otra situación de desbordamiento de Metaspace, que consiste en construir dinámicamente un cargador de clases en algún lugar y cargar continuamente una clase al mismo tiempo. Nos encontramos con un caso en el que usamos el comando jmap para contar el sol. reflect.DelegatingClassLoader El número en realidad llega a decenas de miles

Básicamente, se puede bloquear que el cargador de clases reflectantes cause un desbordamiento de Metaspace. Entonces, ¿por qué hay tantos cargadores de clases reflectantes? hable sobre el principio de reflexión

En Java, la reflexión se escribe principalmente así. Supongamos que hay una clase A

Método Get

Para llamar, debes llamar. Primero obtenga el Método, y la lógica para obtener el Método proviene de la clase Clase, y los métodos y propiedades clave son los siguientes:

Hay una propiedad clave en la Clase llamada reflexiónData, aquí es principalmente Qué Se almacenan algunos atributos de clase obtenidos de la jvm cada vez, como métodos, campos, etc. Esta es su definición en la clase Class

Este atributo es principalmente SoftReference, es decir, en alguna memoria Puede Se reciclará en circunstancias más severas, pero en circunstancias normales, el tiempo de reciclaje se puede controlar a través del parámetro -XX: SoftRefLRUPolicyMSPerMB. Una vez que llegue el momento, se reciclará mientras ocurra GC, lo que significa que habrá demanda nuevamente después. reciclaje Cuando necesita recrear un objeto de este tipo, también necesita volver a recuperar un dato de la JVM. Luego, los campos Método, Campo, etc. asociados con esta estructura de datos son todos objetos regenerados.

Una publicación de blog posterior hablará sobre los problemas causados ​​por la carga constante de SoftReferenc.

El método getDeclaredMethod copia un objeto Method de la lista de métodos devueltos por searchMethods(privateGetDeclaredMethods).privateGetDeclaredMethods y lo devuelve. Si los métodos declarados de la propiedad reflexiónData no están vacíos, entonces privateGetDeclaredMethods puede devolverlo directamente; de ​​lo contrario, se cargará desde la JVM y se asignará al campo de reflexiónData.

Llamadas principales de searchMethods

getReflectionFactory().copyMethod(res) ->langReflectAccess().copyMethod(arg) ->ReflectAccess.copyMethod->method.copy

Se puede ver que el objeto Método devuelto por el método getDeclaredMethod es en realidad un objeto nuevo, por lo que no es adecuado llamar a demasiados. Si se llama con frecuencia, es mejor almacenarlo en caché.

Llamada al método

De hecho, como se mencionó anteriormente, el atributo raíz apunta principalmente al objeto método copiado, es decir, el objeto Método actual en realidad se construye en función del método raíz, por lo que hay un método raíz que deriva de varios métodos.

MethodAccessor es muy crítico. De hecho, el método Method.invoke es llamar al método de invocación de MethodAccessor. Si el atributo MethodAccessor ya existe en la raíz, entonces asígnelo directamente usando el método MethodAccessor de la raíz. . En caso contrario, cree uno nuevo.

Implementación de MethodAccessor

El propio MethodAccessor es una interfaz con tres implementaciones.

DelegatingMethodAccessorImpl

NativeMethodAccessorImpl

GeneratedMethodAccessorXXX

Entre ellos, DelegatingMethodAccessorImpl es el métodoAccessor que finalmente se inyecta en el método, es decir, todos de un método Todos los métodos de invocación llamarán a este DelegatingMethodAccessorImpl.invoke. Es una clase proxy y la implementación real puede ser de los dos tipos siguientes

La implementación predeterminada es NativeMethodAccessorImpl y GeneratedMethodAccessorXXX es una clase generada dinámicamente para cada método que debe llamarse de manera reflexiva. El último XXX es. un número, constantemente incremental

Y todas las reflexiones de métodos usan NativeMethodAccessorImpl primero. De forma predeterminada, los tiempos de ReflectionFactory.inflationThreshold se llaman antes de que se genere una clase GeneratedMethodAccessorXXX. Después de la generación, se usará el método de invocación de la clase generada.

p>

Entonces, ¿cómo pasar de NativeMethodAccessorImpl a GeneratedMethodAccessorXXX? Echemos un vistazo al método de invocación de NativeMethodAccessorImpl

El valor predeterminado de ReflectionFactory.inflationThreshold() es 15 veces. Podemos especificarlo a través de -Dsun.reflect.inflationThreshold. También podemos omitir directamente las 15 llamadas NativeMethodAccessorImpl anteriores a través de -Dsun.reflect.noInflation=true, y

-Dsun.reflect.inflationThreshold= 0 tiene el mismo efecto

Y GeneratedMethodAccessorXXX se genera a través del nuevo MethodAccessorGenerator().generateMethod. Una vez creado, se establece en DelegatingMethodAccessorImpl, de modo que la próxima vez Method.invoke se ajustará a esto en el MethodAccessor recién creado.

DelegatingClassLoader carga las clases creadas dinámicamente. Consulte ClassDefiner, nuevo DelegatingClassLoader para cargar nuevas clases.

Cada nuevo cargador de clases se utiliza por razones de rendimiento. En algunos casos, estas clases generadas se pueden descargar, porque la descarga de clases solo se realizará cuando el cargador de clases se pueda reciclar, si es el original. Si se utiliza un cargador de clases, es posible que estas clases recién creadas nunca se descarguen.

NativeMethodAccessorImpl.invoke en realidad no está bloqueado. Si la concurrencia es alta, muchos subprocesos pueden ingresar a la lógica de creación de la clase GeneratedMethodAccessorXXX al mismo tiempo. Si hay 1000 subprocesos ingresando a la lógica de creación de la clase GeneratedMethodAccessorXXX. , Lógicamente, eso significa que se crean 999 clases inútiles más. Estas clases continuarán ocupando memoria y no se reciclarán hasta que ocurra el GC que pueda recuperar MetaSpace

sd-jdi.jar a dump.sd-. El sun.jvm.hotspot.tools.jcore.ClassDump que viene con jdi.jar puede volcar el contenido de la clase en un archivo.

Se pueden configurar dos propiedades del sistema en ClassDump:

sun.jvm.hotspot.tools.jcore.filter Nombre de clase del filtro

sun.jvm.hotspot . tools.jcore.outputDir directorio de salida

Primero escriba una clase de filtro

y luego compílela en un archivo de clase

Para usar esto, primero debe agregar sa-jdi.jar se agrega a la ruta de clases de Java.

Ingrese el directorio del archivo de clase de la clase de filtro que acaba de escribir. Ejecutar

Cambie MyFilter a su propio nombre de clase, y pid es el pid del proceso Java de destino (puede usar jps para verlo). Luego se generará el archivo de clase correspondiente en .

De esta manera, podemos volcar todos los GeneratedMethodAccessor. En este momento, podemos usar javap -verbose GeneratedMethodAccessor0 para ver el código de bytes de una clase.

Puedes conocerlo mirando. en la línea 36 Su método provoca demasiada reflexión sobre el método.