language-agnostic exception memory memory-management error-recovery

language agnostic - ¿Es "Sin memoria" un error recuperable?



language-agnostic exception (23)

He estado programando durante mucho tiempo, y los programas que veo, cuando se quedan sin memoria, intentan limpiar y salir, es decir, fallan con elegancia. No recuerdo la última vez que vi que uno realmente intentaba recuperarse y continuar operando normalmente.

Tanto procesamiento depende de poder asignar con éxito la memoria, especialmente en los lenguajes recogidos basura, parece que los errores de memoria se deben clasificar como no recuperables. (Los errores no recuperables incluyen cosas como desbordamientos de pila).

¿Cuál es el argumento convincente para que sea un error recuperable?


¿Cuál es el argumento convincente para que sea un error recuperable?

En Java, un argumento convincente para no convertirlo en un error recuperable es que Java permite que OOM sea señalizado en cualquier momento, incluso en los casos en que el resultado podría ser que su programa ingrese en un estado incoherente. La recuperación confiable de un OOM es por lo tanto imposible; Si detecta la excepción OOM, no puede confiar en el estado de su programa. Ver garantías de No-throw VirtualMachineError


Creo que, como muchas otras cosas, es un análisis de costo / beneficio. Puede programar un intento de recuperación de una falla de malloc (), aunque puede ser difícil (es mejor que su controlador no caiga en la misma falta de memoria con la que debe lidiar).

Ya ha notado que el caso más común es limpiar y fallar con gracia. En ese caso, se ha decidido que el costo de abortar correctamente es menor que la combinación del costo de desarrollo y el costo de rendimiento en la recuperación.

Estoy seguro de que puedes pensar en tus propios ejemplos de situaciones en las que terminar el programa es una opción muy costosa (máquina de soporte de vida, control de la nave espacial, cálculo financiero de larga duración y tiempo crítico, etc.), aunque la primera línea de defensa es por supuesto, para garantizar que el programa tenga un uso de memoria predecible y que el entorno pueda proporcionar eso.


Depende de lo que quiere decir con la falta de memoria.

Cuando malloc() falla en la mayoría de los sistemas, es porque se ha agotado el espacio de direcciones.

Si la mayoría de esa memoria está ocupada por la caché, o por regiones mmap''d, es posible que pueda reclamar parte de ella liberando su caché o anulando. Sin embargo, esto realmente requiere que sepas para qué estás usando esa memoria y, como habrás notado, la mayoría de los programas no lo hacen o no hace la diferencia.

Si utilizaste setrlimit() en ti mismo (para protegerte contra ataques imprevistos, tal vez, o tal vez root te lo haya hecho), puedes relajar el límite en tu controlador de errores. Hago esto con mucha frecuencia, después de avisar al usuario si es posible y registrar el evento.

Por otro lado, atrapar el desbordamiento de pila es un poco más difícil y no es portátil. Escribí una solución posixish para ECL y describí una implementación de Windows, si vas por esta ruta. Fue revisado en ECL hace unos meses, pero puedo desenterrar los parches originales si está interesado.


En el caso general, no es recuperable.

Sin embargo, si su sistema incluye alguna forma de almacenamiento en caché dinámico, un manejador de memoria insuficiente a menudo puede volcar los elementos más antiguos en la memoria caché (o incluso la memoria caché completa).

Por supuesto, debe asegurarse de que el proceso de "volcado" no requiera nuevas asignaciones de memoria :) Además, puede ser complicado recuperar la asignación específica que falló, a menos que pueda conectar el código de volcado de la memoria caché directamente en el asignador nivel, para que la falla no se propague a la persona que llama.


En una biblioteca, quiere copiar de manera eficiente un archivo. Cuando haces eso, generalmente encontrarás que copiar usando una pequeña cantidad de trozos grandes es mucho más efectivo que copiar muchos más pequeños (por ejemplo, es más rápido copiar un archivo de 15MB copiando 15 trozos de 1MB que copiar 15,000 Trozos de 1K).

Pero el código funciona con cualquier tamaño de fragmento. Entonces, si bien puede ser más rápido con fragmentos de 1MB, si diseña para un sistema donde se copian muchos archivos, puede ser conveniente capturar OutOfMemoryError y reducir el tamaño del fragmento hasta que tenga éxito.

Otro lugar es un caché para Object almacenado en una base de datos. Desea mantener tantos objetos en la memoria caché como sea posible pero no quiere interferir con el resto de la aplicación. Dado que estos objetos se pueden volver a crear, es una manera inteligente de conservar la memoria para adjuntar el caché a un manejador sin memoria para eliminar las entradas hasta que el resto de la aplicación tenga suficiente espacio para respirar, nuevamente.

Por último, para la manipulación de imágenes, desea cargar la mayor cantidad posible de la imagen en la memoria. Una vez más, un controlador OOM le permite implementar eso sin saber de antemano cuánta memoria el usuario o sistema operativo le otorgará a su código.

[EDITAR] Tenga en cuenta que aquí trabajo bajo la suposición de que le ha dado a la aplicación una cantidad fija de memoria y esta cantidad es menor que la memoria total disponible, excluyendo el espacio de intercambio. Si puede asignar tanta memoria que parte de ella tiene que ser intercambiada, muchos de mis comentarios ya no tienen sentido.


Es recuperable solo si lo atrapa y lo maneja correctamente.

En los mismos casos, por ejemplo, una solicitud intentó asignar una gran cantidad de memoria. Es bastante predecible y puedes manejarlo muy bien.

Sin embargo, en muchos casos en aplicaciones de subprocesos múltiples, OOE también puede ocurrir en el hilo de fondo (incluso creado por el sistema / biblioteca de terceros). Es casi imposible de predecir y es posible que no pueda recuperar el estado de todos sus hilos.


Especialmente en entornos recolectados de basura, es probable que si detecta el error de OutOfMemory en un nivel alto de la aplicación, muchas cosas se salieron de alcance y pueden recuperarse para devolverle la memoria.

En el caso de asignaciones excesivas únicas, la aplicación puede continuar funcionando sin problemas. Por supuesto, si tiene una fuga gradual de memoria, se encontrará nuevamente con el problema (lo más probable es que tarde o temprano), pero aún así es una buena idea dar a la aplicación la oportunidad de bajar con elegancia, guardar los cambios no guardados en el caso de una aplicación GUI, etc.


Esta es una pregunta dificil. A primera vista, parece que no tener más memoria significa "falta de suerte", pero también debes ver que uno puede deshacerse de muchas cosas relacionadas con la memoria si uno realmente insiste. Simplemente tomemos la función de otra manera, strtok, que por un lado no tiene problemas con la memoria. A continuación, tome como contrapartida g_string_split de la biblioteca Glib, que depende en gran medida de la asignación de memoria como casi todo en programas basados ​​en glib o GObject. Uno puede decir definitivamente en lenguajes más dinámicos que la asignación de memoria es mucho más utilizada que en lenguajes más inflexibles, especialmente C. Pero veamos las alternativas. Si acaba el programa si se queda sin memoria, incluso el código desarrollado cuidadosamente puede dejar de funcionar. Pero si tiene un error recuperable, puede hacer algo al respecto. Entonces el argumento, haciéndolo recuperable significa que uno puede elegir "manejar" esa situación de manera diferente (por ejemplo, dejando de lado un bloque de memoria para emergencias, o degradación a un programa menos extensivo en memoria).

Entonces la razón más convincente es. Si proporciona una forma de recuperación, puede intentar la recuperación, si no tiene la opción, todo depende de que siempre obtenga suficiente memoria ...

Saludos


Estoy trabajando en un sistema que asigna memoria para caché IO ​​para aumentar el rendimiento. Luego, al detectar OOM, se lleva algo de vuelta, de modo que la lógica de negocios pueda continuar, incluso si eso significa menos caché de IO y un rendimiento de escritura ligeramente menor.

También trabajé con aplicaciones embebidas de Java que intentaban administrar OOM al forzar la recolección de basura, liberando opcionalmente algunos objetos no críticos, como datos precapturados o en caché.

Los principales problemas con el manejo de OOM son:

1) poder volver a intentar en el lugar donde sucedió o ser capaz de retroceder y volver a intentar desde un punto más alto. La mayoría de los programas contemporáneos confían demasiado en el lenguaje para lanzar y no logran realmente dónde terminan y cómo volver a intentar la operación. Usualmente el contexto de la operación se perderá, si no fue diseñado para ser preservado

2) ser capaz de liberar algo de memoria. Esto significa un tipo de administrador de recursos que sabe qué objetos son críticos y cuáles no, y el sistema puede volver a solicitar los objetos liberados cuando se vuelvan críticos y más adelante.

Otro tema importante es poder retroceder sin desencadenar otra situación OOM. Esto es algo que es difícil de controlar en idiomas de nivel superior.

Además, el sistema operativo subyacente debe comportarse de manera predecible con respecto a OOM. Linux, por ejemplo, no lo hará, si la sobrecomisión de la memoria está habilitada. Muchos sistemas habilitados para el intercambio morirán antes que reportar el OOM a la aplicación ofensiva.

Y, está el caso cuando no es su proceso el que creó la situación, por lo que liberar memoria no ayuda si el proceso ofensivo sigue teniendo fugas.

Debido a todo esto, a menudo son los sistemas grandes e integrados los que emplean estas técnicas, ya que tienen el control sobre el sistema operativo y la memoria para habilitarlos, y la disciplina / motivación para implementarlos.


La falta de memoria normalmente significa que debes abandonar lo que sea que estuvieras haciendo. Sin embargo, si tiene cuidado con la limpieza, puede dejar el programa en funcionamiento y responder a otras solicitudes. Es mejor tener un programa que diga "Lo siento, no hay suficiente memoria para hacer" que decir "Lo siento, sin memoria, apago".


La falta de memoria puede deberse al agotamiento de la memoria libre o al intentar asignar un bloque irracionalmente grande (como un concierto). En los casos de "agotamiento", la falta de memoria es global para el sistema y generalmente afecta a otras aplicaciones y servicios del sistema, y ​​todo el sistema puede volverse inestable, por lo que es aconsejable olvidarse y reiniciarse. En los casos de "bloque irrazonablemente grande", no hay escasez y es seguro continuar. El problema es que no puede detectar automáticamente en qué caso se encuentra. Por lo tanto, es más seguro hacer que el error no sea recuperable y encontrar una solución alternativa para cada caso que encuentre este error: haga que su programa use menos memoria o en algunos casos solo corrija errores en el código que invoca la asignación de memoria.


La pregunta está etiquetada como "agnóstica del lenguaje", pero es difícil de responder sin considerar el idioma y / o el sistema subyacente. (Veo varias aventuras de toher

Si la asignación de memoria es implícita, sin ningún mecanismo para detectar si una asignación determinada fue exitosa o no, entonces la recuperación de una condición de falta de memoria puede ser difícil o imposible.

Por ejemplo, si llama a una función que intenta asignar una matriz enorme, la mayoría de los lenguajes simplemente no definen el comportamiento si la matriz no se puede asignar. (En Ada esto genera una excepción Storage_Error , al menos en principio, y debería ser posible manejar eso).

Por otro lado, si tiene un mecanismo que intenta asignar memoria y es capaz de informar una falla al hacerlo (como C malloc() o C ++ ''s new ), entonces sí, es ciertamente posible recuperarse de ese error. En al menos los casos de malloc() y new , una asignación fallida no hace otra cosa que informar el error (no corrompe ninguna estructura de datos interna, por ejemplo).

Si tiene sentido intentar recuperar depende de la aplicación. Si la aplicación simplemente no puede tener éxito después de una falla de asignación, entonces debe hacer cualquier limpieza que pueda y finalizar. Pero si la falla de asignación simplemente significa que una tarea en particular no se puede realizar, o si la tarea aún se puede realizar más lentamente con menos memoria, entonces tiene sentido continuar con la operación.

Un ejemplo concreto: supongamos que estoy usando un editor de texto. Si trato de realizar alguna operación dentro del editor que requiera mucha memoria y esa operación no se pueda realizar, quiero que el editor me diga que no puede hacer lo que le pedí y que me permita seguir editando . Terminar sin guardar mi trabajo sería una respuesta inaceptable. Guardar mi trabajo y terminar sería mejor, pero aún así es innecesariamente hostil para el usuario.


Los usuarios de MATLAB se quedan sin memoria todo el tiempo cuando realizan operaciones aritméticas con arreglos grandes. Por ejemplo, si la variable x cabe en la memoria y ejecutan "x + 1", MATLAB asigna espacio para el resultado y luego lo llena. Si la asignación falla los errores de MATLAB y el usuario puede intentar algo diferente. Sería un desastre si MATLAB saliera cada vez que surgiera este caso de uso.


Me está desconcertando ahora.

En el trabajo, tenemos un conjunto de aplicaciones trabajando juntas, y la memoria se está agotando. Si bien el problema es hacer que el paquete de aplicaciones sea de 64 bits (y así poder trabajar más allá de los 2 límites de Go que tenemos en un sistema operativo Win32 normal) y / o reducir el uso de memoria, este problema de "Cómo recuperarse de un OOM "no me abandonará la cabeza.

Por supuesto, no tengo solución, pero aún juego buscando uno para C ++ (debido a RAII y excepciones, principalmente).

Tal vez un proceso que supuestamente se recupere con elegancia debería descomponer su procesamiento en tareas atómicas / reversibles (es decir, utilizando únicamente funciones / métodos que otorguen una garantía de excepción fuerte / no urgente), con un "búfer / reserva de memoria" reservado para fines de recuperación.

Si una de las tareas falla, C ++ bad_alloc desenrollaría la pila, liberaría alguna pila / memoria de montón a través de RAII. La función de recuperación se recuperaría tanto como fuera posible (guardando los datos iniciales de la tarea en el disco, para usarla en un intento posterior), y tal vez registrara los datos de la tarea para una prueba posterior.

Creo que el uso de C ++ strong / nothrow guanrantees puede ayudar a que un proceso sobreviva en condiciones de baja disponibilidad de memoria, incluso si fuera similar al intercambio de memoria (es decir, lento, algo no correspondido, etc.), pero por supuesto, esto es solo teoría Solo necesito ser más inteligente sobre el tema antes de intentar simular esto (es decir, crear un programa C ++, con un asignador nuevo / eliminar personalizado con memoria limitada, y luego intentar hacer algo de trabajo bajo esas condiciones estresantes).

Bien...


No. Un error de falta de memoria del GC generalmente no debería ser recuperable dentro del hilo actual. (La creación y terminación de hilo recuperable (usuario o kernel) deberían ser soportadas)

Respecto a los ejemplos de contador: actualmente estoy trabajando en un proyecto de lenguaje de programación D que utiliza la plataforma CUDA de NVIDIA para computación GPU. En lugar de administrar manualmente la memoria GPU, he creado objetos proxy para aprovechar el GC de D''s. Entonces, cuando la GPU devuelve un error de falta de memoria, ejecuto una recopilación completa y solo genero una excepción si falla por segunda vez. Pero, esto no es realmente un ejemplo de recuperación de memoria insuficiente, es más una integración de GC. Los otros ejemplos de recuperación (cachés, listas libres, apilamientos / hashes sin contracción automática, etc.) son todas estructuras que tienen sus propios métodos de recopilación / compactación de memoria que están separados del GC y tienden a no ser locales para la asignación. función. Entonces la gente podría implementar algo como lo siguiente:

T new2(T)( lazy T old_new ) { T obj; try{ obj = old_new; }catch(OutOfMemoryException oome) { foreach(compact; Global_List_Of_Delegates_From_Compatible_Objects) compact(); obj = old_new; } return obj; }

Lo cual es un argumento decente para agregar soporte para registrar / anular el registro de objetos autocompactantes / compactadores para recolectores de basura en general.


OOM debe ser recuperable porque el cierre no es la única estrategia para recuperarse de OOM.

En realidad, existe una solución bastante estándar para el problema OOM en el nivel de aplicación. Como parte del diseño de la aplicación, determine una cantidad mínima segura de memoria necesaria para recuperarse de una condición de falta de memoria. (Por ejemplo, la memoria requerida para guardar documentos automáticamente, abrir cuadros de diálogo de advertencia, cerrar datos de cierre).

Al inicio de su aplicación o al comienzo de un bloque crítico, asigne previamente esa cantidad de memoria. Si detecta una condición de falta de memoria, libere la memoria de protección y realice la recuperación. La estrategia aún puede fallar, pero en general da un gran golpe por el dinero.

Tenga en cuenta que la aplicación no necesita detenerse. Puede mostrar un diálogo modal hasta que se haya resuelto la condición OOM.

No estoy 100% seguro pero estoy bastante seguro de que '' Code Complete '' (lectura obligatoria para cualquier ingeniero de software respetable) lo cubre.

PD: puede extender su marco de aplicaciones para ayudar con esta estrategia, pero no implemente tal política en una biblioteca (las buenas bibliotecas no toman decisiones globales sin el consentimiento de las aplicaciones)


Realmente depende de lo que estás construyendo.

No es completamente irrazonable que un servidor web falle un par de solicitud / respuesta, pero luego continúe solicitándolo. Sin embargo, tendría que estar seguro de que la única falla no tenía efectos perjudiciales en el estado global, sería un poco complicado. Dado que una falla causa una excepción en la mayoría de los entornos administrados (por ejemplo, .NET y Java), sospecho que si la excepción se maneja en "código de usuario" sería recuperable para futuras solicitudes, por ejemplo, si una solicitud intentó asignar 10 GB de memoria y fallado, eso no debería dañar el resto del sistema. Sin embargo, si el sistema se queda sin memoria mientras intenta entregar la solicitud al código de usuario, ese tipo de cosas podrían ser más desagradables.


Sé que pidieron argumentos, pero solo puedo ver argumentos en contra.

No veo de todos modos para lograr esto en una aplicación multiproceso. ¿Cómo se sabe qué hilo es realmente responsable del error de falta de memoria? Un hilo podría asignar nueva memoria constantemente y tener gc-roots al 99% del montón, pero la primera asignación que falla ocurre en otro hilo.

Un ejemplo práctico: cada vez que se produce un OutOfMemoryError en nuestra aplicación Java (que se ejecuta en un servidor JBoss), no es como si un hilo fallece y el resto del servidor sigue ejecutándose: no, hay varios OOMEs, que matan varios hilos (algunos de los cuales son hilos internos de JBoss). No veo lo que podría hacer como programador para recuperarme de eso, o incluso lo que JBoss podría hacer para recuperarse de eso. De hecho, ni siquiera estoy seguro de que PUEDAS: el javadoc para VirtualMachineError sugiere que la JVM puede estar "rota" después de lanzar un error de este tipo. Pero tal vez la pregunta estaba más dirigida al diseño del lenguaje.


Sí, OOM es recuperable. Como ejemplo extremo, los sistemas operativos Unix y Windows se recuperan bastante bien de las condiciones OOM, la mayoría de las veces. Las aplicaciones fallan, pero el sistema operativo sobrevive (suponiendo que haya suficiente memoria para que el sistema operativo se inicie correctamente en primer lugar).

Solo cito este ejemplo para mostrar que se puede hacer.

El problema de tratar con OOM depende realmente de su programa y entorno.

Por ejemplo, en muchos casos, el lugar donde ocurre el OOM es probablemente el mejor lugar para recuperarse de un estado OOM.

Ahora, un asignador personalizado podría funcionar como un punto central dentro del código que puede manejar un OOM. El asignador de Java realizará un GC completo antes de lanzar una excepción OOM.

Mientras más "consciente de la aplicación" sea su asignador, más adecuado sería como controlador central y agente de recuperación para OOM. Usando Java de nuevo, su asignador no es particularmente consciente de las aplicaciones.

Aquí es donde algo como Java es fácilmente frustrante. No puede anular el asignador. Por lo tanto, aunque podría atrapar las excepciones de OOM en su propio código, no hay nada que diga que alguna biblioteca que esté utilizando atrape correctamente, o incluso que muestre correctamente una excepción de OOM. Es trivial crear una clase que se arruine para siempre con una excepción OOM, ya que algunos objetos se establecen en nulos y "eso nunca sucede", y nunca se pueden recuperar.

Entonces, sí, OOM es recuperable, pero puede ser MUY difícil, particularmente en entornos modernos como Java y su gran cantidad de bibliotecas de terceros de diversa calidad.


Si realmente no tienes memoria, estás condenado, ya que no puedes liberar nada más.

Si te quedaste sin memoria, pero algo como un recolector de basura puede entrar en acción y liberar algo de memoria, aún no estás muerto.

El otro problema es la fragmentación. Aunque es posible que no te quedes sin memoria (fragmentada), es posible que aún no puedas asignar la gran cantidad que quieres tener.


Tengo esto:

void *smalloc(size_t size) { void *mem = null; for(;;) { mem = malloc(size); if(mem == NULL) { sleep(1); } else break; } return mem; }

Lo cual ha salvado un sistema algunas veces. El hecho de que se haya quedado sin memoria ahora no significa que alguna otra parte del sistema u otros procesos que se ejecutan en el sistema tengan algo de memoria que devolverán pronto. Es mejor que tengas mucho cuidado antes de intentar tales trucos, y tener todo el control sobre cada memoria que asignas en tu programa.


Ya hay muchas buenas respuestas aquí. Pero me gustaría contribuir con otra perspectiva.

El agotamiento de casi cualquier recurso reutilizable debe ser recuperable en general. El razonamiento es que cada parte del programa es básicamente un subprograma. El hecho de que un submarino no pueda completar su fin en este mismo momento, no significa que todo el estado del programa sea basura. El hecho de que el estacionamiento esté lleno de autos no significa que descargues tu auto. O esperas un rato para que un stand sea gratuito o entras en una tienda más lejos para comprar tus cookies.

En la mayoría de los casos hay una forma alternativa. Hacer que un error sea irrecuperable, elimina de manera efectiva muchas opciones, y ninguno de nosotros quiere que nadie decida por nosotros qué podemos y qué no podemos hacer.

Lo mismo se aplica al espacio en disco. Es realmente el mismo razonamiento. Y, contrariamente a su insinuación sobre el desbordamiento de la pila es irrecuperable, diría que es una limitación arbitraria. No hay una buena razón para que no se pueda lanzar una excepción (apareciendo muchos marcos) y luego usar otro enfoque menos eficiente para hacer el trabajo.

Mis dos centavos :-)


uClibc tiene un búfer estático interno de 8 bytes más o menos para E / S de archivo cuando no hay más memoria asignada dinámicamente.