¿Cuándo está bien capturar una OutOfMemoryException y cómo manejarla?

Ayer participé en un debate sobre SO dedicado a OutOfMemoryException y los pros y los contras de manejarlo ( C # try {} catch {} ).

Mis pros para manejarlo fueron:

  • El hecho de que OutOfMemoryException se lanzó no significa, en general, que el estado de un progtwig esté dañado;
  • De acuerdo con la documentación “las siguientes instrucciones intermedias de Microsoft (MSIL) arrojan OutOfMemoryException: box, newarr, newobj” que simplemente (normalmente) significa que el CLR intentó encontrar un bloque de memoria de un tamaño determinado y no pudo hacerlo; no significa que no haya ningún byte a nuestra disposición;

Pero no todas las personas estuvieron de acuerdo con eso y especularon sobre el estado del progtwig desconocido después de esta excepción y la incapacidad de hacer algo útil, ya que requerirá aún más memoria.

Por lo tanto, mi pregunta es: ¿cuáles son las razones serias para no manejar OutOfMemoryException e inmediatamente darse por vencido cuando ocurre?

Editado: ¿Crees que OOME es tan fatal como ExecutionEngineException?

Todos escribimos diferentes aplicaciones. En una aplicación WinForms o ASP.Net, probablemente solo registre la excepción, notifique al usuario, intente guardar el estado y apagar / reiniciar. Pero como Igor mencionó en los comentarios, esto podría ser construyendo algún tipo de aplicación de edición de imágenes y el proceso de cargar la 100ma imagen RAW de 20MB podría empujar la aplicación al límite. ¿De verdad quieres que el uso pierda todo su trabajo de algo tan simple como decirlo? “Lo siento, no puedo cargar más imágenes en este momento”.

Otra instancia común que podría ser útil para atrapar excepciones de memoria es en el procesamiento por lotes de fondo. Puede tener un modelo estándar para cargar archivos de varios mega bytes en la memoria para su procesamiento, pero luego, un día inesperado, se carga un archivo de varios giga bytes. Cuando se produce la falta de memoria, puede registrar el mensaje en una cola de notificación de usuario y luego pasar al siguiente archivo.

Sí, es posible que algo más pueda explotar al mismo tiempo, pero también se registrarán y notificarán si es posible. Si, finalmente, el GC no puede procesar más memoria, la aplicación va a funcionar de todos modos. (El GC funciona en un hilo desprotegido).

No olvides que todos desarrollamos diferentes tipos de aplicaciones. Y a menos que esté en máquinas antiguas y limitadas, probablemente nunca obtenga una excepción OutOfMemoryException para aplicaciones empresariales típicas … pero tampoco todos somos desarrolladores de herramientas comerciales.

Para su edición …

La falta de memoria puede ser causada por fragmentación de memoria no administrada y fijación. También puede ser causado por grandes solicitudes de asignación. Si tuviéramos que poner una bandera blanca y trazar una línea en la arena por cuestiones tan simples, nunca se haría nada en los grandes proyectos de procesamiento de datos. Ahora, comparando eso con una excepción fatal del motor, no hay nada que puedas hacer en el punto en que el tiempo de ejecución caiga muerto bajo tu código. Es de esperar que pueda iniciar sesión (pero probablemente no) por qué su código cayó en su cara por lo que puede evitarlo en el futuro. Pero, lo que es más importante, es de esperar que su código esté escrito de una manera que permita la recuperación segura de tantos datos como sea posible. Tal vez incluso recuperar el último buen estado conocido en su aplicación y posiblemente omitir los datos corruptos ofensivos y permitir que se procese y recupere manualmente.

Sin embargo, al mismo tiempo, es posible que los datos se dañen a causa de la inyección de SQL, las versiones de software no sincronizadas, la manipulación de punteros, las ejecuciones por exceso de memoria y muchos otros problemas. Evitar un problema simplemente porque piensa que no se puede recuperar de él es una gran manera de dar a los usuarios mensajes de error tan constructivos como Póngase en contacto con su administrador del sistema .

IMO, ya que no puede predecir lo que puede / no puede hacer después de un OOM (por lo que no puede procesar el error de manera confiable), o qué más sucedió / no ocurrió al desenrollar la stack hasta donde se encuentra (por lo el BCL no ha procesado el error de manera confiable), ahora se debe suponer que su aplicación está en un estado corrupto. Si “arreglas” tu código manejando esta excepción, estás enterrando tu cabeza en la arena.

Podría estar equivocado aquí, pero para mí este mensaje dice GRAN PROBLEMÁTICA. La solución correcta es averiguar por qué ha masticado la memoria y abordarla (por ejemplo, ¿tiene una fuga ?, ¿podría cambiar a una API de transmisión?). Incluso cambiar a x64 no es una bala mágica aquí; las matrices (y, por lo tanto, las listas) aún tienen un tamaño limitado; y el aumento del tamaño de referencia significa que puede corregir numéricamente menos referencias en el límite de objetos de 2 GB.

Si necesita procesar algunos datos y está contento de que fallen, inicie un segundo proceso (un AppDomain no es lo suficientemente bueno). Si explota, derriba el proceso. Problema resuelto, y su proceso original / AppDomain es seguro.

Algunos comentaristas han notado que hay situaciones en las que OOM podría ser el resultado inmediato de intentar asignar una gran cantidad de bytes (aplicación de gráficos, asignación de una matriz grande, etc.). Tenga en cuenta que para ese propósito podría usar la clase MemoryFailPoint , que genera una InsufficientMemoryException (en sí misma derivada de OutOfMemoryException). Eso se puede capturar de forma segura, ya que se produce antes de que se haya realizado el bash real de asignar la memoria. Sin embargo, esto solo puede reducir realmente la probabilidad de una OOM, nunca prevenirla por completo.

Todo depende de la situación.

Hace unos años, ahora estaba trabajando en un motor de renderizado en 3D en tiempo real. En ese momento, cargamos toda la geometría del modelo en la memoria al iniciar, pero solo cargamos las imágenes de textura cuando necesitábamos mostrarlas. Esto significaba que cuando llegó el día nuestros clientes estaban cargando modelos enormes (2GB) que pudimos soportar. La geometría ocupaba menos de 2 GB, pero cuando todas las texturas se agregaban, era> 2 GB. Al atrapar el error de falta de memoria que surgió cuando tratábamos de cargar la textura, pudimos seguir mostrando el modelo, pero igual que la geometría simple.

Todavía teníamos un problema si la geometría era> 2GB, pero esa era una historia diferente.

Obviamente, si obtienes un error de falta de memoria con algo fundamental para tu aplicación, entonces no tienes más remedio que desconectarte, pero hazlo tan elegantemente como puedas.

Sugerir el comentario de Christopher Brumme en “Framework Design Guideline” p.238 (7.3.7 OutOfMemoryException):

En un extremo del espectro, una OutOfMemoryException podría ser el resultado de una falla al obtener 12 bytes para el autoboxing implícitamente, o una falla al JIT de algún código que se requiere para retroceso crítico. Estos casos son fallas catastróficas e idealmente darían como resultado la finalización del proceso. En el otro extremo del espectro, una OutOfMemoryException podría ser el resultado de un hilo que solicita una matriz de bytes de 1 GB. El hecho de que hayamos fallado en este bash de asignación no tiene ningún impacto en la consistencia y la viabilidad del rest del proceso.

La triste realidad es que CRL 2.0 no puede distinguir entre ningún punto en este espectro. En la mayoría de los procesos administrados, todas las OutOfMemoryExceptions se consideran equivalentes y todas resultan en una excepción administrada que se propaga por el hilo. Sin embargo, no puede depender de que se ejecute su código de restitución, porque es posible que no podamos JET con algunos de sus métodos de restitución, o podríamos no ejecutar los constructores estáticos requeridos para la restitución.

Además, tenga en cuenta que todas las demás excepciones pueden doblarse en una excepción OutOfMemoryException si no hay memoria suficiente para crear una instancia de esos otros objetos de excepción. Además, le proporcionaremos una OutOfMemoryException única con su propio seguimiento de stack si podemos. Pero si estamos lo suficientemente ajustados a la memoria, compartiremos una instancia global poco interesante con todos los demás en el proceso.

Mi mejor recomendación es que trates OutOfMemoryException como cualquier otra excepción de aplicación. Haces tus mejores bashs para manejarlo y es constante. En el futuro, espero que CLR pueda hacer un mejor trabajo al distinguir el OOM catastrófico del caso de matriz de byte de 1 GB. Si es así, podríamos provocar la finalización del proceso para los casos catastróficos, dejando la aplicación para tratar con los menos riesgosos. Al amenazar todos los casos de OOM como los menos riesgosos, se está preparando para ese día.

Marc Gravell ya ha proporcionado una excelente respuesta; ya que en parte “inspiré” esta pregunta, me gustaría agregar una cosa:

Uno de los principios básicos del manejo de excepciones es nunca arrojar una excepción dentro de un manejador de excepciones. (Nota: volver a lanzar una excepción específica del dominio o envuelta está bien, estoy hablando de una excepción inesperada aquí).

Hay todo tipo de razones por las que necesita evitar que esto suceda:

  • En el mejor de los casos, enmascara la excepción original; se vuelve imposible saber con certeza dónde falló originalmente el progtwig.

  • En algunos casos, el tiempo de ejecución simplemente no puede manejar una excepción no controlada en un manejador de excepciones (digamos 5 veces rápido). En ASP.NET, por ejemplo, la instalación de un controlador de excepción en ciertas etapas de la canalización y la falla en ese controlador simplemente matará la solicitud, o se bloqueará el proceso de trabajo, no recuerdo cuál.

  • En otros casos, puede abrirse a la posibilidad de un bucle infinito en el manejador de excepciones. Esto puede parecer una tontería, pero he visto casos en los que alguien trata de manejar una excepción registrándola, y cuando el registro falla … intentan registrar el error. La mayoría de nosotros probablemente no escribiría un código como este deliberadamente , pero dependiendo de cómo organice el manejo de excepciones de su progtwig, puede terminar haciéndolo por accidente.

Entonces, ¿qué tiene esto que ver con OutOfMemoryException específicamente?

Una OutOfMemoryException no le dice nada sobre por qué falló la asignación de memoria. Puede suponer que fue porque trataste de asignar un gran buffer, pero tal vez no fue así. Tal vez algún otro proceso malicioso en el sistema haya consumido literalmente todo el espacio de direcciones disponible y no le quede un solo byte. Tal vez algún otro hilo en su propio progtwig falló y entró en un bucle infinito, asignando nueva memoria en cada iteración, y ese hilo ha fallado desde hace mucho tiempo cuando la OutOfMemoryException termina en su marco de stack actual. El punto es que en realidad no sabes cuán mala es la situación de la memoria, incluso si piensas que sí .

Así que empieza a pensar en esta situación ahora. Algunas operaciones simplemente fallaron en un punto no especificado en las entrañas del framework .NET y propagaron una OutOfMemoryException . ¿Qué trabajo significativo puede realizar en su controlador de excepciones que no involucra la asignación de más memoria? Escribir en un archivo de registro? Eso requiere memoria. Mostrar un mensaje de error? Eso requiere aún más memoria. ¿Enviar un correo electrónico de alerta? Ni siquiera lo pienses.

Si intenta hacer estas cosas, y falla, entonces terminará con un comportamiento no determinista. Es posible que enmascare el error de falta de memoria y obtenga misteriosos informes de errores con misteriosos mensajes de error que brotan de todo tipo de componentes de bajo nivel que escribió y que se supone que no pueden fallar. Fundamentalmente, has violado las invariantes de tu propio progtwig, y ​​esto será una pesadilla para depurar si tu progtwig alguna vez termina ejecutándose en condiciones de poca memoria.

Uno de los argumentos que se me presentaron antes es que podría atrapar una OutOfMemoryException y luego cambiar a un código de memoria inferior, como un búfer más pequeño o un modelo de transmisión. Sin embargo, este ” Manejo de Expectativas ” es un antipatrón conocido. Si sabe que está a punto de masticar una gran cantidad de memoria y no está seguro de si el sistema puede manejarlo, entonces verifique la memoria disponible , o mejor aún, simplemente refactorice su código para que no lo necesite tanta memoria a la vez. No confíe en OutOfMemoryException para hacerlo por usted, porque – quién sabe – tal vez la asignación apenas tendrá éxito y desencadenará una serie de errores de OutOfMemoryException de memoria inmediatamente después de su controlador de excepción (posiblemente en algún componente completamente diferente).

Entonces mi respuesta simple a esta pregunta es: Nunca.

Mi respuesta de comadreja a esta pregunta es: Está bien en un controlador global de excepciones, si realmente tienes mucho cuidado. No en un bloque de try-catch.

Una razón práctica para detectar esta excepción es intentar un cierre elegante, con un mensaje de error amistoso en lugar de un rastro de excepción.

El problema es que, a diferencia de otras excepciones, generalmente tiene una situación de poca memoria cuando se produce la excepción (excepto cuando la memoria asignada era enorme, pero no se sabe realmente cuándo se detecta la excepción).

Por lo tanto, debe tener mucho cuidado de no asignar memoria al manejar esta excepción. Y aunque suena fácil, no es así, en realidad es muy difícil evitar cualquier asignación de memoria y hacer algo útil. Por lo tanto, atraparlo generalmente no es una buena idea en mi humilde opinión.

El problema es más grande que .NET. Casi cualquier aplicación escrita desde los años cincuenta hasta ahora tiene grandes problemas si no hay memoria disponible.

Con los espacios de direcciones virtuales, el problema ha sido rescatado, pero NO resuelto, ya que incluso espacios de direcciones de 2 GB o 4 GB pueden volverse demasiado pequeños. No hay patrones comúnmente disponibles para manejar la falta de memoria. Podría haber un método de advertencia de falta de memoria, un método de pánico, etc. que garantice que todavía tenga memoria disponible.

Si recibe una excepción OutOfMemoryException de .NET, casi cualquier cosa puede ser el caso. 2 MB todavía están disponibles, solo 100 bytes, lo que sea. No me gustaría ver esta excepción (excepto para cerrar sin un diálogo de falla). Necesitamos mejores conceptos. Entonces puede obtener una MemoryLowException donde PUEDE reactjsr ante todo tipo de situaciones.

Escribir código, no secuestrar la JVM. Cuando VM le dice humildemente que una solicitud de asignación de memoria falló, su mejor opción es descartar el estado de la aplicación para evitar la corrupción de los datos de la aplicación. Incluso si decide capturar OOM, solo debería intentar recostackr información de diagnóstico, como el registro de volcado, stacktrace, etc. No intente iniciar un procedimiento de retroceso ya que no está seguro de si tendrá la posibilidad de ejecutarse o no.

La analogía del mundo real: estás viajando en un avión y todos los motores fallan. ¿Qué harías después de atrapar una AllEngineFailureException? La mejor apuesta es agarrar la máscara y prepararse para un choque.

Cuando en OOM, volcado !!