¿Cuáles son las "mejores prácticas" para crear (y liberar) millones de objetos pequeños?
Estoy escribiendo un programa de ajedrez en Java y el algoritmo de búsqueda genera un único objeto "Mover" para cada movimiento posible, y una búsqueda nominal puede generar fácilmente más de un millón de objetos en movimiento por segundo. La JVM GC ha podido manejar la carga en mi sistema de desarrollo, pero estoy interesado en explorar enfoques alternativos que:
- Minimizar la sobrecarga de recolección de basura y
- Reducir la huella de memoria máxima para los sistemas de gama baja.
La gran mayoría de los objetos tienen una vida muy corta, pero aproximadamente el 1% de los movimientos generados se conservan y se devuelven como valor persistente, por lo que cualquier técnica de agrupación o almacenamiento en caché tendría que proporcionar la capacidad de excluir objetos específicos para que no se reutilicen. .
No espero un código de ejemplo completo, pero agradecería sugerencias para más lecturas / investigaciones, o ejemplos de código abierto de naturaleza similar.
fuente
Respuestas:
Ejecute la aplicación con recolección de basura detallada:
Y te dirá cuándo se cobrará. Habría dos tipos de barridos, uno rápido y uno completo.
La flecha es antes y después del tamaño.
Siempre que se trate de GC y no de GC completo, estará a salvo en casa. El GC normal es un coleccionista de copias en la 'generación joven', por lo que los objetos a los que ya no se hace referencia simplemente se olvidan, que es exactamente lo que querría.
Probablemente sea útil leer el ajuste de recolección de basura de la máquina virtual de Java SE 6 HotSpot .
fuente
Desde la versión 6, el modo servidor de JVM emplea una técnica de análisis de escape . Al usarlo, puede evitar la GC por completo.
fuente
Bueno, ¡aquí hay varias preguntas en una!
1 - ¿Cómo se gestionan los objetos de corta duración?
Como se indicó anteriormente, la JVM puede tratar perfectamente con una gran cantidad de objetos de corta duración, ya que sigue la Hipótesis Generacional Débil .
Tenga en cuenta que estamos hablando de objetos que llegaron a la memoria principal (montón). Este no es siempre el caso. Muchos de los objetos que crea ni siquiera dejan un registro de CPU. Por ejemplo, considere este for-loop
No pensemos en el desenrollado de bucles (una optimización que la JVM realiza en gran medida en su código). Si
max
es igual aInteger.MAX_VALUE
, es posible que el ciclo tarde un poco en ejecutarse. sin embargo, eli
variable nunca escapará del bloque de bucle. Por lo tanto, la JVM pondrá esa variable en un registro de la CPU, la incrementará regularmente pero nunca la enviará de regreso a la memoria principal.Por lo tanto, crear millones de objetos no es un gran problema si se usan solo localmente. Estarán muertos antes de ser almacenados en el Edén, por lo que la CG ni siquiera los notará.
2 - ¿Es útil reducir la sobrecarga del GC?
Como de costumbre, depende.
Primero, debe habilitar el registro de GC para tener una visión clara de lo que está sucediendo. Puede habilitarlo con
-Xloggc:gc.log -XX:+PrintGCDetails
.Si su aplicación pasa mucho tiempo en un ciclo de GC, entonces sí, ajuste el GC; de lo contrario, es posible que no valga la pena.
Por ejemplo, si tiene un GC joven cada 100 ms que tarda 10 ms, pasa el 10% de su tiempo en el GC y tiene 10 colecciones por segundo (lo que es enorme). En tal caso, no dedicaría tiempo a ajustar el GC, ya que esos 10 GC / s todavía estarían allí.
3 - Algo de experiencia
Tuve un problema similar en una aplicación que estaba creando una gran cantidad de una clase determinada. En los registros de GC, noté que la tasa de creación de la aplicación era de alrededor de 3 GB / s, que es demasiado (vamos ... ¡¿3 gigabytes de datos por segundo?!).
El problema: demasiados GC frecuentes provocados por la creación de demasiados objetos.
En mi caso, adjunté un generador de perfiles de memoria y noté que una clase representaba un gran porcentaje de todos mis objetos. Rastreé las instancias para descubrir que esta clase era básicamente un par de valores booleanos envueltos en un objeto. En ese caso, había dos soluciones disponibles:
Vuelva a trabajar el algoritmo para que no devuelva un par de booleanos, sino que tenga dos métodos que devuelvan cada booleano por separado
Almacene los objetos en caché, sabiendo que solo había 4 instancias diferentes
Elegí el segundo, ya que tuvo el menor impacto en la aplicación y fue fácil de introducir. Me tomó minutos poner una fábrica con un caché no seguro para subprocesos (no necesitaba seguridad para subprocesos, ya que eventualmente tendría solo 4 instancias diferentes).
La tasa de asignación bajó a 1 GB / s, al igual que la frecuencia de GC joven (dividida por 3).
Espero que ayude !
fuente
Si solo tiene objetos de valor (es decir, sin referencias a otros objetos) y realmente, pero me refiero a toneladas y toneladas de ellos, puede usar directamente
ByteBuffers
con el orden de bytes nativo [lo último es importante] y necesita algunos cientos de líneas de código para asignar / reutilizar + getter / setters. Getters se parecen along getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}
Eso resolvería el problema de GC casi por completo siempre que asigne una sola vez, es decir, una gran parte y luego administre los objetos usted mismo. En lugar de referencias, solo tendría un índice (es decir,
int
) en elByteBuffer
que debe transmitirse. Es posible que también deba alinear la memoria usted mismo.La técnica se sentiría como si se usara
C and void*
, pero con un poco de envoltura es soportable. Un inconveniente del rendimiento podría ser la comprobación de límites si el compilador no lo elimina. Una ventaja importante es la localidad: si procesa las tuplas como vectores, la falta del encabezado del objeto también reduce la huella de memoria.Aparte de eso, es probable que no necesite un enfoque de este tipo, ya que la generación joven de prácticamente todas las JVM muere trivialmente y el costo de asignación es solo un aumento de puntero. El costo de asignación puede ser un poco más alto si usa
final
campos, ya que requieren un límite de memoria en algunas plataformas (a saber, ARM / Power), aunque en x86 es gratis.fuente
Suponiendo que encuentre que GC es un problema (como otros señalan que podría no serlo), implementará su propia administración de memoria para su caso especial, es decir, una clase que sufre una pérdida masiva. Prueba la agrupación de objetos, he visto casos en los que funciona bastante bien. La implementación de grupos de objetos es un camino muy transitado, por lo que no es necesario volver a visitar aquí, esté atento a:
Mida antes / después, etc., etc.
fuente
Me encontré con un problema similar. En primer lugar, intente reducir el tamaño de los objetos pequeños. Introdujimos algunos valores de campo predeterminados haciendo referencia a ellos en cada instancia de objeto.
Por ejemplo, MouseEvent tiene una referencia a la clase Point. Almacenamos en caché los puntos y los referenciamos en lugar de crear nuevas instancias. Lo mismo para, por ejemplo, cadenas vacías.
Otra fuente fueron múltiples booleanos que fueron reemplazados por un int y para cada booleano usamos solo un byte del int.
fuente
Me ocupé de este escenario con algún código de procesamiento XML hace algún tiempo. Me encontré creando millones de objetos de etiquetas XML que eran muy pequeños (generalmente solo una cadena) y de muy corta duración (la falla de una verificación XPath significaba que no había coincidencias, así que descarte).
Hice algunas pruebas serias y llegué a la conclusión de que solo podía lograr una mejora del 7% en la velocidad utilizando una lista de etiquetas descartadas en lugar de crear otras nuevas. Sin embargo, una vez implementada, descubrí que la cola libre necesitaba un mecanismo agregado para podarla si se volvía demasiado grande; esto anuló completamente mi optimización, así que la cambié a una opción.
En resumen, probablemente no valga la pena, pero me alegra ver que estás pensando en ello, demuestra que te preocupas.
fuente
Dado que está escribiendo un programa de ajedrez, existen algunas técnicas especiales que puede utilizar para obtener un rendimiento decente. Un enfoque simple es crear una gran variedad de longs (o bytes) y tratarlo como una pila. Cada vez que su generador de movimientos crea movimientos, coloca un par de números en la pila, por ejemplo, se mueve de una casilla a otra. A medida que evalúe el árbol de búsqueda, irá sacando movimientos y actualizando una representación del tablero.
Si quieres usar objetos de poder expresivo. Si quieres velocidad (en este caso) hazlo nativo.
fuente
Una solución que he usado para tales algoritmos de búsqueda es crear solo un objeto Move, mutarlo con un nuevo movimiento y luego deshacer el movimiento antes de dejar el alcance. Probablemente esté analizando solo un movimiento a la vez y luego simplemente almacenando el mejor movimiento en algún lugar.
Si eso no es factible por alguna razón, y desea disminuir el uso máximo de memoria, un buen artículo sobre la eficiencia de la memoria está aquí: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- tutorial.pdf
fuente
Simplemente cree sus millones de objetos y escriba su código de la manera adecuada: no guarde referencias innecesarias a estos objetos. GC hará el trabajo sucio por usted. Puede jugar con GC detallado como se mencionó para ver si realmente están GC'd. Java ES sobre la creación y liberación de objetos. :)
fuente
Creo que debería leer sobre la asignación de pilas en Java y el análisis de escape.
Porque si profundiza en este tema, puede encontrar que sus objetos ni siquiera están asignados en el montón, y GC no los recopila de la misma manera que los objetos en el montón.
Hay una explicación de wikipedia del análisis de escape, con un ejemplo de cómo funciona esto en Java:
http://en.wikipedia.org/wiki/Escape_analysis
fuente
No soy un gran admirador de GC, así que siempre trato de encontrar formas de evitarlo. En este caso, sugeriría usar el patrón Object Pool :
La idea es evitar la creación de nuevos objetos almacenándolos en una pila para poder reutilizarlos más tarde.
fuente
Los grupos de objetos proporcionan mejoras tremendas (en ocasiones 10 veces) sobre la asignación de objetos en el montón. ¡Pero la implementación anterior usando una lista vinculada es ingenua y incorrecta! La lista enlazada crea objetos para gestionar su estructura interna anulando el esfuerzo. Un Ringbuffer que usa una variedad de objetos funciona bien. En el ejemplo, dar (un programa de ajedrez que gestiona movimientos) el Ringbuffer debe estar envuelto en un objeto de soporte para la lista de todos los movimientos calculados. Solo se pasarían entonces las referencias de objeto del titular de movimientos.
fuente