java - Rendimiento de GC alcanzado para clase interna vs. clase anidada estática
garbage-collection nested-class (2)
Me imagino que esto se debe a 2 factores. El primero que ya has mencionado. 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 extiende 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 el no estático ocupa más espacio en el montón que el estático. 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 a que la clase interna no estática usa más memoria porque contiene una referencia (al menos) a la clase contenedora. La clase interna estática no contiene esta referencia por lo que la memoria nunca se asigna para ello. Al configurar el tamaño de almacenamiento dinámico tan bajo que en realidad estaba sacudiendo su montón, lo que resultó en la diferencia de rendimiento 3x.
Me encontré con un efecto extraño y, mientras lo rastreaba, noté que parece haber una diferencia sustancial en el rendimiento para recopilar clases anidadas internas frente a estáticas. Considera 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 (para fines de prueba), descartando los objetos utilizados en la última ejecución (que quedan 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í?
El costo de recolección de basura aumenta de forma muy no lineal cuando se acerca al tamaño máximo de almacenamiento dinámico (-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 empinada 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 ser mayor. Este comportamiento no lineal generalmente no se debe considerar un problema con el código, es intrínseco a la JVM.
Por supuesto, por otro lado, la hinchazón es hinchazón. En el caso actual, un buen hábito es hacer que las clases internas estén "estáticas" por defecto, a menos que el acceso a la instancia externa sea útil.