java - not - ¿Las referencias invisibles siguen siendo un problema en las JVM recientes?
memory leak java (4)
Estaba leyendo Java Platform Performance (lamentablemente el enlace parece haber desaparecido de Internet desde que originalmente planteé esta pregunta) y la sección A.3.3 me preocupaba.
He estado trabajando en la suposición de que una variable que se salió del alcance ya no se consideraría una raíz de GC, pero este documento parece contradecir eso.
¿Las JVM recientes, en particular la versión 1.6.0_07 de Sun, todavía tienen esta limitación? Si es así, entonces tengo un montón de código para analizar ...
Formulo la pregunta porque el documento es de 1999; a veces las cosas cambian, particularmente en el mundo de GC.
Como el documento ya no está disponible, me gustaría parafrasear la preocupación. El documento implicaba que las variables que se definieron dentro de un método se considerarían una raíz de GC hasta que el método saliera, y no hasta que finalizara el bloque de código. Por lo tanto, establecer la variable en nulo era necesaria para permitir que el Objeto al que se hace referencia fuera basura.
Esto significaba que una variable local definida en un bloque condicional en el método main () (o método similar que contenía un bucle infinito) causaría una fuga de memoria única a menos que anulara una variable justo antes de que se saliera del alcance.
El código de la respuesta elegida ilustra bien el problema. En la versión de la JVM a la que se hace referencia en el documento, el objeto foo no se puede recolectar como basura cuando se sale del alcance al final del bloque try. En cambio, la JVM mantendrá abierta la referencia hasta el final del método main (), aunque es imposible que algo use esa referencia.
Este parece ser el origen de la idea de que anular una referencia de variable ayudaría al recolector de basura, incluso si la variable estaba a punto de caer fuera del alcance.
Este código debería aclararlo:
public class TestInvisibleObject{
public static class PrintWhenFinalized{
private String s;
public PrintWhenFinalized(String s){
System.out.println("Constructing from "+s);
this.s = s;
}
protected void finalize() throws Throwable {
System.out.println("Finalizing from "+s);
}
}
public static void main(String[] args) {
try {
PrintWhenFinalized foo = new PrintWhenFinalized("main");
} catch (Exception e) {
// whatever
}
while (true) {
// Provoke garbage-collection by allocating lots of memory
byte[] o = new byte[1024];
}
}
}
En mi máquina (jdk1.6.0_05) imprime:
Construyendo desde principal
Finalizando desde principal
Entonces parece que los problemas han sido arreglados.
Tenga en cuenta que el uso de System.gc () en lugar del bucle no hace que el objeto se recopile por algún motivo.
¿Realmente tienes tanto código para analizar? Básicamente, solo puedo ver que esto es un problema significativo para los métodos muy antiguos, que suelen ser solo los que se encuentran en la parte superior de la pila de cada subproceso.
No me sorprendería en absoluto si no está fijado en este momento, pero no creo que sea tan significativo como parece temer.
El artículo dice que:
... una implementación eficiente de la JVM es poco probable que ponga a cero la referencia cuando se sale del alcance
Creo que esto sucede debido a situaciones como esta:
public void doSomething() {
for(int i = 0; i < 10 ; i++) {
String s = new String("boo");
System.out.println(s);
}
}
Aquí, la misma referencia es utilizada por la "JVM eficiente" en cada declaración de Cadenas, pero habrá 10 Cadenas nuevas en el montón si el GC no se activa.
En el ejemplo del artículo, creo que la referencia a foo se mantiene en la pila porque la "JVM eficiente" cree que es muy probable que se cree otro objeto foo y, de ser así, usará la misma referencia. Pensamientos ???
public void run() {
try {
Object foo = new Object();
foo.doSomething();
} catch (Exception e) {
// whatever
}
while (true) { // do stuff } // loop forever
}
También realicé la siguiente prueba con perfil:
public class A {
public static void main(String[] args) {
A a = new A();
a.test4();
}
public void test1() {
for(int i = 0; i < 10 ; i++) {
B b = new B();
System.out.println(b.toString());
}
System.out.println("b is collected");
}
public void test2() {
try {
B b = new B();
System.out.println(b.toString());
} catch (Exception e) {
}
System.out.println("b is invisible");
}
public void test3() {
if (true) {
B b = new B();
System.out.println(b.toString());
}
System.out.println("b is invisible");
}
public void test4() {
int i = 0;
while (i < 10) {
B b = new B();
System.out.println(b.toString());
i++;
}
System.out.println("b is collected");
}
public A() {
}
class B {
public B() {
}
@Override
public String toString() {
return "I''m B.";
}
}
}
y llegar a las conclusiones:
teste1 -> b se recoge
teste2 -> b es invisible
teste3 -> b es invisible
teste4 -> b se recoge
... así que creo que, en bucles, la JVM no crea variables invisibles cuando termina el bucle porque es poco probable que vuelvan a declararse fuera del bucle.
¿¿Alguna idea??
El problema sigue ahí. Lo probé con Java 8 y pude probarlo.
Debe tener en cuenta las siguientes cosas:
La única manera de forzar una recolección de basura garantizada es probar una asignación que termine en un OutOfMemoryError ya que se requiere que la JVM intente liberar objetos no utilizados antes de lanzar. Sin embargo, esto no se cumple si la cantidad solicitada es demasiado grande para tener éxito alguna vez, es decir, excede el espacio de direcciones. Tratar de aumentar la asignación hasta obtener un OOME es una buena estrategia.
El GC garantizado descrito en el punto 1 no garantiza una finalización. La hora en que se invocan los métodos de finalización () no se especifica, es posible que nunca se llamen. Por lo tanto, agregar un método finalize () a una clase puede evitar que se recopilen sus instancias, por lo que finalizar no es una buena opción para analizar el comportamiento del GC.
Crear otra nueva variable local después de que una variable local haya salido del alcance reutilizará su lugar en el marco de la pila. En el siguiente ejemplo, el objeto a se recogerá ya que su lugar en el marco de la pila está ocupado por la variable local b. Pero b dura hasta el final del método principal ya que no hay otra variable local que ocupe su lugar.
import java.lang.ref.*; public class Test { static final ReferenceQueue<Object> RQ=new ReferenceQueue<>(); static Reference<Object> A, B; public static void main(String[] s) { { Object a=new Object(); A=new PhantomReference<>(a, RQ); } { Object b=new Object(); B=new PhantomReference<>(b, RQ); } forceGC(); checkGC(); } private static void forceGC() { try { for(int i=100000;;i+=i) { byte[] b=new byte[i]; } } catch(OutOfMemoryError err){ err.printStackTrace();} } private static void checkGC() { for(;;) { Reference<?> r=RQ.poll(); if(r==null) break; if(r==A) System.out.println("Object a collected"); if(r==B) System.out.println("Object b collected"); } } }