2012-06-08 12 views
11

Estoy leyendo un archivo muy grande y extrayendo algunas pequeñas porciones de texto de cada línea. Sin embargo, al final de la operación, me queda muy poca memoria para trabajar. Parece que el recolector de basura no puede liberar memoria después de leer en el archivo.Java no recolección de basura memoria

Mi pregunta es: ¿hay alguna manera de liberar esta memoria? ¿O es esto un error de JVM?

Creé un SSCCE para demostrar esto. Lee en un archivo de 1 mb (2 mb en Java debido a la codificación de 16 bits) y extrae un carácter de cada línea (~ 4000 líneas, por lo que debería ser de aproximadamente 8 kb). ¡Al final de la prueba, todavía se usan los 2 mb completos!

El uso de la memoria inicial:

Allocated: 93847.55 kb 
Free: 93357.23 kb 

Inmediatamente después de leer en el archivo (antes de cualquier recogida de basura manual):

Allocated: 93847.55 kb 
Free: 77613.45 kb (~16mb used) 

Esto es de esperarse ya que el programa está utilizando una gran cantidad de recursos para leer en el archivo.

Sin embargo luego recoger la basura, pero no todos se libera la memoria:

Allocated: 93847.55 kb 
Free: 91214.78 kb (~2 mb used! That's the entire file!) 

Sé que llamar manualmente el recolector de basura no le da ninguna garantía (en algunos casos es perezoso). Sin embargo, esto estaba sucediendo en mi aplicación más grande, donde el archivo consume casi toda la memoria disponible y hace que el resto del programa se quede sin memoria a pesar de la necesidad. Este ejemplo confirma mi sospecha de que el exceso de datos leídos del archivo no se libera.

Aquí es el SSCCE para generar la prueba:

import java.io.*; 
import java.util.*; 

public class Test { 
    public static void main(String[] args) throws Throwable { 
     Runtime rt = Runtime.getRuntime(); 

     double alloc = rt.totalMemory()/1000.0; 
     double free = rt.freeMemory()/1000.0; 

     System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free); 

     Scanner in = new Scanner(new File("my_file.txt")); 
     ArrayList<String> al = new ArrayList<String>(); 

     while(in.hasNextLine()) { 
      String s = in.nextLine(); 
      al.add(s.substring(0,1)); // extracts first 1 character 
     } 

     alloc = rt.totalMemory()/1000.0; 
     free = rt.freeMemory()/1000.0; 
     System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free); 

     in.close(); 
     System.gc(); 

     alloc = rt.totalMemory()/1000.0; 
     free = rt.freeMemory()/1000.0; 
     System.out.printf("Allocated: %.2f kb\nFree: %.2f kb\n\n",alloc,free); 
    } 
} 
+3

A menos que esté haciendo algo muy inusual que probablemente nadie más esté haciendo, "jvm error" no debería ser su primera suposición. –

+1

Especialmente con respecto al gc. –

+0

¿Cómo esperas que System.gc() libere toda la memoria? Todavía estás usando las cuerdas en al, para que no puedan ser liberadas. –

Respuesta

21

Al hacer una subcadena, su subcadena mantiene una referencia a la matriz de caracteres de la cadena original (esta optimización hace que el manejo de muchas subcadena de una cadena muy rapido). Y así, a medida que mantiene sus subcadenas en la lista al, mantiene todo su archivo en la memoria. Para evitar esto, crea una nueva Cadena usando el constructor que toma una cadena como argumento.

Así que básicamente me gustaría sugerir que haces

while(in.hasNextLine()) { 
     String s = in.nextLine(); 
     al.add(new String(s.substring(0,1))); // extracts first 1 character 
    } 

El código fuente de la cadena (String) del constructor declara explícitamente que su uso es para recortar "el equipaje":

164  public String(String original) { 
    165   int size = original.count; 
    166   char[] originalValue = original.value; 
    167   char[] v; 
    168   if (originalValue.length > size) { 
    169    // The array representing the String is bigger than the new 
    170    // String itself. Perhaps this constructor is being called 
    171    // in order to trim the baggage, so make a copy of the array. 
    172    int off = original.offset; 
    173    v = Arrays.copyOfRange(originalValue, off, off+size); 
    174   } else { 
    175    // The array representing the String is the same 
    176    // size as the String, so no point in making a copy. 
    177    v = originalValue; 
    178   } 
    179   this.offset = 0; 
    180   this.count = size; 
    181   this.value = v; 

Actualización: este problema se ha ido con OpenJDK 7, Actualización 6. Las personas con una versión más reciente no tienen el problema.

+0

Hm ... Interesante. Esa es una optimización extraña que hace la subcadena. Pero explica lo que está pasando. También parece haber un informe de error al respecto: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4513622 – tskuzzy

+0

Como recuerdo, esto estaba presente en las primeras versiones de Java (1.02) y en el tiempo fue visto como una optimización inteligente. El problema es que hace que la basura sea más compleja. –

+0

Puedo ver el razonamiento detrás de esto, ya que reduce la subcadena a una operación 'O (1)'. Pero esto casi parece una pérdida de memoria para mí. – tskuzzy

6

Asegúrese de no mantener las referencias que no necesita más.

Aún tiene referencias al y in.

Pruebe agregar al = null; in = null; antes de llamar al recolector de basura.

Además, debe tener en cuenta cómo se implementa substring. substring mantiene la cadena original, y solo usa una diferencia y longitud diferentes a la misma matriz char[].

al.add(new String(s.substring(0,1))); 

No estoy seguro si hay una manera más elegante de la copia de una subcadena. Tal vez s.getChars() es más útil para ti también.

A partir de Java 8, la subcadena hace ahora copia los caracteres. Puede verificar usted mismo que el constructor llama al Arrays.copyOfRange.

+0

Necesito usar las cadenas en 'al'. Solo quiero que el resto del archivo sea basura. – tskuzzy

+0

Al menos en mi JDK, 'new String' recortará la cadena cuando las longitudes sean diferentes. –

Cuestiones relacionadas