La recolección de basura de Java se encarga de los objetos muertos en el montón, pero a veces congela el mundo. En C ++ tengo que llamar delete
para disponer de un objeto creado al final de su ciclo de vida.
Esto delete
parece un precio muy bajo a pagar por un entorno sin congelación. Colocar todas las delete
palabras clave relevantes es una tarea mecánica. Uno puede escribir un script que recorra el código y elimine una vez que ninguna rama nueva use un objeto dado.
Entonces, ¿cuáles son los pros y los contras de la compilación de Java en el modelo diy C ++ de recolección de basura?
No quiero iniciar un hilo C ++ vs Java. Mi pregunta es diferente
Todo esto del GC: ¿se reduce a "solo estar ordenado, no te olvides de eliminar los objetos que has creado, y no necesitarás ningún GC dedicado? ¿O es más como" deshacerse de los objetos en C ++ es realmente complicado? pasar el 20% de mi tiempo en ello y, sin embargo, las pérdidas de memoria son un lugar común "?
fuente
new
entra en el constructor de tu clase que gestiona la memoria,delete
entra en el destructor. A partir de ahí, todo es automático (almacenamiento). NB que esto funciona para todo tipo de recursos, no solo memoria, a diferencia de la recolección de basura. Los mutexes se toman en un constructor, se liberan en un destructor. Los archivos se abren en un constructor, se cierran en un destructor.Respuestas:
El ciclo de vida del objeto C ++
Si crea objetos locales, no necesita eliminarlos: el compilador genera código para eliminarlos automáticamente cuando el objeto se sale del alcance
Si usa punteros de objetos y crea objetos en la tienda gratuita, debe asegurarse de eliminar el objeto cuando ya no sea necesario (como ha descrito). Desafortunadamente, en un software complejo esto podría ser mucho más desafiante de lo que parece (por ejemplo, ¿qué pasa si se produce una excepción y nunca se alcanza la parte de eliminación esperada?).
Afortunadamente, en C ++ moderno (también conocido como C ++ 11 y posterior), tiene punteros inteligentes, como por ejemplo
shared_ptr
. Cuentan por referencia el objeto creado de una manera muy eficiente, un poco como lo haría un recolector de basura en Java. Y tan pronto como el objeto ya no esté referenciado, el último activoshared_ptr
elimina el objeto por usted. Automáticamente. Al igual que el recolector de basura, pero un objeto a la vez y sin demora (Ok: necesita un cuidado adicional yweak_ptr
hacer frente a las referencias circulares).Conclusión: hoy en día puede escribir código C ++ sin tener que preocuparse por la asignación de memoria, y que es tan libre de fugas como con un GC, pero sin el efecto de congelación.
El ciclo de vida del objeto Java
Lo bueno es que no tiene que preocuparse por el ciclo de vida de los objetos. Simplemente los creas y Java se encarga del resto. Un GC moderno identificará y destruirá los objetos que ya no son necesarios (incluso si hay referencias circulares entre objetos muertos).
Desafortunadamente, debido a esta comodidad, no tiene un control real de cuándo se elimina realmente el objeto . Semánticamente, la eliminación / destrucción coincide con la recolección de basura .
Esto está perfectamente bien si mira los objetos solo en términos de memoria. Excepto por el congelamiento, pero estos no son una fatalidad (la gente está trabajando en esto). No soy un experto en Java, pero creo que la destrucción retrasada hace que sea más difícil identificar fugas en Java debido a referencias guardadas accidentalmente a pesar de que los objetos ya no son necesarios (es decir, no se puede controlar la eliminación de objetos).
Pero, ¿qué pasa si el objeto tiene que controlar otros recursos además de la memoria, por ejemplo, un archivo abierto, un semáforo, un servicio del sistema? Su clase debe proporcionar un método para liberar estos recursos. Y tendrá la responsabilidad de asegurarse de que se llame a este método cuando los recursos ya no sean necesarios. En cada ruta de ramificación posible a través de su código, asegúrese de que también se invoque en caso de excepciones. El desafío es muy similar a la eliminación explícita en C ++.
Conclusión: el GC resuelve un problema de administración de memoria. Pero no aborda la gestión de otros recursos del sistema. La ausencia de la eliminación "justo a tiempo" podría hacer que la gestión de recursos sea muy difícil.
Eliminación, recolección de basura y RAII
Cuando puede controlar la eliminación de un objeto y el destructor que se va a invocar en la eliminación, puede beneficiarse de la RAII . Este enfoque considera la memoria solo como un caso especial de asignación de recursos y vincula la gestión de recursos de manera más segura al ciclo de vida del objeto, lo que garantiza un uso estrictamente controlado de los recursos.
fuente
new
fuera de constructores / punteros inteligentes, y nunca lo usedelete
fuera de un destructor.Like garbage collector, but one object at a time and without delay (Ok: you need some extra care and weak_ptr to cope with circular references).
Sin embargo, el recuento de referencias puede caer en cascada. Por ejemplo, la última referencia aA
desaparece, peroA
también tiene la última referencia aB
, quién tiene la última referencia aC
...And you'll have the responsibility to make sure that this method is called when the resources are no longer needed. In every possible branching path through your code, ensuring it is also invoked in case of exceptions.
Java y C # tienen instrucciones de bloque especiales para esto.Si puedes escribir un guión como ese, felicidades. Eres un mejor desarrollador que yo. Con mucho.
La única forma de evitar pérdidas de memoria en casos prácticos es mediante estándares de codificación muy estrictos con reglas muy estrictas sobre quién es el propietario de un objeto y cuándo puede y debe liberarse, o herramientas como punteros inteligentes que cuentan referencias a objetos y eliminan objetos cuando la última referencia se ha ido.
fuente
Si escribe el código C ++ correcto con RAII, generalmente no escribe ningún nuevo o borrado. Los únicos "nuevos" que escribes están dentro de punteros compartidos, por lo que realmente nunca tienes que usar "eliminar".
fuente
new
en absoluto, incluso con punteros compartidos, debería usarlo en sustd::make_shared
lugar.make_unique
. Es realmente bastante raro que realmente necesites una propiedad compartida.Hacer la vida de los programadores más fácil y evitar pérdidas de memoria es una ventaja importante de la recolección de basura, pero no es la única. Otro es prevenir la fragmentación de la memoria. En C ++, una vez que asigna un objeto usando la
new
palabra clave, permanece en una posición fija en la memoria. Esto significa que, a medida que se ejecuta la aplicación, terminas teniendo espacios libres de memoria entre los objetos asignados. Por lo tanto, la asignación de memoria en C ++ debe ser necesariamente un proceso más complicado, ya que el sistema operativo debe poder encontrar bloques no asignados de un tamaño determinado que se ajusten entre los espacios.La recolección de basura se encarga de tomar todos los objetos que no se eliminan y desplazarlos en la memoria para que formen un bloque continuo. Si experimenta que la recolección de basura lleva algún tiempo, probablemente se deba a este proceso, no a la desasignación de memoria en sí. El beneficio de esto es que cuando se trata de asignación de memoria, es casi tan sencillo como mover un puntero al final de la pila.
Entonces, en C ++, eliminar objetos es rápido, pero crearlos puede ser lento. En Java, la creación de objetos no lleva tiempo, pero debe realizar algunas tareas de limpieza de vez en cuando.
fuente
Las principales promesas de Java fueron
Parece que Java le garantiza que la basura se eliminará (no necesariamente de manera eficiente). Si usa C / C ++, tiene libertad y responsabilidad. Puede hacerlo mejor que el GC de Java, o puede ser mucho peor (omita
delete
todo junto y tenga problemas de pérdida de memoria).Si necesita un código que "cumpla con ciertos estándares de calidad" y para optimizar la "relación precio / calidad", use Java. Si está listo para invertir recursos adicionales (tiempo de sus expertos) para mejorar el rendimiento de la aplicación de misión crítica, use C.
fuente
La gran diferencia que hace la recolección de basura no es que no tenga que eliminar objetos explícitamente. La diferencia mucho mayor es que no tienes que copiar objetos.
Esto tiene efectos que se generalizan en el diseño de programas e interfaces en general. Permítanme dar un pequeño ejemplo para mostrar cuán de largo alcance es esto.
En Java, cuando saca algo de una pila, se devuelve el valor que se está sacando, por lo que obtiene un código como este:
En Java, esto es una excepción segura, porque todo lo que realmente estamos haciendo es copiar una referencia a un objeto, lo que se garantiza que sucederá sin una excepción. Sin embargo, lo mismo no es cierto en C ++. En C ++, devolver un valor significa (o al menos puede significar) copiar ese valor, y con algunos tipos que podrían generar una excepción. Si la excepción se produce después de que el elemento se haya eliminado de la pila, pero antes de que la copia llegue al receptor, el elemento se ha filtrado. Para evitar eso, la pila de C ++ utiliza un enfoque algo más torpe donde recuperar el elemento superior y eliminar el elemento superior son dos operaciones separadas:
Si la primera declaración arroja una excepción, la segunda no se ejecutará, por lo que si se lanza una excepción al copiar, el elemento permanece en la pila como si nada hubiera sucedido.
El problema obvio es que esto es simplemente torpe y (para las personas que no lo han usado) inesperado.
Lo mismo es cierto en muchas otras partes de C ++: especialmente en el código genérico, la seguridad de excepción impregna muchas partes del diseño, y esto se debe en gran parte al hecho de que la mayoría al menos potencialmente implica copiar objetos (que podrían arrojarse), donde Java simplemente crearía nuevas referencias a objetos existentes (que no pueden arrojarse, por lo que no tenemos que preocuparnos por las excepciones).
En cuanto a una secuencia de comandos simple para insertar
delete
donde sea necesario: si puede determinar estáticamente cuándo eliminar elementos en función de la estructura del código fuente, probablemente no debería haber estado utilizandonew
y,delete
en primer lugar.Permíteme darte un ejemplo de un programa para el cual esto seguramente no sería posible: un sistema para hacer, rastrear, facturar (etc.) llamadas telefónicas. Cuando marca su teléfono, crea un objeto de "llamada". El objeto de llamada realiza un seguimiento de a quién llamó, cuánto tiempo hable con ellos, etc., para agregar registros apropiados a los registros de facturación. El objeto de llamada monitorea el estado del hardware, por lo que cuando cuelga, se destruye a sí mismo (usando el ampliamente discutido
delete this;
). Solo que no es tan trivial como "cuando cuelgas". Por ejemplo, puede iniciar una llamada de conferencia, conectar a dos personas y colgar, pero la llamada continúa entre esas dos partes incluso después de que cuelgue (pero la facturación puede cambiar).fuente
Algo que no creo que se haya mencionado aquí es que existen eficiencias que provienen de la recolección de basura. En los recopiladores Java más utilizados, el lugar principal donde se asignan los objetos es un área reservada para un recopilador de copia. Cuando las cosas comienzan, este espacio está vacío. A medida que se crean los objetos, se asignan uno al lado del otro en el gran espacio abierto hasta que no pueda asignar uno en el espacio contiguo restante. El GC se activa y busca cualquier objeto en este espacio que no esté muerto. Copia los objetos vivos a otra área y los junta (es decir, sin fragmentación). El espacio antiguo se considera limpio. Luego continúa asignando objetos estrechamente juntos y repite este proceso según sea necesario.
Hay dos beneficios para esto. La primera es que no se dedica tiempo a eliminar los objetos no utilizados. Una vez que se copian los objetos vivos, la pizarra se considera limpia y los objetos muertos simplemente se olvidan. En muchas aplicaciones, la mayoría de los objetos no viven mucho tiempo, por lo que el costo de copiar el conjunto en vivo es económico en comparación con los ahorros obtenidos al no tener que preocuparse por el conjunto muerto.
El segundo beneficio es que cuando se asigna un nuevo objeto, no hay necesidad de buscar un área contigua. La VM siempre sabe dónde se colocará el siguiente objeto (advertencia: simplificado ignorando la concurrencia).
Este tipo de recolección y asignación es muy rápido. Desde una perspectiva de rendimiento general, es difícil de superar para muchos escenarios. El problema es que algunos objetos van a vivir más tiempo del que desea seguir copiando y, en última instancia, eso significa que el recolector puede tener que hacer una pausa durante un período de tiempo significativo de vez en cuando y cuando eso suceda puede ser impredecible. Dependiendo de la duración de la pausa y el tipo de aplicación, esto puede o no ser un problema. Hay al menos un colector sin pausa . Espero que haya una compensación de menor eficiencia para obtener la naturaleza sin pausa, pero una de las personas que fundaron esa compañía (Gil Tene) es un súper experto en GC y sus presentaciones son una gran fuente de información sobre GC.
fuente
En mi experiencia personal en C ++ e incluso C, las pérdidas de memoria nunca han sido una gran lucha para evitar. Con un procedimiento de prueba sensato y Valgrind, por ejemplo, cualquier fuga física causada por una llamada
operator new/malloc
sin un correspondiente adelete/free
menudo se detecta y repara rápidamente. Para ser justos, algunas bases de códigos C ++ grandes de C o de la vieja escuela podrían tener algunos casos extremos oscuros que podrían perder físicamente algunos bytes de memoria aquí y allá como resultado de no estardeleting/freeing
en ese caso límite que pasó desapercibido.Sin embargo, en lo que respecta a las observaciones prácticas, las aplicaciones con más fugas que encuentro (como en las que consumen más y más memoria cuanto más las ejecutas, a pesar de que la cantidad de datos con los que estamos trabajando no está creciendo) generalmente no se escriben en C o C ++. No encuentro cosas como el Kernel de Linux o Unreal Engine o incluso el código nativo utilizado para implementar Java entre la lista de software con fugas que encuentro.
El tipo más destacado de software con fugas que tiendo a encontrar son cosas como los applets de Flash, como los juegos de Flash, a pesar de que usan la recolección de basura. Y esa no es una comparación justa si se dedujera algo de esto, ya que muchas aplicaciones Flash están escritas por desarrolladores incipientes que probablemente carecen de principios de ingeniería y procedimientos de prueba sólidos (y del mismo modo estoy seguro de que hay profesionales capacitados que trabajan con GC que no luche con el software con fugas), pero tendría mucho que decir a cualquiera que piense que GC evita que se escriba el software con fugas.
Punteros colgantes
Ahora, viniendo de mi dominio particular, experiencia, y como uno que usa principalmente C y C ++ (y espero que los beneficios de GC varíen según nuestras experiencias y necesidades), lo más inmediato que GC resuelve para mí no son problemas prácticos de pérdida de memoria, sino cuelga el acceso del puntero, y eso podría ser literalmente un salvavidas en escenarios de misión crítica.
Desafortunadamente, en muchos de los casos en que GC resuelve lo que de otro modo sería un acceso de puntero colgante, reemplaza el mismo tipo de error del programador con una pérdida de memoria lógica.
Si imagina ese juego Flash escrito por un codificador en ciernes, podría almacenar referencias a elementos del juego en múltiples estructuras de datos, haciéndolos compartir la propiedad de estos recursos del juego. Desafortunadamente, digamos que comete un error cuando olvidó eliminar los elementos del juego de una de las estructuras de datos al avanzar a la siguiente etapa, evitando que se liberen hasta que se cierre todo el juego. Sin embargo, el juego todavía parece funcionar bien porque los elementos no se están dibujando o afectan la interacción del usuario. Sin embargo, el juego está comenzando a usar más y más memoria, mientras que las velocidades de fotogramas funcionan en una presentación de diapositivas, mientras que el procesamiento oculto todavía está recorriendo esta colección oculta de elementos en el juego (que ahora se ha vuelto explosivo en tamaño). Este es el tipo de problema que encuentro con frecuencia en tales juegos Flash.
Ahora digamos que el mismo desarrollador en ciernes escribió el juego en C ++. En ese caso, normalmente solo habría una estructura de datos central en el juego que "posee" la memoria, mientras que otros apuntan a esa memoria. Si comete el mismo tipo de error, lo más probable es que, al avanzar a la siguiente etapa, el juego se bloquee como resultado de acceder a punteros colgantes (o peor, hacer algo diferente al bloqueo).
Este es el tipo de compensación más inmediato que tiendo a encontrar en mi dominio con mayor frecuencia entre GC y no GC. Y en realidad no me importa mucho GC en mi dominio, lo que no es muy crítico para la misión, porque las mayores dificultades que tuve con el software con fugas involucraron el uso accidental de GC en un antiguo equipo que causó el tipo de fugas descritas anteriormente .
En mi dominio particular, prefiero que el software falle o falle en muchos casos porque es al menos mucho más fácil de detectar que tratar de rastrear por qué el software consume misteriosamente cantidades explosivas de memoria después de ejecutarlo durante media hora mientras todos las pruebas de unidad e integración pasan sin ninguna queja (ni siquiera de Valgrind, ya que la memoria está siendo liberada por GC al apagarse). Sin embargo, eso no es un golpe para GC por mi parte o un intento de decir que es inútil o algo así, pero no ha sido ningún tipo de bala de plata, ni siquiera cercana, en los equipos con los que trabajé contra el software con fugas (para Por el contrario, tuve la experiencia opuesta con esa base de código que utiliza GC siendo la más permeable que he encontrado). Para ser justos, muchos miembros de ese equipo ni siquiera sabían qué referencias débiles eran,
Propiedad compartida y psicología
El problema que encuentro con la recolección de basura que puede hacerlo tan propenso a las "pérdidas de memoria" (e insistiré en llamarlo como tal, la "fuga espacial" se comporta exactamente de la misma manera desde la perspectiva del usuario final) en manos de aquellos que no lo usan con cuidado se relacionan con las "tendencias humanas" hasta cierto punto en mi experiencia. El problema con ese equipo y la base de código con más filtraciones que encontré fue que parecían tener la impresión de que GC les permitiría dejar de pensar en quién posee los recursos.
En nuestro caso, teníamos tantos objetos que se hacían referencia entre sí. Los modelos harían referencia a los materiales junto con la biblioteca de materiales y el sistema de sombreado. Los materiales harían referencia a las texturas junto con la biblioteca de texturas y ciertos sombreadores. Las cámaras almacenarían referencias a todo tipo de entidades de escena que deberían excluirse del renderizado. La lista parecía continuar indefinidamente. Eso hizo que casi cualquier recurso considerable en el sistema fuera propiedad y se extendiera de por vida en más de 10 lugares en el estado de la aplicación a la vez, y eso era muy, muy propenso a errores humanos del tipo que se traduciría en una fuga (y no uno menor, estoy hablando de gigabytes en minutos con serios problemas de usabilidad). Conceptualmente, no era necesario compartir todos estos recursos en propiedad, todos conceptualmente tenían un propietario,
Si dejamos de pensar en quién posee qué memoria, y felizmente solo almacenamos referencias a objetos que se extienden por toda la vida en todo el lugar sin pensar en esto, entonces el software no se bloqueará como resultado de punteros colgantes, pero casi con seguridad, bajo tal mentalidad descuidada, comience a perder memoria como loca en formas que son muy difíciles de rastrear y eludirán las pruebas.
Si hay un beneficio práctico para el puntero colgante en mi dominio, es que causa fallas y fallas muy desagradables. Y eso, al menos, tiende a dar a los desarrolladores el incentivo, si quieren enviar algo confiable, para comenzar a pensar en la gestión de recursos y hacer las cosas adecuadas necesarias para eliminar todas las referencias / punteros adicionales a un objeto que ya no es conceptualmente necesario.
Gestión de recursos de aplicaciones
La gestión adecuada de los recursos es el nombre del juego si estamos hablando de evitar fugas en aplicaciones de larga duración con un estado persistente almacenado donde las fugas plantearían serios problemas de velocidad de fotogramas y usabilidad. Y administrar correctamente los recursos aquí no es menos difícil con o sin GC. El trabajo no es menos manual para eliminar las referencias apropiadas a los objetos que ya no son necesarios, ya sean punteros o referencias que extiendan la vida útil.
Ese es el desafío en mi dominio, sin olvidar
delete
lo que nosotrosnew
(a menos que estemos hablando de hora de aficionados con pruebas, prácticas y herramientas de mala calidad). Y requiere pensar y cuidar si estamos usando GC o no.Multithreading
El otro problema que encuentro muy útil con GC, si pudiera usarse con mucha precaución en mi dominio, es simplificar la gestión de recursos en contextos de subprocesos múltiples. Si tenemos cuidado de no almacenar referencias de recursos que se extiendan a lo largo de la vida en más de un lugar en el estado de la aplicación, entonces la naturaleza de las referencias de GC que se extienden a lo largo de la vida podría ser extremadamente útil como una forma para que los hilos extiendan temporalmente un recurso al que se accede para extender es de por vida por solo una corta duración según sea necesario para que el hilo termine de procesarlo.
Creo que un uso muy cuidadoso de GC de esta manera podría generar un software muy correcto que no tenga fugas, al tiempo que simplifica el subprocesamiento múltiple.
Hay formas de evitar esto, aunque ausente GC. En mi caso, unificamos la representación de la entidad de escena del software, con hilos que temporalmente hacen que los recursos de la escena se extiendan por breves períodos de una manera bastante generalizada antes de una fase de limpieza. Esto puede oler un poco a GC pero la diferencia es que no hay una "propiedad compartida" involucrada, solo un diseño de procesamiento de escena uniforme en hilos que difieren la destrucción de dichos recursos. Aún así, sería mucho más simple confiar en GC aquí si pudiera usarse con mucho cuidado con desarrolladores concienzudos, con cuidado de usar referencias débiles en las áreas persistentes relevantes, para tales casos de subprocesos múltiples.
C ++
Finalmente:
En Modern C ++, esto generalmente no es algo que debería hacer manualmente. Ni siquiera se trata de olvidar hacerlo. Cuando involucra el manejo de excepciones en la imagen, incluso si escribió un correspondiente
delete
debajo de alguna llamada anew
, algo podría lanzarse en el medio y nunca llegar a ladelete
llamada si no confía en las llamadas destructoras automáticas insertadas por el compilador para hacer esto. tú.Con C ++ prácticamente necesita, a menos que esté trabajando como un contexto incrustado con excepciones desactivadas y bibliotecas especiales que están programadas deliberadamente para no arrojar, evite dicha limpieza manual de recursos (que incluye evitar llamadas manuales para desbloquear un mutex fuera de un dtor , por ejemplo, y no solo desasignación de memoria). El manejo de excepciones casi lo exige, por lo que toda la limpieza de recursos debe automatizarse a través de destructores en su mayor parte.
fuente