2009-11-13 8 views
28

En mi trabajo tuvimos un problema con OutOfMemoryExceptions. Escribí un código simple para imitar algún comportamiento, y terminé con el siguiente misterio. Mira este código simple que explota cuando se queda sin memoria..NET Garbage Collector mystery

class Program 
{ 
    private static void Main() 
    { 
     List<byte[]> list = new List<byte[]>(200000); 
     int iter = 0; 

     try 
     { 
      for (;;iter++) 
      { 
       list.Add(new byte[10000]); 
      } 
     } 
     catch (OutOfMemoryException) 
     { 
      Console.WriteLine("Iterations: " + iter); 
     } 
    } 
} 

En mi máquina terminó con

Iterations: 148008

Luego he añadido una llamada GC.Collect al bucle después de cada mil iteraciones:

  //... 
      for (;;iter++) 
      { 
       list.Add(new byte[10000]); 

       if (iter % 1000 == 0) 
        GC.Collect(); 
      } 
      //... 

y sorpresa:

Iterations: 172048

Cuando llamé a GC.Collect después de cada 10 iteraciones, incluso obtuve 193716 ciclos. Hay dos cosas extrañas:

  1. ¿Cómo puede una llamada manual para GC.Collect tener un impacto tan grave (hasta un 30% más asignó)?

  2. ¿Qué demonios puede recolectar GC, cuando no hay referencias "perdidas" (incluso preestablecí la capacidad de la Lista)?

+6

Pregunta interesante. Supongo que tiene que ver con la desfragmentación del espacio de memoria, y si llama a GC. Recolecte lo suficiente para mantener el bloque contiguo más grande, de modo que la excepción se produzca más tarde. Pero eso es solo una suposición descabellada y estoy esperando a ver lo que otros tienen que decir. – Lucero

+2

No puedo responder la pregunta, pero en 2) no puede indicar que no hay referencias perdidas porque no puede ver el método "Agregar". – flq

+0

@Frank: Es un punto justo, pero he intentado lo mismo con una matriz simple y el resultado fue el mismo. – Elephantik

Respuesta

11

Una parte del proceso de recolección de basura es la fase de compactación. Durante esta fase, los bloques de memoria asignada se mueven para reducir la fragmentación. Cuando se asigna memoria, no siempre se asigna justo después de que se haya apagado el último fragmento de memoria asignada. Entonces puede exprimir un poco más porque el recolector de basura está haciendo más espacio al hacer un mejor uso del espacio disponible.

Estoy tratando de ejecutar algunas pruebas, pero mi máquina no puede manejarlas. Probar esto, se le dirá al GC de precisar los objetos en la memoria para que no se mueven en torno

byte[] b = new byte[10000]; 
GCHandle.Alloc(b, GCHandleType.Pinned); 
list.Add(b); 

En cuanto a su comentario, cuando las cosas GC mueve alrededor, no es borrar nada , solo está haciendo un mejor uso de todo el espacio de la memoria. Intentemos y simplifiquemos esto. Cuando asigna su matriz de bytes la primera vez, digamos que se inserta en la memoria desde el punto 0 a 10000. La próxima vez que asigne la matriz de bytes, no se garantiza que comience en 10001, puede comenzar en 10500. Así que ahora tiene 499 bytes que no están siendo utilizados y su aplicación no los utilizará. Entonces, cuando el GC se compacta, moverá la matriz 10500 a 10001 para poder usar esos 499 bytes adicionales. Y de nuevo, esto es demasiado simplificado.

+1

Eso tendría sentido, pero 1) Todavía no puedo ver ningún objeto borrado (bueno, List.Agregar puede agregar algo de ruido, pero una comprobación rápida con reafilado muestra que no es así; 2) cuando se asigna tanta memoria, GC debe ser invocado muchas veces por el framework y debería hacer lo mismo. – Elephantik

+0

Eso también es lo que pensé (vea mi comentario sobre la pregunta). Sin embargo, lo que realmente no tiene sentido es que el GC debe invocarse en el caso de memoria insuficiente, compactando así la memoria en ese momento. Sin embargo, esto puede ser diferente ya que el GC de alguna manera también asigna memoria en bloques del sistema operativo, por lo tanto, no tiene un bloque de memoria grande, pero clasifica una serie de bloques de memoria para tratar. Llamar a GC.COllect puede reorganizar los bloques más, de modo que hay menos espacio perdido (memoria no utilizada a pequeña para ser utilizada para esta asignación al final de un bloque del sistema operativo). – Lucero

+2

Veo su punto, pero mientras no haya objetos muertos, no veo una razón para esa fragmentación intencional de la memoria. Uno de los beneficios de GC debería ser beneficiarse de la memoria libre no fragmentada, por lo que los objetos recién creados no tienen que buscar espacios libres. Y, como se mencionó anteriormente, la llamada automática a GC debería hacer lo mismo. – Elephantik

5

Dependiendo del CLR que esté utilizando, puede haber algunos problemas de montón de objetos grandes involucrados.

Eche un vistazo a este artículo, que explica los problemas con las asignaciones de bloques grandes (y la lista con 200000 elementos es un bloque grande seguro, el otro puede o no ser, algunas matrices parecen estar en LOH cuando alcanzan 8k, otros después de 85k).

http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/

+0

Un buen punto, LOH podría estar involucrado, pero la gran lista sigue estando ahí todo el tiempo, por lo que LOH no debería estar fragmentado. – Elephantik

+0

Usted prueba el punto de Lucero al reducir la matriz pequeña. Aunque solo sé de ese límite de 85000. –

+0

Hice una prueba para insertar en una matriz de matrices más pequeñas para evitar LOH, y el comportamiento sigue siendo el mismo. – Elephantik

2

El CLR vez en cuando coloca matrices en la LOH. Si alguna vez observas un volcado de memoria a través de WinDbg, verás que hay arreglos que tienen menos de 85,000 bytes.Es un comportamiento no documentado, pero así es como funciona.

Obtiene los OutOfMemoryErrors porque está fragmentando el LOH Heap y LOH Heap nunca se compacta.

Con respecto a su pregunta de:

2) ¿Qué demonios puede recoger GC, cuando hay ninguna referencia "perdidos" (incluso he presintonizar la capacidad de la lista)?

Hay referencias sobreescritas al new byte[10000] que pasa para agregarlas a la lista. Se compila y asigna una variable local al new byte[10000]. Para cada iteración en el ciclo crea un nuevo byte [] con un tamaño predefinido de 10000 y se asigna a la variable local. Cualquier valor previo para la variable se sobrescribe y esa memoria es elegible para la recolección la próxima vez que se ejecute el GC para la generación en la que vive la variable (en este caso, posiblemente el LOH).

+0

"Se hace una copia de la matriz y se pasa a la Lista" es simplemente incorrecto. –

+0

¿Cómo es esto "simplemente incorrecto"? Se crea una nueva copia de la matriz en cada iteración (lo que quise decir con copia, ya que es el mismo tamaño de matriz). Luego esa nueva matriz se agrega a una lista existente aumentando el número de bytes [10000] en uno por cada iteración. –

+0

Muy bien, ahora entiendo que querías decir "se hace una nueva matriz". Para mí, una copia de una matriz se entiende como una copia de su contenido, y eso no sucede aquí. –

0

Tuve un problema similar en .NET con la diferencia de que mi byte [] tenía tamaños aleatorios.

probé dos maneras:

  • escribir un administrador del montón propia (memoria alloc con un buffer de gran tamaño y simplemente ajustar punteros)

  • utilizar un archivo mapeado en memoria (en mi opinión la mejor solución)

Si es posible se puede probar .NET 4.5 http://blogs.msdn.com/b/dotnet/archive/2012/07/20/the-net-framework-4-5-includes-new-garbage-collector-enhancements-for-client-and-server-apps.aspx

+0

¿Qué hará el entorno x64? Sí, la cantidad de memoria virtual aumenta, pero lo único que hará es prolongar el tiempo que transcurre antes de que ocurra una OutOfMemoryException. la causa raíz del problema aún no está resuelta. El tamaño del LOH no cambia de x86 a x64 y el LOH seguirá estando fragmentado ya que el GC no compacta el LOH. –

+0

Corregí mi respuesta. Tienes razón. – NickD

Cuestiones relacionadas