Un artículo para comprender los puntos de conocimiento de JVM y optimización del rendimiento que los programadores deben conocer

Tabla de contenido

JVM y optimización del rendimiento

1. Área de memoria Java

Historia de las Máquinas Virtuales

Una mirada al futuro de la tecnología Java

área de datos de tiempo de ejecución

  • El papel de cada región

    • contador de programa

      El indicador de número de línea del bytecode ejecutado por el subproceso actual, que ocupa un espacio pequeño y no puede ser interferido

    • la pila

      Cada subproceso es privado, y cuando el subproceso se ejecuta, se empaquetará en un marco de pila al ejecutar cada método, almacenando la tabla de variables locales, la pila de operandos, el enlace dinámico, la salida del método y otra información, y luego lo colocará en la pila. El método actual que se ejecuta en cada momento es el marco de la pila en la parte superior de la pila de la máquina virtual. La ejecución del método corresponde al proceso de empujar y sacar el marco de la pila en la pila de la máquina virtual.
      El tamaño del marco de pila es de 1 M de forma predeterminada, y el tamaño se puede ajustar mediante el parámetro –Xss, por ejemplo -Xss256k

    • montón

      Casi todos los objetos se asignan aquí, que también es el área principal donde ocurre la recolección de basura, que se puede ajustar con los siguientes parámetros:
      -Xms: el valor mínimo del montón;
      -Xmx: el valor máximo del montón;
      -Xmn: el tamaño de la nueva generación;
      -XX:NewSize; El valor mínimo de la nueva generación;
      -XX:MaxNewSize: el valor máximo de la nueva generación;
      por ejemplo - Xmx256m

    • área de método

      Se utiliza para almacenar información de clase cargada por la máquina virtual, constantes ("zdy", "123", etc.), variables estáticas (static variables) y otros datos, que se pueden ajustar con los siguientes parámetros: jdk1.7 y antes: -XX:PermSize;
      - XX:MaxPermSize;
      Después de jdk1.8: -XX:MetaspaceSize; -XX:MaxMetaspaceSize
      Después de jdk1.8, el tamaño solo está limitado por la memoria total de la máquina.
      Por ejemplo: -XX: MaxMetaspaceSize=3M

    • grupo de constantes de tiempo de ejecución

      • Cambios en el área de memoria de cada versión

        • 1.6

          El grupo de constantes de tiempo de ejecución está en el área de métodos.

        • 1.7

          El grupo de constantes de tiempo de ejecución está en el montón

        • 1.8

          El grupo de constantes de tiempo de ejecución está en el área de metadatos

  • memoria directa

    No es parte del área de datos cuando la máquina virtual se está ejecutando, ni es el área de memoria definida en la especificación de la máquina virtual java; si se usa NIO, esta área se usará con frecuencia y puede ser directamente referenciada y operada por el objeto directByteBuffer en el montón de Java; esta
    área La memoria no está limitada por el tamaño del montón de Java, pero está limitada por la memoria total de la máquina, que se puede establecer mediante -XX:MaxDirectMemorySize (el valor predeterminado es el mismo que el valor máximo de la memoria del montón), por lo que también se producirán excepciones OOM.

Mirando el montón y la pila desde la perspectiva de los subprocesos

Análisis en profundidad de montón y pila

###Análisis en profundidad de montón y pila

  • Función
  1. Almacene el proceso de llamada de método en forma de marco de pila y almacene variables de tipos de datos básicos (int, short, long, byte, float, double, boolean, har, etc.) y variables de referencia de objetos durante el proceso de llamada de método. La asignación de memoria está en la pila, las variables se liberarán automáticamente cuando salgan del alcance
  1.  La memoria del montón se utiliza para almacenar objetos en Java. Ya se trate de variables miembro, variables locales o variables de clase, los objetos a los que apuntan se almacenan en la memoria del montón.
  • hilo exclusivo o compartido
  1. La memoria de pila pertenece a un solo hilo, y cada hilo tiene una memoria de pila, y las variables almacenadas en él solo se pueden ver en el hilo al que pertenece, es decir, la memoria de pila puede entenderse como la memoria privada del hilo.
  1. Los objetos en la memoria del montón son visibles para todos los subprocesos. Todos los subprocesos pueden acceder a los objetos en la memoria del montón.
  • tamaño del espacio

La memoria de pila es mucho más pequeña que la memoria de montón

pila de métodos

  • marco de pila

    Una llamada de método asigna un marco de pila en la pila

  • asignación de pila

    Una tecnología de optimización proporcionada por la máquina virtual. La idea básica es que para los objetos privados de subprocesos, se dispersa y se asigna en la pila en lugar de en el montón. La ventaja es que el objeto se destruye solo después de la llamada al método y no se requiere recolección de elementos no utilizados, lo que puede mejorar el rendimiento.
    La base técnica requerida para la asignación en la pila, análisis de escape. El propósito del análisis de escape es determinar si el alcance del objeto escapará del cuerpo del método. Tenga en cuenta que cualquier objeto que se pueda compartir entre varios subprocesos debe ser un objeto de escape.

    • El efecto de la asignación en la pila
    public void test(int x,inty ){
          
          
       String x = “”;
       User u =.
    }
    

    La misma instancia de objeto de usuario se asigna 100 000 000 veces, y solo se necesitan 6 ms para habilitar la asignación en la pila y se necesitan 3 segundos para deshabilitarla.

Objetos en la máquina virtual

  • proceso de asignación

    Cuando la máquina virtual encuentra una nueva instrucción, primero ejecuta el proceso de carga de clase correspondiente y
    luego la máquina virtual asignará memoria para el nuevo objeto. La tarea de asignar espacio para un objeto es equivalente a dividir un cierto tamaño de memoria del montón de Java.
    Si la memoria en el montón de Java es absolutamente regular, toda la memoria utilizada se coloca en un lado, la memoria libre se coloca en el otro lado y se coloca un puntero en el medio como indicador del punto de demarcación, entonces la memoria asignada es solo para poner ese puntero Mueva una distancia igual al tamaño del objeto al espacio libre, este método de asignación se llama "colisión de puntero".
    Si la memoria en el montón de Java no es regular, y la memoria usada y la memoria libre están intercaladas, entonces no hay manera de simplemente colisionar punteros.La máquina virtual debe mantener una lista para registrar qué bloques de memoria están disponibles. un espacio lo suficientemente grande de la lista para asignar a la instancia del objeto y actualizar los registros en la lista.Este método de asignación se denomina "lista libre".
    El método de asignación a elegir está determinado por si el almacenamiento dinámico de Java es regular, y si el almacenamiento dinámico de Java es regular está determinado por si el recolector de elementos no utilizados tiene una función de compactación.
    Además de cómo dividir el espacio disponible, hay otra cuestión que debe tenerse en cuenta. La creación de objetos es un comportamiento muy frecuente en la máquina virtual. Aunque solo se modifique la ubicación a la que apunta un puntero, no es seguro para subprocesos. en condiciones concurrentes, puede suceder que se esté asignando memoria al objeto A, el puntero no ha tenido tiempo de modificarse y el objeto B usa el puntero original para asignar memoria al mismo tiempo.
    Hay dos soluciones a este problema: una es sincronizar la acción de asignar espacio de memoria; de hecho, la máquina virtual usa CAS con reintentos fallidos para garantizar la atomicidad de las operaciones de actualización;
    La otra es dividir la asignación de memoria en diferentes espacios según los hilos, es decir, cada hilo asigna previamente una pequeña parte de la memoria privada en el montón de Java, es decir, el búfer de asignación de hilo local (Thread Local Allocation Buffer, TLAB ), si se establece el parámetro de máquina virtual -XX:UseTLAB, cuando se inicializa el subproceso, también se aplicará a una memoria de un tamaño específico, que solo es utilizada por el subproceso actual, de modo que cada subproceso tenga un búfer separado. Si necesita asignar memoria, solo en Asignar en su propio búfer, para que no haya competencia, lo que puede mejorar en gran medida la eficiencia de asignación. Cuando la capacidad del búfer no es suficiente, puede solicitar una pieza del área de Eden para continuar usándolo
    El propósito de TLAB es permitir que cada subproceso de aplicación Java use su propio puntero de asignación dedicado para asignar espacio al asignar espacio de memoria para nuevos objetos, lo que reduce la sobrecarga de sincronización.
    TLAB solo permite que cada subproceso tenga un puntero de asignación privado, pero el espacio de memoria subyacente para almacenar objetos sigue siendo accesible para todos los subprocesos, pero otros subprocesos no pueden asignarse en esta área. Cuando una TLAB está llena (la parte superior del puntero de asignación toca el límite de asignación), se solicita una nueva TLAB.
    3)
    Una vez completada la asignación de memoria, la máquina virtual debe inicializar el espacio de memoria asignado a valores cero (como el valor int es 0, el valor booleano es falso, etc.). Este paso asegura que los campos de instancia del objeto se puedan usar directamente en el código Java sin asignar valores iniciales, y el programa puede acceder a los valores cero correspondientes a los tipos de datos de estos campos.
    4)
    A continuación, la máquina virtual debe realizar los ajustes necesarios para el objeto, como de qué clase es una instancia, cómo encontrar la información de metadatos de la clase, el código hash del objeto, la edad de generación de GC del objeto y otra información. Esta información se almacena en el encabezado de objeto del objeto.
    5)
    Después de completar el trabajo anterior, desde la perspectiva de la máquina virtual, se ha generado un nuevo objeto, pero desde la perspectiva del programa Java, la creación del objeto acaba de comenzar y todos los campos siguen siendo cero. Por lo tanto, en términos generales, después de ejecutar la nueva instrucción, el objeto se inicializará de acuerdo con los deseos del programador, de modo que se pueda generar completamente un objeto realmente utilizable.
    Diseño de la memoria de los objetos
    En la máquina virtual HotSpot, el diseño de los objetos almacenados en la memoria se puede dividir en tres áreas: encabezado de objeto (Header), datos de instancia (Instance Data) y relleno de alineación (Padding).
    El encabezado del objeto incluye dos partes de información. La primera parte se utiliza para almacenar los datos de tiempo de ejecución del propio objeto, como el código hash (HashCode), la edad de generación del GC, el indicador de estado de bloqueo, el bloqueo retenido por el subproceso, la ID del subproceso sesgado, marca de tiempo sesgada, etc.
    La otra parte del encabezado del objeto es el puntero de tipo, que es el puntero del objeto a sus metadatos de clase.La máquina virtual usa este puntero para determinar de qué clase es una instancia el objeto.
    La tercera parte del relleno de alineación no existe necesariamente y no tiene un significado especial, solo actúa como marcador de posición. Porque el sistema de administración de memoria automática de HotSpot VM requiere que el tamaño del objeto sea un múltiplo entero de 8 bytes. Cuando otras partes de datos del objeto no están alineadas, debe completarse con el relleno de alineación.

  • diseño de memoria

  • Ubicación de acceso a objetos

    El propósito de crear un objeto es usar el objeto Nuestro programa Java necesita manipular el objeto específico en el montón a través de los datos de referencia en la pila. En la actualidad, existen dos métodos de acceso principales, que utilizan identificadores y punteros directos.
    Si usa un identificador para acceder, se asignará una parte de la memoria en el montón de Java como un grupo de identificadores. La referencia almacena la dirección del identificador del objeto, y el identificador contiene la información de dirección específica de los datos y el tipo de la instancia del objeto. datos.
    Si usa un puntero directo para acceder, lo que se almacena en la referencia es directamente la dirección del objeto.
    Estos dos métodos de acceso a objetos tienen sus propias ventajas. La mayor ventaja de usar un identificador para acceder es que la referencia almacena una dirección de identificador estable. Cuando se mueve el objeto (mover objetos durante la recolección de basura es un comportamiento muy común), solo el identificador será cambiado El puntero de datos de la instancia y la referencia en sí no necesitan ser modificados.
    La mayor ventaja de usar el método de acceso directo al puntero es que es más rápido, lo que ahorra la sobrecarga de tiempo de un posicionamiento del puntero. Dado que el acceso a objetos es muy frecuente en Java, este tipo de sobrecarga también es un costo de ejecución muy considerable después de acumularse.
    Para Sun HotSpot, utiliza acceso de puntero directo para el acceso a objetos.

Configuración de parámetros de montón y combate de desbordamiento de memoria

  • Desbordamiento de montón de Java
  • configuración de nueva generación
  • Área de método y desbordamiento de grupo constante de tiempo de ejecución
  • Desbordamiento de pila de máquina virtual y pila de método nativo
  • Desbordamiento de memoria directa nativa

2. Recolector de basura y estrategia de asignación de memoria

Resumen de GC

Juzgar la supervivencia del objeto.

  • conteo de referencia

Rápido, conveniente y simple de implementar Desventaja: Cuando los objetos se refieren entre sí, es difícil juzgar si el objeto ha cambiado o no.

  • análisis de accesibilidad

Para determinar si el objeto está vivo o no. La idea básica de este algoritmo es usar una serie de objetos llamados "GC Roots" como punto de partida, y comenzar a buscar hacia abajo desde estos nodos. El camino recorrido por la búsqueda se llama cadena de referencia (Reference Chain). Cuando Roots no está conectado por ninguna cadena de referencia, prueba que este objeto no está disponible.
Los objetos utilizados como raíces de GC incluyen los siguientes tipos:
 1. Objetos a los que se hace referencia en la pila de la máquina virtual (tabla de variables locales en el marco de la pila).
 2. Objetos referenciados por propiedades estáticas de clase en el área de método.
 3. Objetos referenciados por constantes en el área de método.
 4. El objeto al que hace referencia JNI (en términos generales, método nativo) en la pila de métodos locales.

Diferenciar entre referencias fuertes y débiles.

  • fuerte referencia

General Object obj = new Object() es una referencia fuerte.

  • Referencia suave Referencia suave

Algunos objetos útiles pero no necesarios asociados con referencias flexibles se reciclarán antes de que se produzca el OOM del sistema.

  • Referencia débil Referencia débil

Algunas son útiles (más bajas que las referencias blandas), pero no necesarias. Los objetos asociados con referencias débiles solo pueden sobrevivir hasta la próxima recolección de elementos no utilizados. Cuando se produce la recopilación de basura, independientemente de si hay suficiente memoria, se reciclará.

  • Referencia virtual PhantomReference

Las referencias de Spectre, las más débiles, reciben una notificación cuando se recolecta basura

###Nota
La referencia suave SoftReference y la referencia débil WeakReference se pueden usar en el caso de recursos de memoria limitados y la creación de cachés de datos que no son muy importantes. Cuando la memoria del sistema es insuficiente, se puede liberar el contenido de la memoria caché.
Por ejemplo, se utiliza un programa para procesar imágenes proporcionadas por los usuarios. Si todas las imágenes se leen en la memoria, aunque las imágenes se pueden abrir rápidamente, el espacio de la memoria será enorme y algunas imágenes menos utilizadas desperdiciarán espacio en la memoria y deberán eliminarse manualmente de la memoria. Si cada vez que se abre una imagen, se lee desde el archivo del disco a la memoria y luego se muestra. Aunque el uso de la memoria es pequeño, algunas imágenes de uso frecuente necesitan acceder al disco cada vez que se abren, y el costo es enorme. En este momento, puede usar referencias suaves para crear cachés.

algoritmo GC

  • algoritmo de barrido de marcas

    El algoritmo se divide en dos etapas de "marcado" y "limpieza": primero marque todos los objetos que deben reciclarse y recicle todos los objetos marcados de manera uniforme después de completar el marcado.
    Su problema principal es la falta de espacio. Después de borrar la marca, se generará una gran cantidad de fragmentos de memoria discontinuos. Demasiada fragmentación del espacio puede causar que cuando el programa necesite asignar objetos grandes en el futuro, no pueda encontrar suficiente memoria continua y tiene que activarse con antelación Otra acción de recogida de basura.

  • algoritmo de copia

    Divida la memoria disponible en dos partes del mismo tamaño según la capacidad, y use solo una de ellas a la vez. Cuando se agote la memoria de este bloque, copie el objeto sobreviviente a otro bloque y luego limpie el espacio de memoria usado a la vez. De esta manera, la memoria se reclama para toda la mitad del área cada vez, y no es necesario considerar situaciones complejas como la fragmentación de la memoria al asignar la memoria, siempre que la memoria se asigne en orden, lo cual es simple de implementar y eficiente. para operar. Es solo que el costo de este algoritmo es reducir la memoria a
    la mitad de su tamaño original.

  • Algoritmo de clasificación de marcas

    Primero marque todos los objetos que necesitan ser reciclados. Después de completar el marcado, el siguiente paso no es limpiar directamente los objetos reciclables, sino mover todos los objetos sobrevivientes a un extremo y luego limpiar directamente la memoria fuera del final. Perímetro.

colección generacional

La recolección de basura actual de las máquinas virtuales comerciales adopta el algoritmo "Generational Collection" (Colección generacional), este algoritmo no tiene ninguna idea nueva, pero divide la memoria en varios bloques de acuerdo con los diferentes ciclos de vida de los objetos. Generalmente, el montón de Java se divide en la nueva generación y la generación anterior, de modo que se pueda adoptar el algoritmo de recopilación más adecuado según las características de cada época.
Una investigación especial muestra que el 98 % de los objetos de la nueva generación "viven y mueren", por lo que no es necesario dividir el espacio de la memoria en una proporción de 1:1, sino dividir la memoria en un espacio Eden más grande y dos los más pequeños Espacio pequeño de Survivor, use Eden y uno de Survivor [1] cada vez. Al reciclar, copie los objetos sobrevivientes en Eden y Survivor a otro espacio de Survivor a la vez, y finalmente limpie Eden y el espacio de Survivor que acaba de usar. La proporción predeterminada de Eden a Survivor para la máquina virtual HotSpot es 8:1, es decir, el espacio de memoria disponible en cada nueva generación es el 90 % (80 %+10 %) de la capacidad total de la nueva generación y solo el 10 % de la memoria será "desperdiciada".". Por supuesto, el 98 % de los objetos reciclables son solo datos en escenarios generales. No tenemos forma de garantizar que no más del 10 % de los objetos sobrevivirán a cada reciclaje. Cuando el espacio Superviviente no es suficiente, debemos confiar en otros memoria (aquí se refiere a la generación anterior) para llevar a cabo la garantía de Cesión (Manejar Promoción).
En la nueva generación, se encuentra que una gran cantidad de objetos mueren y solo una pequeña cantidad de objetos sobreviven cada vez que se recolecta la basura. Luego, se usa un algoritmo de copia y la recolección solo se puede completar pagando el costo de la copia. un pequeño número de objetos supervivientes. En la generación anterior, debido a que el objeto tiene una alta tasa de supervivencia y no hay espacio adicional para asignarlo, es necesario usar el algoritmo "marcar-limpiar" o "marcar-organizar" para reciclar.

fenómeno Stop The World

El objetivo del colector GC y nuestro ajuste GC es reducir el tiempo y la frecuencia de STW tanto como sea posible.

Interpretación de los registros del GC

Estrategia de asignación y reciclaje de memoria

Los objetos se asignan primero en Eden. Si no hay suficiente espacio de memoria en Eden,
los objetos grandes en Minor GC pasarán directamente a la antigüedad. Objetos grandes: Objetos Java que requieren una gran cantidad de espacio de memoria continua, como cadenas muy largas y arreglos, 1. Causa Si hay espacio en la memoria, todavía es necesario realizar la recolección de basura por adelantado para obtener espacio continuo para almacenarlos 2. Se realizará una gran cantidad de copias de memoria.
-XX: parámetro PretenureSizeThreshold, más de esta cantidad se asigna directamente en la generación anterior, el valor predeterminado es 0, lo que significa que nunca se asignará directamente en la generación anterior.
Los objetos de larga duración entrarán en la vejez, el valor predeterminado es 15 años, -XX: MaxTenuringThreshold ajusta la
determinación dinámica de la edad del objeto, para adaptarse mejor al estado de la memoria de diferentes programas, la máquina virtual no siempre requiere la edad del objeto para alcanzar MaxTenuringThreshold Para promover la generación anterior, si la suma del tamaño de todos los objetos de la misma edad en el espacio Superviviente es mayor que la mitad del espacio Superviviente, los objetos cuya edad es mayor o igual a esta edad puede ingresar directamente a la generación anterior sin esperar la edad requerida en MaxTenuringThreshold Garantía de asignación de espacio: nueva
generación Hay una gran cantidad de objetos que sobreviven en , y el espacio de supervivencia no es suficiente. Cuando una gran cantidad de objetos aún sobreviven después de MinorGC (el El caso más extremo es que todos los objetos de la nueva generación sobreviven después de la recuperación de memoria), se requiere la generación anterior para garantizar la asignación, y los Objetos supervivientes que no se pueden acomodar van directamente a la generación anterior. Siempre que el espacio continuo de la generación anterior es mayor que el tamaño total de los objetos de nueva generación o el tamaño promedio de las promociones anteriores, se realizará Minor GC, de lo contrario, se realizará FullGC.

Diferenciación y análisis de pérdida de memoria y desbordamiento de memoria

Desbordamiento de memoria: causado por una falta real de espacio en la memoria;
pérdida de memoria: el objeto a liberar no se libera, lo que es más común cuando se utiliza un contenedor para almacenar elementos.

Las herramientas proporcionadas por JDK

  • jps

Enumere los procesos de la máquina virtual que se ejecutan en la máquina actual
-p: solo muestra el logotipo de la VM, no muestra el jar, la clase, los parámetros principales y otra información
-m: muestra los parámetros pasados ​​por la función principal. el programa desde Parámetros ingresados ​​en la línea de comando
-l: muestra el nombre completo del paquete de la clase principal de la aplicación o el nombre completo del jar
-v: enumera los parámetros jvm, -Xms20m -Xmx50m son los parámetros jvm especificados por el programa de inicio

  • estar de pie

Es una herramienta de línea de comandos para monitorear diversa información de estado de ejecución de máquinas virtuales. Puede mostrar los datos en ejecución, como la carga de clases, la memoria, la recolección de elementos no utilizados, la compilación JIT, etc., en el proceso de la máquina virtual local o remota. La herramienta preferida para los problemas de rendimiento de la máquina.
Suponiendo que necesita consultar el estado de recolección de elementos no utilizados del proceso 2764 cada 250 milisegundos, un total de 20 consultas, el comando debe ser: jstat-gc 2764 250 20 Parámetros comunes: -clase (cargador de clases) -compilador
(
JIT
)
-gc (Estado del montón del GC)
-gccapacity (tamaño del distrito)
-gccause (últimas estadísticas y motivos del GC)
-gcnew (nuevas estadísticas del distrito)
-gcnewcapacity (nuevo tamaño del distrito)
-gcold (antiguas estadísticas del distrito)
-gcoldcapacity (antiguo tamaño del distrito)
-gcpermcapacity (tamaño del distrito permanente)
-gcutil (resumen de estadísticas de GC)
-printcompilation (estadísticas de compilación de HotSpot)

  • jinfo

Ver y modificar los parámetros de la máquina virtual
jinfo –sysprops Puede ver los parámetros obtenidos por System.getProperties()
jinfo –flag El valor predeterminado del sistema del parámetro que no se especifica explícitamente
jinfo –flags (nota s) Muestra los parámetros de la máquina virtual
jinfo –flag +[parameter] puede agregar parámetros, pero solo se limita a
los parámetros manejables
jinfo –flag -[parameter] puede eliminar parámetros

  • jmap

Se utiliza para generar instantáneas de volcado de montón (generalmente llamados volcado de montón o archivos de volcado). La función de jmap no es solo obtener archivos de volcado, sino que también puede consultar los detalles de la cola de ejecución de finalización, el montón de Java y la generación permanente, como el uso del espacio, qué recopilador se usa actualmente, etc. Al igual que el comando jinfo, jmap tiene muchas funciones que están limitadas en la plataforma Windows, excepto la opción -dump para generar archivos de volcado y la opción -histo para ver instancias de cada clase y estadísticas de ocupación de espacio, que están disponibles en todos los sistemas operativos. Salvo que se proporcione, el resto de las opciones solo están disponibles en Linux/Solaris.
jmap -dump:live,format=b,file=heap.bin
Sun JDK proporciona el comando jhat (herramienta de análisis de montón de JVM) que se utilizará con jmap para analizar la instantánea de volcado de montón generada por jmap.

  • jajaja

Después del nombre del archivo de volcado jhat,
la pantalla muestra "El servidor está listo".

  • jstack

El comando (Stack Trace for Java) se usa para generar una instantánea de hilo de la máquina virtual en el momento actual. Una instantánea de subproceso es una colección de pilas de métodos que ejecuta cada subproceso en la máquina virtual actual. El objetivo principal de generar una instantánea de subproceso es localizar la causa de una pausa larga en un subproceso, como un punto muerto entre subprocesos, un bucle infinito , y mucho tiempo causado por la solicitud de recursos externos. La espera, etc., son causas comunes de pausas de subprocesos durante largos períodos de tiempo.
En el código, el método getAllStackTraces() de la clase java.lang.Thread se puede utilizar para obtener los objetos StackTraceElement de todos los subprocesos de la máquina virtual. El uso de este método puede completar la mayoría de las funciones de jstack con unas pocas líneas simples de código. En proyectos reales, es posible que desee llamar a este método para crear una página de administrador, y puede usar un navegador para ver la pila de subprocesos en cualquier momento. .

Para administrar el proceso remoto, debe agregar los parámetros de inicio del programa remoto:
-Djava.rmi.server.hostname=...
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8888
-Dcom.sun.management .jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

Más información sobre MAT

  • Montones poco profundos y profundos

Montón superficial : (Montón superficial) se refiere a la memoria consumida por un objeto. Por ejemplo, en un sistema de 32 bits, una referencia de objeto ocupará 4 bytes, un tipo int ocupará 4 bytes, una variable de tipo largo ocupará 8 bytes y cada encabezado de objeto ocupará 8 bytes.
Montón profundo : después de que GC reclama este objeto, el tamaño de la memoria que realmente se puede liberar, es decir, la colección de todos los objetos a los que solo se puede acceder directa o indirectamente a través de objetos. En términos sencillos, se refiere a la colección de objetos en manos de objetos. El almacenamiento dinámico profundo es la suma de los tamaños de almacenamiento dinámico superficial de todos los objetos en el conjunto de retención del objeto.
Ejemplo : el objeto A hace referencia a C y D, y el objeto B hace referencia a C y E. Entonces, el tamaño de montón poco profundo del objeto A es solo A en sí mismo, excluyendo C y D, y el tamaño real de A es la suma de A, C y D. El tamaño de montón profundo de A es la suma de A y D. Dado que también se puede acceder al objeto C a través del objeto B, no está dentro del rango de montón profundo del objeto A.

recolector de basura

  • usar los algoritmos

La recolección de basura actual de las máquinas virtuales comerciales adopta el algoritmo "Generational Collection" (Colección generacional), este algoritmo no tiene ninguna idea nueva, pero divide la memoria en varios bloques de acuerdo con los diferentes ciclos de vida de los objetos. Generalmente, el montón de Java se divide en la nueva generación y la generación anterior, de modo que se pueda adoptar el algoritmo de recopilación más adecuado según las características de cada época.
Una investigación especial muestra que el 98 % de los objetos de la nueva generación "viven y mueren", por lo que no es necesario dividir el espacio de la memoria en una proporción de 1:1, sino dividir la memoria en un espacio Eden más grande y dos los más pequeños Espacio pequeño de Survivor, use Eden y uno de Survivor [1] cada vez. Al reciclar, copie los objetos sobrevivientes en Eden y Survivor a otro espacio de Survivor a la vez, y finalmente limpie Eden y el espacio de Survivor que acaba de usar. La proporción predeterminada de Eden a Survivor para la máquina virtual HotSpot es 8:1, es decir, el espacio de memoria disponible en cada nueva generación es el 90 % (80 %+10 %) de la capacidad total de la nueva generación y solo el 10 % de la memoria será "desperdiciada".". Por supuesto, el 98 % de los objetos reciclables son solo datos en escenarios generales. No tenemos forma de garantizar que no más del 10 % de los objetos sobrevivirán a cada reciclaje. Cuando el espacio Superviviente no es suficiente, debemos confiar en otros memoria (aquí se refiere a la generación anterior) para llevar a cabo la garantía de Cesión (Manejar Promoción).
En la nueva generación, se encuentra que una gran cantidad de objetos mueren y solo una pequeña cantidad de objetos sobreviven cada vez que se recolecta la basura. Luego, se usa un algoritmo de copia y la recolección solo se puede completar pagando el costo de la copia. un pequeño número de objetos supervivientes. En la generación anterior, debido a que el objeto tiene una alta tasa de supervivencia y no hay espacio adicional para asignarlo, es necesario usar el algoritmo "marcar-limpiar" o "marcar-organizar" para reciclar.

  • Descripción general del recolector de basura

    Serial/Serial Old, ParNew, Barrido paralelo (ParallerGC)/Parallel Old, Barrido de marcas concurrente (CMS), G1

  • Funcionamiento detallado del recolector de basura

  • Explicación detallada de G1

  • futura recolección de basura

    ZGC utiliza medios técnicos para controlar la situación de STW solo una vez, es decir, sucederá la primera marca inicial, por lo que no es difícil entender por qué el tiempo de pausa del GC no aumenta con el aumento del montón, sin importar cómo. Por grande que sea, también uso el tiempo de concurrencia para recuperar
    tecnologías clave

    1. Punteros de colores
    2. Barrera de carga

recolector de basura

  • usar los algoritmos

La recolección de basura actual de las máquinas virtuales comerciales adopta el algoritmo "Generational Collection" (Colección generacional), este algoritmo no tiene ninguna idea nueva, pero divide la memoria en varios bloques de acuerdo con los diferentes ciclos de vida de los objetos. Generalmente, el montón de Java se divide en la nueva generación y la generación anterior, de modo que se pueda adoptar el algoritmo de recopilación más adecuado según las características de cada época.
Una investigación especial muestra que el 98 % de los objetos de la nueva generación "viven y mueren", por lo que no es necesario dividir el espacio de la memoria en una proporción de 1:1, sino dividir la memoria en un espacio Eden más grande y dos los más pequeños Espacio pequeño de Survivor, use Eden y uno de Survivor [1] cada vez. Al reciclar, copie los objetos sobrevivientes en Eden y Survivor a otro espacio de Survivor a la vez, y finalmente limpie Eden y el espacio de Survivor que acaba de usar. La proporción predeterminada de Eden a Survivor para la máquina virtual HotSpot es 8:1, es decir, el espacio de memoria disponible en cada nueva generación es el 90 % (80 %+10 %) de la capacidad total de la nueva generación y solo el 10 % de la memoria será "desperdiciada".". Por supuesto, el 98 % de los objetos reciclables son solo datos en escenarios generales. No tenemos forma de garantizar que no más del 10 % de los objetos sobrevivirán a cada reciclaje. Cuando el espacio Superviviente no es suficiente, debemos confiar en otros memoria (aquí se refiere a la generación anterior) para llevar a cabo la garantía de Cesión (Manejar Promoción).
En la nueva generación, se encuentra que una gran cantidad de objetos mueren y solo una pequeña cantidad de objetos sobreviven cada vez que se recolecta la basura. Luego, se usa un algoritmo de copia y la recolección solo se puede completar pagando el costo de la copia. un pequeño número de objetos supervivientes. En la generación anterior, debido a que el objeto tiene una alta tasa de supervivencia y no hay espacio adicional para asignarlo, es necesario usar el algoritmo "marcar-limpiar" o "marcar-organizar" para reciclar.

  • Descripción general del recolector de basura

    Serial/Serial Old, ParNew, Barrido paralelo (ParallerGC)/Parallel Old, Barrido de marcas concurrente (CMS), G1

  • Funcionamiento detallado del recolector de basura

  • Explicación detallada de G1

  • futura recolección de basura

    ZGC utiliza medios técnicos para controlar la situación de STW solo una vez, es decir, sucederá la primera marca inicial, por lo que no es difícil entender por qué el tiempo de pausa del GC no aumenta con el aumento del montón, sin importar cómo. Por grande que sea, también uso el tiempo de concurrencia para recuperar
    tecnologías clave

    1. Punteros de colores
    2. Barrera de carga

3. El subsistema de ejecución de la JVM

Esencia del archivo de clase

  1. Las máquinas virtuales en varias plataformas y el formato de almacenamiento de programas utilizado de manera uniforme por todas las plataformas: el código de bytes (ByteCode) es la piedra angular de la independencia de la plataforma y la base de la independencia del idioma. La máquina virtual Java no está vinculada a ningún lenguaje, incluido Java. Solo está asociada con el formato de archivo binario específico del "archivo de clase". El archivo de clase contiene el conjunto de instrucciones y la tabla de símbolos de la máquina virtual Java, así como otra información auxiliar. . .
  2. Cualquier archivo de clase corresponde a la información de definición de una clase o interfaz única, pero a la inversa, el archivo de clase no existe necesariamente en forma de archivo de disco.
    Los archivos de clase son un conjunto de flujos binarios basados ​​en bytes de 8 bits.

formato de archivo de clase

Cada elemento de datos está estrictamente organizado en el archivo de Clase en un orden compacto sin agregar ningún separador en el medio, lo que hace que casi todo el contenido almacenado en todo el archivo de Clase sea la información necesaria para que el programa se ejecute, y no hay espacios.
El formato de archivo Class utiliza una pseudoestructura similar a la estructura del lenguaje C para almacenar datos. Solo hay dos tipos de datos en esta pseudoestructura: números sin signo y tablas.
Los números sin signo pertenecen a los tipos de datos básicos. U1, u2, u4 y u8 representan números sin signo de 1 byte, 2 bytes, 4 bytes y 8 bytes respectivamente. Los números sin signo se pueden usar para describir números, referencias de índice, valores de cantidad, o formar valores de cadena según la codificación UTF-8.
Una tabla es un tipo de datos compuesto por varios números sin firmar u otras tablas como elementos de datos, y todas las tablas suelen terminar con "_info". Una tabla se usa para describir datos con una estructura compuesta con una relación jerárquica, y todo el archivo de Clase es esencialmente una tabla.

  • formato detallado

    La estructura de Class no es como XML y otros lenguajes de descripción. Dado que no tiene separadores, los elementos de datos que contiene, ya sea en orden o cantidad, están estrictamente limitados. Qué byte representa qué significado, cuál es la longitud y el orden Pase lo que pase, no se permiten cambios. Incluir en orden:

    • La versión del número mágico y el archivo de clase.

    Los primeros 4 bytes de cada archivo de Clase se denominan Número Mágico y su única función es determinar si el archivo es un archivo de Clase que puede aceptar la máquina virtual. El uso de números mágicos en lugar de extensiones para la identificación se basa principalmente en consideraciones de seguridad, ya que las extensiones de archivo se pueden cambiar a voluntad. Los autores del formato de archivo son libres de elegir el valor mágico, siempre que el valor mágico no haya sido ampliamente adoptado y no cause confusión.
    Los 4 bytes que siguen al número mágico almacenan el número de versión del archivo de Clase: los bytes 5 y 6 son el número de versión menor (MinorVersion), y los bytes 7 y 8 son el número de versión principal (Versión Mayor). El número de versión de Java comienza en 45. Después de JDK 1.1, el número de versión principal de cada versión principal de JDK aumenta en 1. La versión superior de JDK puede ser compatible con la versión anterior del archivo Class, pero no puede ejecutar la versión posterior. versión del archivo de clase, incluso si el formato del archivo no ha cambiado de ninguna manera, y la máquina virtual también debe negarse a ejecutar archivos de clase que excedan su número de versión.

    • piscina constante

    El número de constantes en el grupo de constantes no es fijo, por lo que se debe colocar un tipo de datos u2 en la entrada del grupo de constantes, que representa el valor de conteo de capacidad del grupo constante (constant_pool_count). A diferencia de los hábitos del lenguaje en Java, este conteo de capacidad comienza desde 1 en lugar de 0. El
    conjunto de constantes almacena principalmente dos tipos de constantes: referencias literales y simbólicas.
    Las cantidades literales están más cerca del concepto constante a nivel del lenguaje Java, como cadenas de texto, valores constantes declarados como finales, etc.
    Las referencias simbólicas pertenecen al concepto de principios de compilación, incluidos los siguientes tres tipos de constantes:
    nombres completos de clases e interfaces (Fully Qualified Name), nombres de campos y descriptores (Descriptor), nombres de métodos y descriptores

    • señal de acceso

    Información de acceso utilizada para identificar algunas clases o niveles de interfaz, que incluye: si la Clase es una clase o una interfaz; si se define como un tipo público; si se define como un tipo abstracto; si es una clase, si es declarado como definitivo, etc.

    • Colección de índice de clase, índice de clase principal e índice de interfaz

    Estos tres elementos de datos se utilizan para determinar la relación de herencia de esta clase. El índice de clase se utiliza para determinar el nombre completo de esta clase y el índice de la clase principal se usa para determinar el nombre completo de la clase principal de esta clase. Dado que el lenguaje Java no permite la herencia múltiple, solo hay un índice de clase principal. Excepto java.lang.Object, todas las clases de Java tienen clases principales. Por lo tanto, excepto java.lang.Object, los índices de clase principal de todas las clases de Java las clases son no es 0. La colección de índice de interfaz se usa para describir qué interfaces implementa esta clase, y estas interfaces implementadas se organizarán en el índice de interfaz de izquierda a derecha según el orden de las interfaces después de la instrucción implements (si la clase en sí es una interfaz, debe ser una declaración extendida) en la colección

    • colección de mesa de campo

    Describe las variables declaradas en una interfaz o clase. Los campos incluyen variables de nivel de clase, así como variables de nivel de instancia.
    El nombre del campo y el tipo de datos del campo no son fijos y solo se pueden describir haciendo referencia a constantes en el conjunto de constantes.
    Los campos heredados de superclases o interfaces principales no se incluirán en la colección de tablas de campos, pero se pueden incluir campos que no existen en el código Java original. Por ejemplo, para mantener el acceso a clases externas en clases internas, un campo que apunta a la instancia de clase externa se agrega automáticamente.

    • colección de tablas de métodos

    Describe la definición del método, pero el código Java en el método, después de que el compilador lo compila en instrucciones de código de bytes, se almacena en un atributo denominado "Código" en la tabla de atributos del método establecida en el conjunto de tablas de atributos.
    De manera similar a la colección de tablas de campos, si el método de la clase principal no se anula en la subclase (Anular), la información del método de la clase principal no aparecerá en la colección de tablas de métodos. Pero de la misma forma, pueden existir métodos agregados automáticamente por el compilador, los más típicos son el método constructor de clases "<clinit>" y el constructor de instancias "<init>"

    • colección de tablas de atributos

    Almacene archivos de clases, tablas de campos y tablas de métodos con sus propias colecciones de tablas de atributos, que se utilizan para describir información específica de ciertos escenarios. Por ejemplo, el código del método se almacena en la tabla de atributos Código.

instrucciones de código de bytes

  • saber

La instrucción de la máquina virtual Java consta de un número de longitud de bytes que representa el significado de una operación específica (llamado código de operación, Opcode) seguido de cero o más parámetros necesarios para que se forme esta operación (llamado operando, Operandos).
Dado que la longitud del código de operación de la máquina virtual Java está limitada a un byte (es decir, de 0 a 255), esto significa que el número total de códigos de operación en el conjunto de instrucciones no puede exceder los 256.
La mayoría de las instrucciones incluyen información sobre el tipo de datos con el que operan. Por ejemplo:
la instrucción iload se usa para cargar datos de tipo int desde la tabla de variables locales a la pila de operandos, mientras que la instrucción fload carga datos de tipo float.
La mayoría de las instrucciones no admiten los tipos enteros byte, char y short, y ninguna de las instrucciones admite siquiera el tipo booleano. La mayoría de las operaciones con datos de tipo booleano, byte, corto y char en realidad utilizan el tipo int correspondiente como tipo de operación.
Leer el código de bytes es una habilidad básica para comprender la máquina virtual Java, por favor, domínela. Familiarícese y domine las instrucciones comunes.

  • instrucciones de carga y almacenamiento

Se utiliza para transferir datos de un lado a otro entre la tabla de variables locales en el marco de la pila y la pila de operandos, tales instrucciones incluyen lo siguiente.
Cargue una variable local en la pila de operaciones: iload, iload_<n>, lload, lload_<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n>.
Almacene un valor de la pila de operandos en la tabla de variables locales: istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>.
Cargue una constante en la pila de operandos: bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, iconst_<i>, lconst_<l>, fconst_<f>, dconst_<d>.
El comando para expandir el índice de acceso de la tabla de variables locales: ancho.

  • operación o instrucción aritmética

Se utiliza para realizar una operación específica en los valores de las dos pilas de operandos y almacenar el resultado en la parte superior de la pila de operaciones.
Instrucciones de adición: iadd, ladd, fadd, dadd.
Instrucciones de resta: isub, lsub, fsub, dsub.
Instrucciones de multiplicación: imul, lmul, fmul, dmul, etc.

  • instrucción de conversión de tipo

Se pueden convertir dos tipos numéricos diferentes entre sí.La
máquina virtual Java admite directamente la conversión de tipo ampliado de los siguientes tipos numéricos (es decir, la conversión segura de un tipo de rango pequeño a un tipo de rango amplio): tipo int a long
, tipo flotante o doble.
Tipo largo para flotar, tipo doble.
tipo flotante a tipo doble.
Cuando se trata de conversiones de tipo restrictivo (conversiones numéricas restrictivas), debe hacerse explícitamente usando instrucciones de conversión, que incluyen: i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l y d2f.

  • Instrucciones para crear instancias de clase

nuevo

  • Instrucciones para crear arreglos

matriz nueva, matriz nueva, matriz múltiple

  • comando de campo de acceso

getfield, putfield, getstatic, putstatic

  • Instrucciones relacionadas con el acceso a la matriz

Instrucciones que cargan un elemento de matriz en la pila de operandos: baload, caload, saload, iaload, laload, faload, daload, aaload.
Instrucciones que almacenan el valor de una pila de operandos en un elemento de matriz: bastore, castore, sastore, iastore, fastore, dastore, aastore.
Comando para obtener la longitud de la matriz: arraylength.

  • Instrucciones para verificar el tipo de una instancia de clase

instancia de, checkcast

* Instrucciones de gestión de la pila de operandos

Al igual que operar una pila en una estructura de datos ordinaria, la máquina virtual de Java proporciona algunas instrucciones para manipular directamente la pila de operandos, que incluyen: extraer uno o dos elementos de la parte superior de la pila de operandos: pop, pop2.
Duplica uno o dos valores de la parte superior de la pila y empuja el duplicado o los duplicados duplicados nuevamente a la pila: dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2.
Intercambiar los dos valores en la parte superior de la pila: intercambio

  • instrucción de transferencia de control

La instrucción de transferencia de control permite que la máquina virtual Java continúe, de forma condicional o incondicional, ejecutando el programa desde la instrucción de ubicación especificada en lugar de la siguiente instrucción de la instrucción de transferencia de control.Desde el modelo conceptual, se puede considerar que la instrucción de transferencia de control es condicionalmente o modificado incondicionalmente El valor del registro de PC. Las instrucciones de transferencia de control son las siguientes.
Ramas condicionales: ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmpgt, if_icmple, if_icmpge, if_acmpeq y if_acmpne.
Rama de condición compuesta: tableswitch, lookupswitch.
Rama incondicional: goto, goto_w, jsr, jsr_w, ret.

  • instrucción de llamada de método

La instrucción invocarvirtual se usa para llamar al método de instancia del objeto y enviarlo de acuerdo con el tipo real del objeto (despacho de método virtual), que también es el método de envío de método más común en el lenguaje Java.
El comando invocar interfaz se usa para llamar al método de interfaz, buscará un objeto que implemente el método de interfaz en tiempo de ejecución y encontrará un método adecuado para llamar.
La instrucción invocar especial se usa para llamar a algunos métodos de instancia que requieren un manejo especial, incluidos los métodos de inicialización de instancia, métodos privados y métodos de clase padre.
La instrucción invocar estática se utiliza para llamar a un método de clase (método estático).
La instrucción invocación dinámica se usa para analizar dinámicamente el método al que hace referencia el calificador de punto de llamada en tiempo de ejecución y ejecutar el método. determinado por el usuario Está determinado por el método de arranque establecido.
Las instrucciones de llamada a métodos son independientes del tipo de datos.

  • instrucción de retorno del método

Se distingue según el tipo de valor de retorno, incluyendo ireturn (usado cuando el valor de retorno es booleano, byte, char, short e int), lreturn, freturn, dreturn y areturn, y también existe una instrucción de retorno para los métodos declarados como void , métodos de inicialización de instancias y métodos de inicialización de clases para clases e interfaces.

  • instrucciones de manejo de excepciones

La operación (sentencia de lanzamiento) que lanza explícitamente una excepción en un programa Java se implementa mediante la instrucción athrow

  • comando síncrono

Hay dos instrucciones monitorenter y monitorexit para admitir la semántica de la palabra clave sincronizada

mecanismo de carga de clases

  • Proceso de carga detallado

    • descripción general

    Desde que se carga una clase en la memoria de la máquina virtual, hasta que se descarga de la memoria, todo su ciclo de vida incluye: carga (Loading), verificación (Verification), preparación (Preparation), resolución (Resolution), inicialización (Initialization ), uso (Using) y descarga (Unloading) 7 etapas. Entre ellas, las tres partes de verificación, preparación y análisis se denominan colectivamente Vinculación
    en la fase de inicialización. La especificación de la máquina virtual estipula estrictamente que solo hay 5 situaciones en las que la clase debe "inicializarse" inmediatamente (carga, verificación , y la preparación naturalmente requiere comenzar antes):

    1. Al encontrar las cuatro instrucciones de código de bytes de new, getstatic, putstatic o invocar estática, si la clase no se ha inicializado, su inicialización debe activarse primero. Los escenarios de código Java más comunes para generar estas 4 instrucciones son: cuando se usa la nueva palabra clave para crear una instancia de un objeto, se lee o se configura un campo estático de una clase (campo estático modificado por final y el resultado se ha colocado en el grupo de constantes en tiempo de compilación) campos), y al llamar a un método estático de una clase.
    2. Al usar el método del paquete java.lang.reflect para realizar una llamada reflexiva a una clase, si la clase no se ha inicializado, primero debe activar su inicialización.
    3. Al inicializar una clase, si encuentra que su clase principal no se ha inicializado, primero debe activar la inicialización de su clase principal.
    4. Cuando se inicia la máquina virtual, el usuario debe especificar una clase principal para ejecutar (la clase que contiene el método main()), y la máquina virtual primero inicializa la clase principal.
    5. Cuando se utiliza el soporte de lenguaje dinámico de JDK 1.7, si el resultado del análisis final de una instancia de java.lang.invoke.MethodHandle es el identificador de método de REF_getStatic, REF_putStatic, REF_invokeStatic y la clase correspondiente a este identificador de método no se ha inicializado, primero debe activarse su inicialización.
    • Aviso

    Para los campos estáticos, solo se inicializará la clase que define directamente este campo, por lo que hacer referencia al campo estático definido en la clase principal a través de su subclase solo activará la inicialización de la clase principal y no la inicialización de la subclase.
    La constante HELLOWORLD, pero de hecho, a través de la optimización de la propagación constante durante la etapa de compilación, el valor de esta constante "hola mundo" se ha almacenado en el grupo de constantes de la clase NotInitialization. En el futuro, las referencias de la constante ConstClass .HELLOWORLD by NotInitialization en realidad se convierten en la propia clase NotInitialization, una referencia al conjunto de constantes.
    Es decir, de hecho, no hay ninguna entrada de referencia de símbolo de la clase ConstClass en el archivo Class de NotInitialization, y no hay conexión entre estas dos clases después de compilarlas en Class.

    • etapa de carga

    La máquina virtual necesita completar las siguientes 3 cosas:

    1. Obtenga el flujo de bytes binarios que define esta clase por su nombre completo.
    2. Convierta la estructura de almacenamiento estática representada por este flujo de bytes en la estructura de datos de tiempo de ejecución del área de método.
    3. Un objeto java.lang.Class que representa esta clase se genera en la memoria como una entrada de acceso para varios datos de esta clase en el área de métodos.
    • verificar

    Es el primer paso de la fase de conexión. El objetivo de esta fase es garantizar que la información contenida en el flujo de bytes del archivo Class cumpla con los requisitos de la máquina virtual actual y no ponga en peligro la seguridad de la máquina virtual en sí. Pero, en general, la fase de verificación completará aproximadamente las siguientes cuatro etapas de acciones de verificación: verificación de formato de archivo, verificación de metadatos, verificación de código de bytes y verificación de referencia de símbolo.

    • Fase de preparación

    Es la etapa de asignar memoria formalmente para las variables de clase y establecer el valor inicial de las variables de clase.La memoria utilizada por estas variables se asignará en el área de método. En esta etapa, hay dos conceptos confusos que deben enfatizarse. Primero, la asignación de memoria en este momento solo incluye variables de clase (variables modificadas por estática), no variables de instancia. Las variables de instancia se instanciarán cuando se instancia el objeto. Junto con el objeto asignado en el montón de Java. En segundo lugar, el valor inicial mencionado aquí es "normalmente" el valor cero del tipo de datos. Supongamos que una variable de clase se define como:
    public static int value=123;
    el valor inicial de la variable value después de la fase de preparación es 0 en lugar de 123 , porque no se ha ejecutado ningún método Java en este momento, y la instrucción putstatic que asigna el valor a 123 se almacena en el método constructor de clase <clinit> () después de compilar el programa, por lo que la acción de asignar el valor a 123 estar en La fase de inicialización sólo se ejecutará. La Tabla 7-1 enumera los valores cero de todos los tipos de datos primitivos en Java.
    Supongamos que la definición del valor de la variable de clase anterior se convierte en: public static final int value=123;
    Javac generará un atributo ConstantValue para el valor durante la compilación, y la máquina virtual asignará el valor a 123 de acuerdo con la configuración de ConstantValue durante la etapa de preparación .

    • etapa de análisis

    Es el proceso por el cual la máquina virtual reemplaza las referencias simbólicas en el pool constante con referencias directas

    • fase de inicialización de clase

    Es el último paso del proceso de carga de clases.En el proceso de carga de clases anterior, excepto que la aplicación de usuario puede participar en la fase de carga a través del cargador de clases personalizado, el resto de acciones están completamente dominadas y controladas por la máquina virtual. En la fase de inicialización, se ejecuta realmente el código del programa Java definido en la clase. En la fase de preparación, a las variables se les ha asignado el valor inicial requerido por el sistema una vez. En la fase de inicialización, las variables de clase y las variables se inicializan de acuerdo con el plan subjetivo formulado por el programador a través del programa Otros recursos, o puede expresarse desde otro ángulo: la fase de inicialización es el proceso de ejecución del método constructor de clases <clinit>(). El método <clinit>() se genera mediante la combinación de las acciones de asignación de todas las variables de clase en la clase recopiladas automáticamente por el compilador y las sentencias en el bloque de sentencias estáticas (bloque {} estático), determinadas por el orden en que aparecer.
    El método <clinit>() no es necesario para una clase o interfaz. Si no hay un bloque de instrucciones estáticas en una clase y ninguna operación de asignación a variables, entonces el compilador no necesita generar el método <clinit>() para esto. clase.
    La máquina virtual asegura que el método <clinit> () de una clase esté correctamente bloqueado y sincronizado en un entorno de subprocesos múltiples. Si varios subprocesos inicializan una clase al mismo tiempo, solo un subproceso ejecutará el método <clinit> ( ) , otros subprocesos deben bloquearse y esperar hasta que el subproceso activo ejecute el método <clinit>(). Si hay una operación que consume mucho tiempo en el método <clinit>() de una clase, puede provocar que se bloqueen varios procesos.

  • cargador de clases

    • La carga de clases personalizadas cifra y descifra las clases

      Anular el método findClass

    • cargador de clases del sistema

      Para cualquier clase, su unicidad en la máquina virtual Java debe ser establecida por el cargador de clases que la carga y la clase misma.Cada cargador de clases tiene un espacio de nombres de clase independiente. Esta oración se puede expresar de manera más general: comparar si dos clases son "iguales" solo tiene sentido si las dos clases se cargan con el mismo cargador de clases; de lo contrario, incluso si las dos clases provienen del mismo archivo de clase. máquina virtual, siempre que los cargadores de clases que las carguen sean diferentes, las dos clases deben ser desiguales.
      La "igualdad" a la que se hace referencia aquí incluye los resultados de retorno del método equals(), el método isAssignableFrom() y el método isInstance() del objeto Class que representa la clase, así como el uso de la palabra clave instanceof para determinar la relación de el objeto.
      Al personalizar la subclase de ClassLoader, generalmente tenemos dos métodos, uno es reescribir el método loadClass y el otro es reescribir el método findClass. De hecho, estos dos métodos son similares en esencia, después de todo, loadClass también llamará a findClass, pero lógicamente hablando, es mejor no modificar directamente la lógica interna de loadClass. El enfoque que sugiero es anular solo el método de carga de la clase personalizada en findClass.
      El método loadClass es donde se implementa la lógica del modelo de delegación principal. La modificación de este método sin autorización hará que el modelo se destruya y cause problemas. Por lo tanto, es mejor para nosotros hacer cambios a pequeña escala dentro del marco del modelo de delegación de los padres sin destruir la estructura estable original. Al mismo tiempo, también evita tener que escribir el código repetido confiado por los padres en el proceso de reescritura del método loadClass Desde la perspectiva de la reutilización del código, siempre es una mejor opción no modificar directamente este método.

    • Modelo de delegación de padres

      Desde la perspectiva de la máquina virtual Java, solo existen dos cargadores de clases diferentes: uno es el cargador de clases bootstrap (Bootstrap ClassLoader), que está implementado en lenguaje C++ y forma parte de la propia máquina virtual; es decir, todos los demás cargadores de clases son implementados por el lenguaje Java, independientemente de la máquina virtual, y todos heredan de la clase abstracta java.lang.ClassLoader.
      Bootstrap ClassLoader: Este cargador de clases se encarga de almacenarlo en el directorio <JAVA_HOME>\lib, o en la ruta especificada por el parámetro -Xbootclasspath, y es reconocido por la máquina virtual (solo por el nombre del archivo), como rt. jar, la biblioteca de clases cuyo nombre no coincida no se cargará incluso si se coloca en el directorio lib) La biblioteca de clases se carga en la memoria de la máquina virtual. Los programas Java no pueden hacer referencia directamente al cargador de clases de inicio. Cuando los usuarios escriben cargadores de clases personalizados, si necesitan delegar la solicitud de carga al cargador de clases de arranque, pueden usar directamente nulo en su lugar.
      Extensión ClassLoader (Extensión ClassLoader): Este cargador está implementado por sun.misc.Launcher$ExtClassLoader, que es responsable de cargar el directorio <JAVA_HOME>\lib\ext, o la ruta especificada por la variable de sistema java.ext.dirs Para todas las clases bibliotecas, los desarrolladores pueden usar directamente el cargador de clases de extensión.
      Application ClassLoader (cargador de clases de aplicaciones): este cargador de clases se implementa mediante sun.misc.Launcher $App-ClassLoader. Dado que este cargador de clases es el valor de retorno del método getSystemClassLoader() en ClassLoader, generalmente se le llama cargador de clases del sistema. Es responsable de cargar la biblioteca de clases especificada en la ruta de clases del usuario (ClassPath). Los desarrolladores pueden usar este cargador de clases directamente. Si la aplicación no ha personalizado su propio cargador de clases, generalmente esta es la clase predeterminada en el programa. Loader.
      Nuestras aplicaciones son cargadas por estos tres cargadores de clases en cooperación entre sí. Si es necesario, también puede agregar su propio cargador de clases definido.
      El modelo de delegación principal requiere que todos los cargadores de clases, excepto el cargador de clases de inicio de nivel superior, tengan su propio cargador de clases principal. La relación padre-hijo entre los cargadores de clases aquí generalmente no se implementa como una relación de herencia (Herencia), sino que utiliza una relación de composición (Composición) para reutilizar el código del cargador padre.
      Uno de los beneficios obvios de usar el modelo de delegación parental para organizar la relación entre cargadores de clases es que la clase Java tiene una relación jerárquica con prioridad junto con su cargador de clases. Por ejemplo, la clase java.lang.Object se almacena en rt.jar. No importa qué cargador de clases quiera cargar esta clase, eventualmente se delegará al cargador de clases de inicio en la parte superior del modelo para cargar. Por lo tanto, el La clase de objeto se carga en el programa. La misma clase está presente en los diversos contextos del cargador de clases de . Por el contrario, si no se utiliza el modelo de delegación parental, y cada cargador de clases lo carga por sí mismo, si el usuario escribe una clase llamada java.diferentes clases de objetos, no se puede garantizar el comportamiento más básico en el sistema de tipos Java, y el la aplicación se volverá caótica.

  • Mecanismo de carga de clase Tomcat

    Tomcat en sí también es un proyecto de Java, por lo que también debe cargarse mediante el mecanismo de carga de clases de JDK, por lo que debe haber un cargador de clases de arranque, un cargador de clases de extensión y un cargador de clases de aplicación (sistema).
    Common ClassLoader es el padre de Catalina ClassLoader y Shared ClassLoader, y Shared ClassLoader puede tener varios cargadores de clases secundarios WebApp ClassLoader. Un WebApp ClassLoader en realidad corresponde a una aplicación web, por lo que puede haber páginas Jsp en la aplicación web. Estas páginas Jsp eventualmente se convertirá en una clase de clase para cargar, por lo que también se requiere un cargador de clases Jsp.
    Cabe señalar que a nivel de código, las clases de entidad correspondientes a Catalina ClassLoader, Shared ClassLoader y Common ClassLoader son en realidad URLClassLoader o SecureClassLoader. Generalmente, solo las dividimos lógicamente en estas tres según la diferencia en la carga de contenido y la relación. entre la carga del pedido principal y secundario. Un cargador de clases; WebApp ClassLoader y JasperLoader tienen cargadores de clases correspondientes.
    Cuando se inicia Tomcat, se crean varios cargadores de clases:
    1 El cargador de clases Bootstrap Bootstrap carga las clases necesarias para el inicio de JVM, así como las clases de extensión estándar (ubicadas en jre/lib/ext)
    2 El cargador de clases System system carga las clases iniciadas por Tomcat Classes , como bootstrap.jar, generalmente se especifican en catalina.bat o catalina.sh. Ubicado en CATALINA_HOME/bin.
    3 Común El cargador de clases comunes carga algunas clases comunes utilizadas por tomcat y aplicadas, ubicadas en CATALINA_HOME/lib, como servlet-api.jar
    4 cargador de clases de aplicaciones webapp Después de implementar cada aplicación, se creará un cargador de clases único. El cargador de clases cargará la clase en el archivo jar en WEB-INF/lib y el archivo de clase en WEB-INF/classes.

Marco de pila detallado

Llamada de método detallada

El destino de la llamada debe determinarse cuando se escribe el código del programa y el compilador compila. Las llamadas de tales métodos se llaman resoluciones.
En el lenguaje Java, los métodos que cumplen el requisito de "cognoscible en tiempo de compilación e inmutable en tiempo de ejecución" incluyen principalmente métodos estáticos y métodos privados. El primero está directamente asociado con el tipo, y el segundo no se puede acceder desde el exterior. Las características de ellos determinan que les es imposible reescribir otras versiones a través de la herencia u otros medios, por lo que todos son adecuados para analizar en la fase de carga de clases.

Motor de interpretación y ejecución de código de bytes basado en pila

  • Conjunto de instrucciones basado en pilas frente a conjunto de instrucciones basado en registros
  • Analizar la ejecución de código en la máquina virtual

4. Escriba programas Java eficientes y elegantes

¿Qué pasa si hay demasiados parámetros de constructor?

Use el modo constructor y utilícelo para
1, 5 o más variables miembro
2. No hay muchos parámetros, pero en el futuro, los parámetros aumentarán

Las clases que no necesitan ser instanciadas deben tener constructores privados

No crees objetos innecesarios

  • Evite crear objetos sin darse cuenta, como dentro de un bucle de autoboxing porque el autoboxing y el unboxing crearán objetos inútiles.

  • Para las variables miembro que se pueden reutilizar en varias instancias de la clase, intente usar estática.

Evite los finalizadores

El método finalizador, jdk no puede garantizar cuándo se ejecutará, ni puede garantizar que se ejecutará. Si hay recursos que realmente necesitan liberarse, se debe usar try/finally.

Minimizar la accesibilidad de clases y miembros

Al escribir programas y diseñar arquitecturas, uno de los objetivos más importantes es el desacoplamiento entre módulos. Minimizar la accesibilidad de clases y miembros es, sin duda, una de las formas efectivas.

minimizar la variabilidad

Intente hacer que la clase sea inmutable. Las clases inmutables son más fáciles de diseñar, implementar y usar que las clases mutables, y son menos propensas a errores y más seguras.
Métodos de uso común:
no proporcione ningún método que pueda modificar el estado del objeto;
haga que todos los campos sean definitivos.
Haz que todos los campos sean privados.
Utilice un mecanismo de copia en escritura. Problemas provocados: Hará que el sistema genere una gran cantidad de objetos y el rendimiento tendrá un cierto impacto, que debe sopesarse cuidadosamente durante el uso.

Compuesto

La herencia rompe fácilmente la encapsulación y hace que la implementación de las subclases dependa de la clase principal.
Compuesto es agregar un campo privado en la clase y hacer referencia a una instancia de la clase, para evitar depender de la implementación específica de la clase.
La herencia solo es apropiada cuando la subclase es de hecho un subtipo de la clase principal.

Las interfaces son mejores que las clases abstractas.

Los parámetros variables deben usarse con precaución.

Los parámetros variables pueden pasar parámetros 0.
Si el número de parámetros está entre 1 y varios, se requiere un control comercial separado.

Devuelva una matriz o colección de longitud cero, no devuelva nulo

Preferir excepciones estándar

Es necesario buscar la reutilización del código tanto como sea posible, al mismo tiempo que se reduce la cantidad de carga de clases y se mejora el rendimiento de la carga de clases.
Excepciones comúnmente utilizadas:
IlegalAraumentException: los parámetros pasados ​​por la persona que llama son inapropiados
lllegalStateException: el estado del objeto recibido es incorrecto,
NullPointException: excepción de puntero nulo
UnsupportedOperationException: operación no admitida

use enum en lugar de int constante

Minimizar el alcance de las variables locales

1. Declarar en el lugar donde se usa por primera vez.
2. Las variables locales deben inicializarse por sí mismas. Si no se cumplen las condiciones de inicialización, no declarar
los beneficios de la minimización, reducir el tamaño de la tabla de variables locales, y mejorar el rendimiento, al mismo tiempo, evitar el exceso de variables locales La declaración temprana conduce a un uso incorrecto.

Cálculo preciso, evite usar float y double

Cuidado con el rendimiento de la concatenación de cadenas

Cuando hay una gran cantidad de empalmes de cadenas o empalmes de cadenas grandes, intente usar StringBuilder y StringBuffer

5. Comprensión profunda de la optimización del rendimiento

Indicadores de evaluación/prueba de desempeño de uso común

  • Tiempo de respuesta

    • El tiempo transcurrido entre el envío de una solicitud y la devolución de una respuesta a esa solicitud, generalmente centrándose en el tiempo de respuesta promedio.
      Lista de tiempos de respuesta para operaciones comunes:
    funcionar Tiempo de respuesta
    abrir un sitio pocos segundos
    Consulta de base de datos para un registro (con índice) diez milisegundos
    Direccionamiento y posicionamiento únicos del disco mecánico 4 milisegundos
    Lea 1M de datos secuencialmente desde el disco mecánico 2 milisegundos
    Lea 1M de datos secuencialmente desde el disco SSD 0,3 milisegundos
    Leer datos desde un control remoto distribuido a Redis 0,5 milisegundos
    Leer 1M de datos de la memoria diez microsegundos
    Llamada al método nativo del programa Java unos microsegundos
    Transferencia de red 2Kb de datos 1 sutil
  • número concurrente

    Al mismo tiempo, el número de solicitudes de interacción real con el servidor.
    Correlación con el número de usuarios en línea del sitio web: 1000 usuarios en línea simultáneos, se puede estimar que el número de concurrentes está entre 5% y 15%, es decir, el número de concurrentes simultáneos está entre 50 y 150.

  • rendimiento

    Una medida de la cantidad de trabajo (solicitudes) completado por unidad de tiempo

  • relación mutua

    • La relación entre el rendimiento del sistema, la concurrencia del sistema y el tiempo de respuesta:

    Se entiende como el estado del tráfico de la autopista:
    el rendimiento es el número de vehículos que pasan por la cabina de peaje todos los días (se puede convertir en el peaje de alta velocidad cobrado por la cabina de peaje), el
    número concurrente es el número de vehículos en la autopista, y
    el tiempo de respuesta es la velocidad del vehículo.
    Cuando hay pocos vehículos, la velocidad es rápida. Pero el peaje de alta velocidad recibido es correspondientemente menor; a medida que aumenta el número de vehículos en la autopista, la velocidad del vehículo se ve ligeramente afectada, pero el peaje de alta velocidad recibido aumenta rápidamente; a medida que aumenta el número de vehículos, la la velocidad del vehículo se vuelve cada vez más
    lenta, la autopista se está congestionando cada vez más, y los peajes no aumentarán sino que disminuirán;
    si el flujo de tráfico continúa aumentando, después de exceder cierto límite, los factores accidentales de la tarea conducirán a la paralización total de la vía expresa salida del estacionamiento (recurso agotado).

Métodos comunes de optimización del rendimiento

  • principios generales

    • evitar la optimización prematura

    No se debe gastar mucho tiempo en pequeñas mejoras de rendimiento, pensar en la optimización antes de tiempo es la raíz de todas las pesadillas.
    Por lo tanto, debemos escribir código claro, directo, legible y comprensible, y la optimización real debe dejarse para más adelante, cuando el análisis de rendimiento muestre que las medidas de optimización tienen grandes beneficios.
    Pero la optimización prematura no significa que debamos escribir estructuras de código que ya sabemos que son malas para el rendimiento.

    • Realizar pruebas de rendimiento del sistema.

    Todos los ajustes de rendimiento deben basarse en pruebas de rendimiento. La intuición es muy importante, pero los datos deben usarse para hablar. Se puede especular, pero debe verificarse mediante pruebas.

    • Encuentre cuellos de botella en el sistema, divida y vencerá y optimice gradualmente

    Después de la prueba de rendimiento, analice cada enlace de toda la experiencia de la solicitud, compruebe dónde se producen los cuellos de botella en el rendimiento, localice los problemas y analice cuáles son los principales factores que afectan al rendimiento. ¿Problemas de memoria, E/S de disco, red, CPU o código? ¿Diseño arquitectónico insuficiente? ¿O es realmente una falta de recursos del sistema?

  • Medios de optimización front-end

    navegador/aplicación

    • reducir el número de solicitudes;

    Combinar CSS, Js, imágenes

    • Utilice el almacenamiento en búfer del lado del cliente;

    Los archivos de recursos estáticos se almacenan en caché en el navegador. Si las propiedades relacionadas Cache-Control y Expires
    cambian y necesitan actualizarse, el nombre del archivo se puede cambiar para resolver el problema.

    • habilitar la compresión

    Reduzca la cantidad de transmisión de la red, pero ejercerá presión sobre el rendimiento del navegador y el servidor, que debe usarse de manera equilibrada.

    • Orden de carga del archivo de recursos

    css en la parte superior de la página, js en la parte inferior

    • Reducir la transmisión de cookies

    Las cookies se incluyen en cada solicitud y respuesta, por lo que se debe considerar cuidadosamente qué datos escribir en las cookies.

    • dar al usuario una pista

    A veces, dar al usuario un aviso en la parte delantera puede dar buenos resultados. Después de todo, lo que necesitan los usuarios es no ignorarlo.

    Aceleración CDN

    CDN, también conocida como red de distribución de contenido, sigue siendo un caché en esencia y almacena datos en el lugar más cercano al usuario. Cuando la CDN no se puede implementar por sí misma, se pueden considerar los servicios comerciales de CDN.

    caché de proxy inverso

    Almacene en caché los archivos de recursos estáticos en un servidor proxy inverso, generalmente Nginx.

    Separación de componentes WEB

    Coloque los archivos js, css y de imagen bajo diferentes nombres de dominio. Puede aumentar el número simultáneo de navegadores que descargan componentes web. Porque el navegador tiene un límite en el número de descargas simultáneas de los datos del mismo nombre de dominio.

  • Optimización del rendimiento del servicio de aplicaciones

  • Optimización del rendimiento del almacenamiento

    • Elija la estructura de datos correcta

    Elegir ArrayList y LinkedList tiene un gran impacto en el rendimiento de nuestro programa, ¿por qué? Debido a que ArrayList se implementa internamente como una matriz, existe una expansión y replicación de datos constantes.

    • Elija un mejor algoritmo

    Por ejemplo, el problema de suma máxima de subcolumnas:
    Dada una secuencia de enteros, a0, a1, a2, ... , an (el elemento puede ser negativo), encuentre la suma de subsecuencias más grande.
    Si todos los enteros son negativos, entonces la suma máxima de subsecuencias es 0;
    por ejemplo (a[1],a[2],a[3],a[4],a[5],a[6])=(- 2,11,-4,13,-5,-2),
    la suma máxima de incisos es 20, y los incisos son a[2], a[3], a[4].
    El peor algoritmo: el método exhaustivo, el tiempo de cálculo requerido es O(n^3). El
    algoritmo general: la complejidad del tiempo de cálculo del método divide y vencerás es O(nlogn).
    El mejor algoritmo: la suma máxima del subsegmento El algoritmo de programación dinámica, la complejidad del tiempo de cálculo es O (n)
    Cuanto mayor es n, mayor es la diferencia de tiempo, como 10,000 elementos, la brecha entre el peor algoritmo y el mejor algoritmo no es de ninguna manera multi-hilo o rendimiento de agrupamiento Solución fácil.

    • escribir menos código

    El mismo programa correcto, los programas pequeños son más rápidos que los grandes, esto no tiene nada que ver con el lenguaje de programación.

Más información sobre la optimización del rendimiento del servicio de aplicaciones

  • cache

    • Los fundamentos y la naturaleza del almacenamiento en caché

      Caché es almacenar datos en un medio con alta velocidad de acceso. El tiempo de acceso a los datos se puede reducir evitando la doble contabilidad.

    • Directrices para el uso adecuado de los tampones

      Los datos modificados con frecuencia no deben almacenarse en caché tanto como sea posible. El valor del almacenamiento en caché es solo cuando la relación de lectura y escritura es superior a 2: 1.
      El caché debe ser datos activos.
      La aplicación necesita tolerar la inconsistencia de los datos durante un cierto período de tiempo.
      Los problemas de disponibilidad de caché generalmente se resuelven mediante el modo de espera en caliente o la agrupación en clústeres.
      El caché se ha calentado. El sistema de caché recién iniciado no tiene ningún dato. Puede considerar cargar algunos datos calientes en el sistema de caché con anticipación.
      Resuelva el desglose del caché:
      1. Filtro Bloom, o 2. Caché de datos inexistentes. Por ejemplo, hay una solicitud para acceder siempre a los datos con clave = 23, pero estos datos con clave = 23 no existen en el sistema. Puede Considere construir datos (clave = 23 valor = nulo) en el caché.

    • Caché distribuida y hash consistente

      • Hay dos implementaciones para proporcionar servicios de almacenamiento en caché en forma de clústeres;
      1. Es necesario actualizar el caché distribuido sincronizado. Todos los servidores almacenan los mismos datos en caché. El problema es que la cantidad de datos en caché es limitada. En segundo lugar, los datos deben sincronizarse en todas las máquinas, lo cual es muy costoso.
      1. Cada máquina solo almacena en caché parte de los datos y luego selecciona el servidor de caché a través de un cierto algoritmo. El algoritmo hash de resto común tiene el problema de reconstruir una gran cantidad de datos almacenados en caché cuando un servidor se desconecta. Por lo tanto, se propone un algoritmo hash consistente.
      • hash consistente
      1. Primero calcule el valor hash del servidor (nodo) y configúrelo en el continuo de 0-232.
      1. Luego use el mismo método para encontrar el valor hash de la clave para almacenar los datos y asigne el mismo círculo.
      2. Luego mira en el sentido de las agujas del reloj desde donde se asignan los datos, guardando los datos en el primer servidor que encuentra. Si no se puede encontrar el servidor después de exceder 232, se guardará en el primer servidor.
        El algoritmo hash consistente solo necesita reubicar una pequeña parte de los datos en el espacio del anillo para aumentar o disminuir los nodos, lo que tiene buena tolerancia a fallas y escalabilidad.
        Cuando el algoritmo hash consistente tiene muy pocos nodos de servicio, es fácil causar sesgo de datos debido a la división desigual de los nodos. En este momento, una gran cantidad de datos se concentrará en el Nodo A, y solo una pequeña cantidad se ubicará en el Nodo B. Para resolver este problema de sesgo de datos, el algoritmo hash consistente introduce un mecanismo de nodo virtual, es decir, calcula múltiples hashes para cada nodo de servicio y coloca un nodo de servicio en cada posición de resultado del cálculo, lo que se denomina nodo virtual. El método específico se puede realizar agregando un número después de la IP del servidor o el nombre del host. Por ejemplo, se pueden calcular tres nodos virtuales para cada servidor, por lo que "Nodo A#1", "Nodo A#2", "Nodo A#3", "Nodo B#1", "Nodo B#2", " Valor hash del nodo B#3", formando así seis nodos virtuales: al mismo tiempo, el algoritmo de posicionamiento de datos permanece sin cambios, pero hay un paso adicional de mapeo de nodos virtuales a nodos reales, como el posicionamiento del "Nodo A#1" , " Los datos de los tres nodos virtuales "Nodo A#2" y "Nodo A#3" se encuentran en el Nodo A. Esto resuelve el problema del sesgo de datos cuando hay pocos nodos de servicio. En aplicaciones prácticas, la cantidad de nodos virtuales generalmente se establece en 32 o más, por lo que incluso unos pocos nodos de servicio pueden lograr una distribución de datos relativamente uniforme.
  • grupo

  • asincrónico

    • Síncrono y asíncrono, bloqueante y no bloqueante

      Enfoque síncrono y asíncrono en el mecanismo de comunicación del mensaje de resultado.

      • Sincronizar

      La sincronización significa que la persona que llama debe esperar activamente a que se devuelva el resultado

      • asincrónico

      Asíncrono significa que no hay necesidad de esperar activamente la devolución del resultado, sino a través de otros medios, como la notificación de estado, la función de devolución de llamada, etc.

      El bloqueo y el no bloqueo se relacionan principalmente con el estado de espera de que los resultados regresen a la persona que llama.

      • bloquear

      Significa que antes de que se devuelva el resultado, el hilo actual se suspende y no hace nada.

      • sin bloqueo

      Significa que antes de que se devuelva el resultado, el subproceso puede hacer otras cosas sin suspenderse.

      • bloqueo síncrono

      El bloqueo síncrono es básicamente el modelo más común en la programación. Por ejemplo, si vas a una tienda a comprar ropa y descubres que la ropa está agotada después de ir allí, entonces esperas en la tienda sin hacer nada (incluso mirar en su teléfono), etc. Es muy ineficiente seguir a los comerciantes para comprar productos hasta que estén en stock. BIO en jdk pertenece al bloqueo síncrono

      • no bloqueante síncrono

      El no bloqueo sincrónico se puede abstraer como un modo de sondeo en la programación. Después de ir a la tienda, descubre que la ropa está agotada. Todavía necesito ir a la tienda de vez en cuando para preguntarle al jefe si la ropa nueva ha llegado. NIO en jdk es síncrono y no bloquea

      • bloqueo asíncrono

      El bloqueo asincrónico rara vez se usa en la programación. Es un poco como si escribiera un grupo de subprocesos, lo enviara y luego future.get() inmediatamente, de modo que el subproceso aún esté suspendido. Es un poco como cuando vas a la tienda a comprar ropa, y en ese momento te encuentras con que la ropa no está, le dejas un número de teléfono al jefe en este momento, diciendo que me llamarás cuando llegue la ropa, y entonces solo vigila el teléfono y espera a que suene. No lo hagas. Esto se siente un poco tonto, por lo que este modo se usa menos.

      • sin bloqueo asíncrono

      Es como cuando vas a la tienda a comprar ropa y la ropa no está, solo necesitas decirle al jefe que este es mi número de teléfono y llamar cuando llegue la ropa. Entonces puedes jugar como quieras y no tienes que preocuparte por cuándo llegará la ropa. Una vez que llegue la ropa, puedes ir a comprar ropa tan pronto como suene el teléfono. AIO en jdk es asíncrono

    • medios asíncronos comunes

      • Servlet asíncrono

      Solo está disponible en servlet3 y los contenedores web admitidos son posteriores a tomcat7 y jetty8.

      • subprocesos múltiples
      • cola de mensajes
      • grupo

      Puede asignar bien las solicitudes de los usuarios a múltiples máquinas para su procesamiento, lo que mejora en gran medida el rendimiento general.

      • nivel de código de programa

      El rendimiento de una aplicación depende en última instancia de cómo se escribe el código.

  • relacionado con la aplicación

    • nivel de código

      El rendimiento de una aplicación depende en última instancia de cómo se escribe el código.

      • Elija la estructura de datos correcta

      Elegir ArrayList y LinkedList tiene un gran impacto en el rendimiento de nuestro programa, ¿por qué? Debido a que ArrayList se implementa internamente como una matriz, existe una expansión y replicación de datos constantes.

      • Elija un mejor algoritmo

      Por ejemplo, el problema de suma máxima de subcolumnas:
      Dada una secuencia de enteros, a0, a1, a2, ... , an (el elemento puede ser negativo), encuentre la suma de subsecuencias más grande.
      Si todos los enteros son negativos, entonces la suma máxima de subsecuencias es 0;
      por ejemplo (a[1],a[2],a[3],a[4],a[5],a[6])=(- 2,11,-4,13,-5,-2),
      la suma máxima de incisos es 20, y los incisos son a[2], a[3], a[4].
      El peor algoritmo: el método exhaustivo, el tiempo de cálculo requerido es O(n^3). El
      algoritmo general: la complejidad del tiempo de cálculo del método divide y vencerás es O(nlogn).
      El mejor algoritmo: la suma máxima del subsegmento El algoritmo de programación dinámica, la complejidad del tiempo de cálculo es O (n)
      Cuanto mayor es n, mayor es la diferencia de tiempo, como 10,000 elementos, la brecha entre el peor algoritmo y el mejor algoritmo no es de ninguna manera multi-hilo o rendimiento de agrupamiento Solución fácil.

      • escribir menos código

      El mismo programa correcto, los programas pequeños son más rápidos que los grandes, esto no tiene nada que ver con el lenguaje de programación.

    • programación concurrente

      1. Aproveche al máximo la CPU multinúcleo,
      1. Implemente clases seguras para subprocesos para evitar problemas de seguridad de subprocesos
      2. Reducir la contención de bloqueo bajo sincronización
    • Reutilización de recursos

      El propósito es reducir la creación y destrucción de recursos costosos del sistema, como conexiones de base de datos, conexiones de comunicación de red, recursos de subprocesos, etc.

    • JVM

      • Optimizaciones relacionadas con el compilador JIT

        • El concepto de compilación caliente

        Para un programa, generalmente solo una parte del código se ejecuta con frecuencia, y estos códigos clave se denominan puntos calientes de la aplicación, y cuanto más se ejecuta, más caliente es. La compilación de estos códigos en códigos binarios locales específicos de la máquina puede mejorar efectivamente el rendimiento de la aplicación.

        • Seleccione el tipo de compilador

        -servidor, compila más tarde, pero más optimizado después de la compilación, mayor rendimiento
        -cliente, comienza a compilar muy pronto

        • Caché de código relacionado

        Después de la compilación, habrá un caché de código para guardar el código compilado. Una vez que el caché esté lleno, jvm no podrá continuar compilando el código.
        Cuando jvm indica: CodeCache is full, significa que se debe aumentar el tamaño de la memoria caché de código.
        –XX:ReservedCodeCacheSize=N puede usarse para ajustar este tamaño.

        • umbral de compilación

        Que el código se compile depende de la frecuencia de ejecución del código y de si se alcanza el umbral de compilación.
        Hay dos tipos de contadores: el contador de llamada de método y el contador de borde de bucle inverso en el método El
        hecho de que un método alcance el umbral de compilación depende de la suma de los dos contadores en el método. Los parámetros para compilar el ajuste del umbral son: -XX:CompileThreshold=N
        El contador de llamadas al método no cuenta el número absoluto de veces que se llama a un método, sino una frecuencia de ejecución relativa, es decir, el número de veces que se llama a un método dentro de un período de tiempo. Cuando se supera un cierto límite de tiempo, si el número de llamadas al método aún no es suficiente para enviarlo al compilador justo a tiempo para su compilación, el contador de llamadas de este método se reducirá a la mitad. contra el decaimiento del calor (Counter Decay), y este período de tiempo se denomina período de vida media de este método (Counter Half Life Time). La acción del decaimiento del calor se realiza cuando la máquina virtual realiza la recolección de elementos no utilizados. Puede usar el parámetro de la máquina virtual -XX:-UseCounterDecay para desactivar el decaimiento del calor y permitir que el contador del método cuente el número absoluto de llamadas al método. En este De esta manera, siempre que el sistema se ejecute el tiempo suficiente, la mayoría de los métodos se compilarán en código nativo. Además, puede usar el parámetro -XX:CounterHalfLifeTime para establecer el tiempo del ciclo de vida media en segundos.
        A diferencia del contador del método, el contador del borde de retorno no cuenta el proceso de disminución del calor, por lo que este contador cuenta el número absoluto de veces que se repite el método.

        • compilar hilo

        Al compilar el código, se compila con varios subprocesos.

        • método en línea

        Inline está habilitado de forma predeterminada, -XX:-Inline, puede desactivarlo, pero no lo desactive, una vez que esté desactivado, tendrá un gran impacto en el rendimiento.
        Si el método está en línea depende de qué tan caliente sea el método y el tamaño del método.
        Si el código de bytes del método de un método muy caliente es inferior a 325 bytes, estará en línea. Este tamaño se ajusta mediante el parámetro -XX: MaxFreqInlinesSzie =N, pero esto es muy caliente y, a diferencia de la compilación de puntos de acceso, no hay parámetros para ajustar el calor.
        Si el método tiene menos de 35 bytecodes, debe estar en línea. Este tamaño se puede ajustar con el parámetro -XX:MaxInlinesSzie=N.

        • análisis de escape

        Es la optimización más agresiva realizada por la JVM, y es mejor no ajustar los parámetros relacionados.

      • ajuste de GC

        • Objetivo

        El tiempo de GC es lo suficientemente pequeño,
        la frecuencia de GC
        es lo suficientemente pequeña y el ciclo de GC completo es lo suficientemente largo y el tiempo es razonable. Es mejor que no suceda.

        • Principios y pasos de afinación
        1. La mayoría de las aplicaciones Java no requieren ajuste de GC
        1. La mayor parte de lo que necesita el ajuste del GC no es un problema de parámetros, sino un problema de código
        2. En el uso real, analizar la situación del GC para optimizar el código es mucho más que optimizar los parámetros del GC;
        3. El ajuste de GC es el último recurso. Hay
          tres opciones más importantes para el ajuste de GC:
          primero: elija un reciclador de GC apropiado
          segundo: elija un tamaño de montón apropiado
          tercero: elija la proporción de la generación joven en el montón

        ###Paso
        1. Supervisar el estado del GC

        Use varias herramientas de JVM para ver el registro actual, analice la configuración actual de los parámetros de JVM y analice la instantánea de la memoria del montón actual y el registro de gc De acuerdo con la división de memoria real de cada área y el tiempo de ejecución del GC, piense si optimizar;

        2. Analice los resultados y juzgue si se necesita optimización

        Si los parámetros se configuran razonablemente, no hay un registro de tiempo de espera en el sistema, la frecuencia del GC no es alta y el tiempo del GC no es alto, entonces no hay necesidad de realizar la optimización del GC; si el tiempo del GC excede 1-3 segundos , o GC frecuente, debe optimizarse; Nota
        : si se cumplen los siguientes indicadores, generalmente no hay necesidad de realizar GC:
        El tiempo de ejecución de GC menor es inferior a 50 ms;
        La ejecución de GC menor es poco frecuente, aproximadamente una vez cada 10 segundos;
        Completo El tiempo de ejecución del GC es inferior a 1 s;
        la frecuencia de ejecución del GC completo no es demasiado frecuente, no baja 1 vez en 10 minutos;

        3. Ajuste el tipo de GC y la asignación de memoria

        Si la asignación de memoria es demasiado grande o demasiado pequeña, o el colector de GC utilizado es relativamente lento, primero debe ajustar estos parámetros y primero encontrar una o varias máquinas para la versión beta y luego comparar el rendimiento de la máquina optimizada y la máquina no optimizada. Compare y tome una decisión final específica;

        4. Análisis y ajuste continuo

        Mediante ensayo y error continuo, analiza y encuentra los parámetros más adecuados

        5. Parámetros de aplicación completos

        Si se encuentran los parámetros más adecuados, se aplican a todos los servidores y se realiza un seguimiento.

      • Ajuste de JVM en la práctica

        ### Estrategia recomendada

        • selección de tamaño de generación joven
        1. Aplicación de la prioridad del tiempo de respuesta: configúrelo lo más grande posible hasta que esté cerca del límite de tiempo de respuesta mínimo del sistema (seleccionado de acuerdo con la situación real). En este caso, la frecuencia de recolección de la generación joven también es la más pequeña. Al mismo tiempo, reduzca el tiempo para alcanzar el objeto de la generación anterior.
        1. Aplicaciones con prioridad de rendimiento: configúrelo lo más grande posible, posiblemente llegando a Gbit. Debido a que no hay requisitos para el tiempo de respuesta, la recolección de basura se puede realizar en paralelo, lo que generalmente es adecuado para aplicaciones con más de 8 CPU.
        2. Evite la configuración demasiado pequeña. Cuando la configuración de la nueva generación es demasiado pequeña, dará lugar a: 1. El número de YGC será más frecuente. 2. Puede causar que los objetos YGC ingresen directamente a la generación anterior. Si la generación anterior está llena en este momento, se activará el FGC.
        • Selección de tamaño de generación anterior
        1. Aplicaciones con prioridad en el tiempo de respuesta: la generación anterior usa recopiladores concurrentes, por lo que su tamaño debe configurarse con cuidado. En general, se deben considerar algunos parámetros, como la tasa de sesión concurrente y la duración de la sesión. Si la configuración del montón es pequeña, puede causar fragmentación de la memoria y alta frecuencia de reciclaje Y la aplicación se detiene y utiliza el método tradicional de limpieza de marcas; si el montón es grande, se tarda mucho tiempo en recopilarlo. La solución óptima generalmente debe obtenerse consultando los siguientes datos:
        1. Información de recolección de basura simultánea, el número de colecciones de generación permanente simultáneas, información de GC tradicional y la proporción de tiempo dedicado a la recolección de generación joven y generación anterior.
        2. Aplicaciones que priorizan el rendimiento Por lo general, las aplicaciones que priorizan el rendimiento tienen una gran generación joven y una pequeña generación anterior. La razón es que esto puede reciclar la mayoría de los objetos a corto plazo tanto como sea posible, reduciendo los objetos a mediano plazo, mientras que los antiguos Dedicados almacenamiento de objetos de larga vida
    • Optimización del rendimiento del almacenamiento

      1. Intenta usar SSD
      1. Limpie regularmente los datos o guárdelos por separado según la naturaleza de los datos

escribir al final

inserte la descripción de la imagen aquí

El contenido ha sido compartido con todos, y puede seguir prestando atención. Continuaré trabajando duro para compartir más productos secos para usted.
Si tiene alguna pregunta, puede corregirme. He
preparado una mente. versión del mapa para todos, y la pequeña tarjeta al final del artículo

Supongo que te gusta

Origin blog.csdn.net/sulli_F/article/details/130545179
Recomendado
Clasificación