Golpe de rendimiento de GC para clase interna vs. clase anidada estática

Acabo de encontrar un efecto extraño y, al rastrearlo, noté que parece haber una diferencia de rendimiento sustancial para recostackr clases anidadas internas y estáticas. Considere este fragmento de código:

public class Test { private class Pointer { long data; Pointer next; } private Pointer first; public static void main(String[] args) { Test t = null; for (int i = 0; i < 500; i++) { t = new Test(); for (int j = 0; j < 1000000; j++) { Pointer p = t.new Pointer(); p.data = i*j; p.next = t.first; t.first = p; } } } } 

Entonces, lo que hace el código es crear una lista vinculada utilizando una clase interna. El proceso se repite 500 veces (con fines de prueba), descartando los objetos utilizados en la última ejecución (que están sujetos a GC).

Cuando se ejecuta con un límite de memoria ajustado (como 100 MB), este código tarda unos 20 minutos en ejecutarse en mi máquina. Ahora, simplemente reemplazando la clase interna con una clase anidada estática, puedo reducir el tiempo de ejecución a menos de 6 minutos. Aquí están los cambios:

  private static class Pointer { 

y

  Pointer p = new Pointer(); 

Ahora mis conclusiones de este pequeño experimento son que el uso de clases internas hace que sea mucho más difícil para el GC averiguar si los objetos pueden ser recolectados, haciendo que las clases anidadas estáticas sean más de 3 veces más rápidas en este caso.

Mi pregunta es si esta conclusión es correcta; en caso afirmativo, ¿cuál es el motivo y, en caso negativo, por qué las clases internas son mucho más lentas aquí?

Me imagino que esto se debe a 2 factores. La primera que ya tocaste. El segundo es el uso de clases internas no estáticas que generan más uso de memoria. ¿Porque preguntas? Debido a que las clases internas no estáticas también tienen acceso a sus miembros y métodos de datos que contienen clases, lo que significa que está asignando una instancia de Pointer que básicamente extiende la superclase. En el caso de clases internas no estáticas, no está extendiendo la clase contenedora. Aquí hay un ejemplo de lo que estoy hablando.

Test.java (clase interna no estática)

 public class Test { private Pointer first; private class Pointer { public Pointer next; public Pointer() { next = null; } } public static void main(String[] args) { Test test = new Test(); Pointer[] p = new Pointer[1000]; for ( int i = 0; i < p.length; ++i ) { p[i] = test.new Pointer(); } while (true) { try {Thread.sleep(100);} catch(Throwable t) {} } } } 

Test2.java (clase interna estática)

 public class Test2 { private Pointer first; private static class Pointer { public Pointer next; public Pointer() { next = null; } } public static void main(String[] args) { Test test = new Test(); Pointer[] p = new Pointer[1000]; for ( int i = 0; i < p.length; ++i ) { p[i] = new Pointer(); } while (true) { try {Thread.sleep(100);} catch(Throwable t) {} } } } 

Cuando se ejecutan ambos, puede ver que los elementos no estáticos ocupan más espacio de almacenamiento dynamic que los estáticos. Específicamente, la versión no estática usó 2,279,624 B y la versión estática usó 10,485,760 1,800,000 B.

Entonces, a lo que se reduce es que la clase interna no estática usa más memoria porque contiene una referencia (como mínimo) a la clase contenedora. La clase interna estática no contiene esta referencia por lo que la memoria nunca se asigna para ella. Al establecer su tamaño de stack tan bajo que en realidad estaba golpeando su stack, lo que resultó en la diferencia de rendimiento 3x.

El costo de recolección de basura aumenta de forma muy no lineal cuando se acerca al tamaño máximo de almacenamiento dynamic (-Xmx), con un límite artificial casi infinito donde la JVM finalmente se rinde y arroja un OutOfMemoryError. En este caso particular, está viendo que la parte escarpada de esa curva se encuentra entre la clase interna que es estática o no estática. La clase interna no estática no es realmente la causa, aparte de usar más memoria y tener más enlaces. He visto muchos otros cambios en el código que “causan” un golpeteo de GC, donde resultaron ser la desventurada savia que lo empujó al límite, y el límite del montón simplemente debe establecerse más alto. Este comportamiento no lineal generalmente no debería considerarse un problema con el código, es intrínseco a la JVM.

Por supuesto, por otro lado, hincharse es hincharse. En el caso actual, un buen hábito es hacer que las clases internas sean estáticas “por defecto” a menos que el acceso a la instancia externa sea útil.