Usando VelocityContext: OutOfMemoryError: Java heap space

Ojo con el uso del VelocityContext.
Parece que el recolector de basura no funciona todo lo bien que deberia, al menos en Java5.
Hace ya tiempo tuvimos un problema de consumo excesivo de memoria al reutilizar una List en el mismo objeto VelocityContext
.
Digamos que, por ejemplo, teniamos en una plantilla vm, el siguiente codigo:


#foreach $absurdo in $absurdos
 private String tremendo${absurdo};
#end

Esa plantilla la queriamos usar para distintas cosas pasandole disntinta informacion en $absurdos. Asi que reutilizabamos nuestro objecto VelocityContext, modificandole el valor del contexto absurdos.
Algo como esto:


VelocityContext vcontext=new VelocityContext();
List ls=new LinkedList();
//Puebla ls
for (int j=0; j<900000; j++){
	ls.add("Foo"+j);
}
vcontext.put("absurdos", ls);

//Escribimos el fichero usando la plantilla vm
......
//Y reutilizamos
ls=new LinkedList();
//Puebla ls
for (int j=0; j<900000; j++){
	ls.add("Bar"+j);
}
vcontext.put("absurdos", ls);
//Escribimos un nuevo fichero usando la plantilla vm
......

Bien, pues parece que el recolector de basura no hace su trabajo adecuadamente, al menos de manera automatica….y el consumo de memoria se nos disparaba…

Un pequeño programa cutrecillo para demostrar el problema (Con JDK 1.5.10).


package test.absurdo;

import java.util.LinkedList;
import java.util.List;
import org.apache.velocity.VelocityContext;

public class TestAbsurdo {

	private static final
	String murcielago="El veloz murciélago hindú comía feliz"+
	" cardillo y kiwi.";

	private static long mem0,mem1,mem2;

	public static long usoMem(){
		return Runtime.getRuntime().totalMemory() -
			Runtime.getRuntime().freeMemory();
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		VelocityContext vcontext=new VelocityContext();
		List ls=new LinkedList();
		mem0=usoMem();
		for(int i=1; i<3; i++){
			for (int j=0; j<900000; j++){
				ls.add(murcielago);
			}
			vcontext.put("testabsurdo", ls);
			mem1=usoMem();
			System.out.println("En la iteracion="+i+" mem="+(mem1-mem0));
			ls=new LinkedList();
			mem2=usoMem();
			System.out.println("Despues del new, mem="+(mem2-mem0));
		}
		System.out.println("fin");
	}
}

La salida por consola es:


En la iteracion=1 mem=21403744
Despues del new, mem=21446016
En la iteracion=2 mem=43010320
Despues del new, mem=43010320
fin

Preocupante.

En nuestro caso nos comia la memoria hasta desbordar el heap, y romper la ejecucion.

Curisomente, si forzamos el recolector de basurar, el resultado es muy distinto:


public static void main(String[] args) {
VelocityContext vcontext=new VelocityContext();
List ls=new LinkedList();
mem0=usoMem();
for(int i=1; i<3; i++){
	for (int j=0; j<900000; j++){
		ls.add(murcielago);
	}
	vcontext.put("testabsurdo", ls);
	mem1=usoMem();
	System.out.println("En la iteracion="+i+" mem="+(mem1-mem0));
	ls=new LinkedList();

//FORZAMOS GARBAGE COLLECTION
System.gc();System.gc();System.gc();System.gc();
System.gc();System.gc();System.gc();System.gc();

	mem2=usoMem();
	System.out.println("Despues del new, mem="+(mem2-mem0));
	}
System.out.println("fin");
}

La salida por consola es:


En la iteracion=1 mem=21403744
Despues del new, mem=21402928
En la iteracion=2 mem=43079200
Despues del new, mem=21402928
fin

Lo que demuestra que el problema esta en la gestion del recolector de basuras.

En cualquier caso, como no nos mola nada programar la gestion de la maquina virtual, optamos por otra solucion, menos agresiva. Y aun mas eficaz. Remplazamos las posible llamada al recolector de basura, y recargamos todo lo que necesitamos en el context.


public static void main(String[] args) {
VelocityContext vcontext=new VelocityContext();
List ls=new LinkedList();
mem0=usoMem();
for(int i=1; i<3; i++){
	for (int j=0; j<900000; j++){
		ls.add(murcielago);
	}
	vcontext.put("testabsurdo", ls);
	mem1=usoMem();
	System.out.println("En la iteracion="+i+" mem="+(mem1-mem0));
	ls=new LinkedList();

//RESET VelocityContext
context=null;
context = new VelocityContext();

	mem2=usoMem();
	System.out.println("Despues del new, mem="+(mem2-mem0));
	}
System.out.println("fin");
}

Con salida:


En la iteracion=1 mem=21403744
Despues del new y el reset, mem=21446016
En la iteracion=2 mem=21617248
Despues del new y el reset, mem=21617248
fin

Quizas, lo que debiamos haber hecho desde un principio.

Comentarios

  1. En realidad el problema no es del recolector de basura, y tampoco es de VelocityContext, si sustituyes en tu ejemplo VelocityContext por un simple HashMap, tendrás el mismo problema.
    El tema es que en Java no podemos estar seguros nunca de cuando se invoca al recolector de basura, la llamada a System.gc() es solo una recomendación( aunque 6 llamadas seguidas sean una gran recomendación :p). Como comentas, la única solución es portarse bien con el recolector de basura, y dejarle las cosas ya mascadas. Por ejemplo antes de ls=new LinkedList(); lo ideal sería hacer un ls.clear();. Esto tampoco soluciona definitivamente el problema pero facilita la labor del recolector y almeno en este ejemplo es equivalente a llamar 6 veces a System.gc().

Comments are closed.