Pregunta Hemorragia masiva de memoria que hace que el tamaño del montón pase de unos 64 mb a 1,5 gb en aproximadamente 8 segundos. ¿Problema con el recolector de basura?


Aquí está el problema:

the memory usage balloons out of control

Como puede ver, ¡el uso de la memoria se sale de control! Tuve que agregar argumentos a la JVM para aumentar el tamaño máximo para evitar errores de falta de memoria mientras descubro lo que está sucediendo. ¡No está bien!

Resumen de la aplicación básica (para el contexto)

Esta aplicación se usará (eventualmente) para operaciones básicas de CV y ​​concordancia de plantilla con fines de automatización. Quiero alcanzar la mayor velocidad posible de cuadros para ver la pantalla y manejar todo el procesamiento a través de una serie de hilos de consumo separados.

Rápidamente descubrí que la clase de Robot original es realmente terrible en cuanto a la velocidad, así que abrí la fuente, saqué todo el esfuerzo duplicado y desperdicié, y lo reconstruí como mi propia clase llamada FastRobot.

El Código de la clase:

public class FastRobot {
    private Rectangle screenRect;
    private GraphicsDevice screen;
    private final Toolkit toolkit;
    private final Robot elRoboto;
    private final RobotPeer peer;
    private final Point gdloc;
    private final DirectColorModel screenCapCM;
    private final int[] bandmasks;

    public FastRobot() throws HeadlessException, AWTException {
        this.screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
        this.screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
        toolkit = Toolkit.getDefaultToolkit();
        elRoboto = new Robot();
        peer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);

        gdloc = screen.getDefaultConfiguration().getBounds().getLocation();
        this.screenRect.translate(gdloc.x, gdloc.y);

        screenCapCM = new DirectColorModel(24,
                /* red mask */    0x00FF0000,
                /* green mask */  0x0000FF00,
                /* blue mask */   0x000000FF);
        bandmasks = new int[3];
        bandmasks[0] = screenCapCM.getRedMask();
        bandmasks[1] = screenCapCM.getGreenMask();
        bandmasks[2] = screenCapCM.getBlueMask();

        Toolkit.getDefaultToolkit().sync();
    }

    public void autoResetGraphicsEnv() {
        this.screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
        this.screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
    }

    public void manuallySetGraphicsEnv(Rectangle screenRect, GraphicsDevice screen) {
        this.screenRect = screenRect;
        this.screen = screen;
    }


    public BufferedImage createBufferedScreenCapture(int pixels[]) throws HeadlessException, AWTException {
//      BufferedImage image;
        DataBufferInt buffer;
        WritableRaster raster;

        pixels = peer.getRGBPixels(screenRect);
        buffer = new DataBufferInt(pixels, pixels.length);

        raster = Raster.createPackedRaster(buffer, screenRect.width, screenRect.height, screenRect.width, bandmasks, null);
        return new BufferedImage(screenCapCM, raster, false, null);
    }

    public int[] createArrayScreenCapture() throws HeadlessException, AWTException {
            return peer.getRGBPixels(screenRect);
    }

    public WritableRaster createRasterScreenCapture(int pixels[]) throws HeadlessException, AWTException {
    //  BufferedImage image;
        DataBufferInt buffer;
        WritableRaster raster;

        pixels = peer.getRGBPixels(screenRect);
        buffer = new DataBufferInt(pixels, pixels.length);

        raster = Raster.createPackedRaster(buffer, screenRect.width, screenRect.height, screenRect.width, bandmasks, null);
    //  SunWritableRaster.makeTrackable(buffer);
        return raster;
    }
}

En esencia, todo lo que he cambiado del original es mover muchas de las asignaciones de los cuerpos de las funciones, y establecerlas como atributos de la clase para que no se llamen todas las veces. Haciendo esto en realidad tenía una significativo afectar a la velocidad de cuadros. Incluso en mi computadora portátil severamente bajo, pasó de ~ 4 fps con la clase de stock Robot, a ~ 30 fps con mi clase FastRobot.

Primer examen:

Cuando comencé a realizar errores de memoria en mi programa principal, establecí esta prueba muy simple para vigilar el FastRobot. Nota: este es el código que produjo el perfil de montón arriba.

public class TestFBot {
    public static void main(String[] args) {
        try {
            FastRobot fbot = new FastRobot();

            double startTime = System.currentTimeMillis();
            for (int i=0; i < 1000; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

        } catch (AWTException e) {
            e.printStackTrace();
        }
    }
}

Examinado:

No hace esto cada el tiempo, que es realmente extraño (¡y frustrante!). De hecho, rara vez lo hace en absoluto con el código anterior. Sin embargo, el problema de la memoria se convierte fácilmente reproducible si tengo múltiples bucles for back.

Prueba 2

public class TestFBot {
    public static void main(String[] args) {
        try {
            FastRobot fbot = new FastRobot();

            double startTime = System.currentTimeMillis();
            for (int i=0; i < 1000; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 500; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 200; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 1500; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

        } catch (AWTException e) {
            e.printStackTrace();
        }
    }
}

Examinado

El montón fuera de control ahora es reproducible, diría aproximadamente el 80% del tiempo. He visto todo el perfilador, y lo que más nota (creo) es que el recolector de basura aparentemente se detiene derecho como el cuarto y último ciclo comienza.

El resultado del código anterior dio los siguientes tiempos:

Time taken: 24.282    //Loop1
Time taken: 11.294    //Loop2
Time taken: 7.1       //Loop3
Time taken: 70.739    //Loop4

Ahora, si sumas los primeros tres bucles, suman 42.676, lo que sospechosamente corresponde a la hora exacta en que el recolector de basura se detiene y la memoria aumenta.

enter image description here

Ahora, este es mi primer rodeo con perfiles, sin mencionar la primera vez que pensé en la recolección de basura, siempre fue algo que funcionaba mágicamente de fondo, así que no estoy seguro de qué, en todo caso, me he enterado.

Información de perfil adicional

enter image description here

Augusto sugirió mirar el perfil de la memoria. Hay más de 1500 int[] que se enumeran como "inalcanzable, pero aún no recopilado". Estos son sin duda el int[] matrices que el peer.getRGBPixels() crea, pero por alguna razón no están siendo destruidos. Esta información adicional, desafortunadamente, solo aumenta mi confusión, ya que no estoy seguro por qué el GC no los estaría recogiendo


Perfil con argumento de montón pequeño -Xmx256m:

En sugerencia irrecuperable y Hot Licks, establezco el tamaño máximo de almacenamiento dinámico en algo significativamente más pequeño. Mientras esto hace evitar que haga 1gb de salto en el uso de la memoria, todavía no explica por qué el programa se está expandiendo a su tamaño máximo de almacenamiento dinámico al ingresar a la 4ª iteración.

enter image description here

Como puede ver, el problema exacto todavía existe, se ha reducido. ;) El problema con esta solución es que el programa, por alguna razón, sigue consumiendo toda la memoria que puede, también hay un cambio marcado en el rendimiento de fps desde la primera las iteraciones, que consumen muy poca memoria, y la iteración final, que consume tanta memoria como puede.

La pregunta permanece por qué ¿se está hinchando en absoluto?


Resultados después de presionar el botón "Forzar recolección de basura":

A sugerencia de jtahlborn, golpeé el Fuerza la recolección de basura botón. Funcionó maravillosamente. Va de 1gb de uso de memoria, hasta la línea base de 60mb o más.

enter image description here

Entonces, esta parece ser la cura. La pregunta ahora es, ¿cómo obligo programáticamente al GC a hacer esto?


Resultados después de agregar Peer local al alcance de la función:

En la sugerencia de David Waters, modifiqué createArrayCapture() función para que tenga un local Peer objeto.

Lamentablemente, no hubo cambios en el patrón de uso de la memoria.

enter image description here

Todavía se vuelve enorme en la tercera o cuarta iteración.


Análisis de grupo de memoria:

Capturas de pantalla de las diferentes agrupaciones de memoria

Todas las piscinas:

All pools

Eden Pool:

Eden Pool

Viejo Gen:

Old Gen
Casi todo el uso de la memoria parece estar en este grupo.

Nota: PS Survivor Space tuvo (aparentemente) 0 uso


Me quedan varias preguntas:

(a) ¿significa el gráfico Garbage Profiler lo que creo que significa? ¿O estoy confundiendo la correlación con la causalidad? Como dije, estoy en un área desconocida con estos problemas.

(b) Si es el recolector de basura ... ¿qué hago al respecto ...? ¿Por qué se detiene por completo y luego se ejecuta a un ritmo reducido para el resto del programa?

(c) ¿Cómo soluciono esto?

¿Que está pasando aqui?


32
2018-02-28 16:46


origen


Respuestas:


Intente especificar un recolector de basura manualmente.

Concurrent Mark Sweep es un buen propósito general que proporciona un buen equilibrio entre baja pausa y rendimiento razonable.

Si está en Java 7 o posterior Java 6, el recopilador G1 probablemente sea mejor, ya que también es capaz de evitar la fragmentación de la memoria.

Usted puede verificar Java SE Hotspot Virtual Machine Recogida de Basura Tuning página para más información y punteros :-D


4
2018-03-02 11:13



Dijiste que moviste la creación de objetos de los métodos a los campos de la clase. Fue una de las dependencias que movió "par"? p.ej.

peer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);

Bien podría ser que el par se aferre a todas las capturas de pantalla tomadas durante la vida del objeto, esto se borrará cuando el par se salga del alcance, final del método en Robot, la vida de la clase para FastRobot.

Intente trasladar la creación y el alcance de los pares a su método y vea cuál es la diferencia.

public int[] createArrayScreenCapture() throws HeadlessException, AWTException {
        RobotPeer localPeer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);
        return localPeer.getRGBPixels(screenRect);
}

Intento 2

Entonces, esta parece ser la cura. La pregunta ahora es, ¿cómo lo hago?   ¿forzar gramaticalmente al GC a hacer esto?

Puede llamar a System.gc () para solicitar una recolección de basura. Tenga en cuenta que esta es una solicitud, no una demanda. La JVM solo ejecutará la recolección de basura si cree que vale la pena.

Como puede ver, el problema exacto aún existe, simplemente se ha realizado   menor. ;) El problema con esta solución es que el programa, para algunos   razón, todavía está consumiendo toda la memoria que puede: hay   también un cambio marcado en el rendimiento de fps desde la primera las iteraciones,   que consumen muy poca memoria, y la iteración final, que   consume tanta memoria como puede.

La pregunta sigue siendo ¿por qué se está hinchando en absoluto?

La JVM intentará y solo ejecutará una Colección mayor de basura cuando sea absolutamente necesario (se usará la mayoría del montón). (Lea sobre Young Generation vs Old Generation y dentro de Young Generation, espacio de Eden y espacio de sobrevivientes). Por lo tanto, espere que las aplicaciones Java de larga duración o de memoria se queden cerca del tamaño de almacenamiento dinámico máximo. Vale la pena señalar que para que la memoria se mueva a la Vieja Generación debe sobrevivir a 3 carreras menores de GC (Eden => Survivor 1 => Survivor 2 => Vieja Generación [Dependiendo de qué JVM está ejecutando y qué esquema de GC ha seleccionado en argumentos de línea de comando.])

En cuanto a por qué cambia este comportamiento, podría ser cualquier cantidad de cosas. Este último ciclo es el más largo, ¿Bloque System.getCurrentTimeMillis () durante el tiempo suficiente para que el GC tenga un ir en un hilo diferente? ¿Entonces el problema solo se muestra en los bucles más largos? El proceso de tomar una captura de pantalla me suena bastante bajo, supongo que se implementó con una llamada al kernel del sistema operativo, ¿bloquea esto el proceso en el espacio kernal impidiendo que se ejecuten otros subprocesos? (lo que detendría el funcionamiento del gc en el hilo de fondo).

Mira esto http://www.javacodegeeks.com/2012/01/practical-garbage-collection-part-1.html para una introducción al mundo de la recolección de basura. O Explicación de la memoria Java (SUN JVM) para montones más enlaces.

Espero que haya ayudado.


3
2018-02-28 17:46