OutOfMemoryException cuando hay RAM disponible

En ciertos casos, .NET puede arrojar una excepción OutOfMemoryException aún cuando el equipo sobre el que está corriendo la aplicación tiene mucha memoria libre, tanto física como swap. Agregar más memoria de cualquier tipo no soluciona el problema, y de hecho el problema aparece con montos mucho menores de memoria utilizados.

Causa

Esto se debe a la forma en que .NET maneja la memoria, y a algunas características de los sistemas operativos Windows (que de alguna manera recuerdan los problemas de los tiempos del “modo protegido”).

Todo parte de un hecho fundamental: cualquier sistema operativo Windows de 32 bits puede direccionar un máximo de 4Gb de memoria RAM. Esto muestra que los 32 bits no sólo determinan el tamaño de los registros de la CPU, sino que tienen incidencia sobre otros temas.

Ahora bien, de estos 4Gb, el sistema operativo toma 2Gb, dejando disponibles sólo los otros 2Gb para aplicaciones de usuario. Si a esto agregamos todos los programas, el framework .NET y otros, el espacio disponible es de alrededor de 1.2Gb.

En .NET el encargado de manejar la memoria, asignándola y liberándola, es el Garbage Collector. Éste recibe las solicitudes de uso de memoria de los programas (algo como el Malloc de C/C++), y busca un espacio de memoria libre contigua para entregar. Esto es, el pedazo de memoria asignado no puede estar compuesto de pedazos independientes y separados, sino que debe ser un sólo pedazo contiguo. Si a esto agregamos el hecho de que el manejo interno de memoria se realiza en pedazos de 64Mb en .NET, vemos que el framework tendrá problemas para asignar memoria especialmente cuando los pedazos solicitados son muy grandes.

De hecho, el error descrito en este artículo aparece normalmente cuando simplemente el GC no ha podido encontrar un pedazo de memoria contiguo suficientemente grande para lo que se pide. Por ejemplo, podemos tener un equipo con 4Gb de memoria RAM, pero si intentamos asignar un objeto de más de 800 Mb, .NET probablemente no encontrará un pedazo tan grande, lanzando la excepción. Para trozos más pequeños no habrá problema, lo que da la sensación de que existiera una contradicción vital dada la memoria disponible que según nosotros existe.

Solución

Hay varias formas de solucionar esto, aunque en general en Internet se acepta que esto es “un hecho de la vida”, es decir, es la forma en que el framework maneja la memoria, y no hay mucho que hacer al respecto. Algunas soluciones son:

La primera y más básica, instalar .NET 1.1 Service Pack 1. Efectivamente la versión original del framework en esta versión trae un error en el manejo de la memoria, que hace que aparezca mucho más seguido aún cuando se tiene mucha más memoria libre.
En el archivo BOOT.INI es posible especificar que Windows debe partir en modo de “3GB”, es decir, que se quede con 1GB y entregue los restantes tres para el usuario. Esto se realiza agregando el modificador /3GB. Si bien esto no asegura que vayamos a tener espacios contiguos más grandes, la probabilidad de que esto ocurra aumenta algo. Un detalle importante: si la aplicación es C++, se debe agregar la opción LARGEADRESSAWARE al linker (cosa que no se puede hacer en C#, se hace automáticamente).
Se debe revisar el código y evitar utilizar objetos tan grandes. Mala respuesta, pero es probablemente la forma más directa y correcta de solucionar el problema.