¿Por qué no se eliminan los objetos Java inmediatamente después de que ya no se hace referencia a ellos?

77

En Java, tan pronto como un objeto ya no tenga referencias, puede eliminarse, pero la JVM decide cuándo se elimina realmente el objeto. Para usar la terminología de Objective-C, todas las referencias de Java son inherentemente "fuertes". Sin embargo, en Objective-C, si un objeto ya no tiene referencias fuertes, el objeto se elimina inmediatamente. ¿Por qué no es este el caso en Java?

moonman239
fuente
46
No debería importarle cuando los objetos Java se eliminen realmente. Es un detalle de implementación.
Basile Starynkevitch
154
@BasileStarynkevitch Debes preocuparte y desafiar absolutamente cómo funciona tu sistema / plataforma. Hacer preguntas 'cómo' y 'por qué' es una de las mejores maneras de convertirse en un mejor programador (y, en un sentido más general, una persona más inteligente).
Artur Biesiadowski
66
¿Qué hace el objetivo C cuando hay referencias circulares? ¿Asumo que solo los filtra?
Mehrdad
45
@ArturBiesiadowksi: No, la especificación de Java no indica cuándo se elimina un objeto (y del mismo modo, para R5RS ). Podrías y probablemente deberías desarrollar tu programa Java como si esa eliminación nunca ocurriera (y para procesos de corta duración como un mundo hola de Java, de hecho no sucede). Puede que le importe el conjunto de objetos vivos (o el consumo de memoria), que es una historia diferente.
Basile Starynkevitch
28
Un día, el novato le dijo al maestro "Tengo una solución para nuestro problema de asignación. Le daremos a cada asignación un recuento de referencia, y cuando llegue a cero, podemos eliminar el objeto". El maestro respondió "Un día, el novicio le dijo al maestro" Tengo una solución ...
Eric Lippert

Respuestas:

79

En primer lugar, Java tiene referencias débiles y otra categoría de mejor esfuerzo llamada referencias suaves. Las referencias débiles frente a las fuertes son un tema completamente separado del recuento de referencias frente a la recolección de basura.

En segundo lugar, existen patrones en el uso de la memoria que pueden hacer que la recolección de basura sea más eficiente en el tiempo al sacrificar el espacio. Por ejemplo, es mucho más probable que se eliminen los objetos más nuevos que los objetos más antiguos. Entonces, si espera un poco entre barridos, puede eliminar la mayor parte de la nueva generación de memoria, mientras mueve a los pocos sobrevivientes al almacenamiento a largo plazo. Ese almacenamiento a largo plazo se puede escanear con mucha menos frecuencia. La eliminación inmediata mediante la gestión manual de la memoria o el recuento de referencias es mucho más propenso a la fragmentación.

Es algo así como la diferencia entre ir de compras al supermercado una vez por cada cheque de pago e ir todos los días para obtener suficiente comida por un día. Su viaje grande durará mucho más que un viaje pequeño individual, pero en general terminará ahorrando tiempo y probablemente dinero.

Karl Bielefeldt
fuente
58
La esposa de un programador lo envía al supermercado. Ella le dice: "Compre una barra de pan y, si ve algunos huevos, tome una docena". El programador luego regresa con una docena de hogazas de pan debajo del brazo.
Neil
77
Sugiero mencionar que el tiempo gc de nueva generación es generalmente proporcional a la cantidad de objetos vivos , por lo que tener más objetos eliminados significa que su costo no se pagará en absoluto en muchos casos. Eliminar es tan simple como voltear el puntero de espacio de sobreviviente y opcionalmente poner a cero todo el espacio de memoria en un gran conjunto de memoria (no estoy seguro si se hace al final de gc o se amortiza durante la asignación de tlabs u objetos en los jvms actuales)
Artur Biesiadowski
64
@Neil, ¿no deberían ser 13 panes?
JAD
67
"Apagado por un error en el pasillo 7"
joeytwiddle
13
@JAD Hubiera dicho 13, pero la mayoría no tiende a entender eso. ;)
Neil
86

Porque saber correctamente que algo ya no está referenciado no es fácil. Ni siquiera cerca de fácil.

¿Qué pasa si tiene dos objetos que se refieren entre sí? ¿Se quedan para siempre? Si extiende esa línea de pensamiento para resolver cualquier estructura de datos arbitraria, pronto verá por qué la JVM u otros recolectores de basura se ven obligados a emplear métodos mucho más sofisticados para determinar qué es lo que aún se necesita y qué puede pasar.

whatsisname
fuente
77
O podría adoptar un enfoque de Python en el que utilice el recuento tanto como sea posible, recurriendo a un GC cuando espere que haya dependencias circulares que pierdan memoria. ¿No veo por qué no pudieron tener un recuento además de GC?
Mehrdad
27
@Mehrdad podrían. Pero probablemente sería más lento. Nada le impide implementar esto, pero no espere superar ninguno de los GC en Hotspot o OpenJ9.
Josef
21
@ jpmc26 porque si elimina objetos tan pronto como ya no se usen, la probabilidad es alta de que los elimine en una situación de carga alta que aumenta aún más la carga. GC puede funcionar cuando hay menos carga. El conteo de referencias en sí es una pequeña sobrecarga para cada referencia. Además, con un GC, a menudo puede descartar una gran parte de la memoria sin referencias sin manejar los objetos individuales.
Josef
33
@Josef: el recuento de referencias adecuado tampoco es gratuito; La actualización del recuento de referencias requiere incrementos / decrementos atómicos, que son sorprendentemente costosos , especialmente en arquitecturas multinúcleo modernas. En CPython no es un gran problema (CPython es extremadamente lento por sí solo, y el GIL limita su rendimiento multiproceso a niveles de un solo núcleo), pero en un lenguaje más rápido que también admite paralelismo puede ser un problema. No es una posibilidad que PyPy se deshaga por completo del conteo de referencias y solo use GC.
Matteo Italia
10
@Mehrdad una vez que haya implementado su GC de conteo de referencia para Java, con gusto lo probaré para encontrar un caso en el que funcione peor que cualquier otra implementación de GC.
Josef
45

AFAIK, la especificación JVM (escrita en inglés) no menciona cuándo se debe eliminar exactamente un objeto (o un valor), y lo deja a la implementación (del mismo modo para R5RS ). De alguna manera requiere o sugiere un recolector de basura, pero deja los detalles para la implementación. Y del mismo modo para la especificación Java.

Recuerde que los lenguajes de programación son especificaciones (de sintaxis , semántica , etc.), no implementaciones de software. Un lenguaje como Java (o su JVM) tiene muchas implementaciones. Su especificación es publicada , descargable (para que pueda estudiarla) y escrita en inglés. §2.5.3 El montón de la especificación JVM menciona un recolector de basura:

El almacenamiento de almacenamiento dinámico para objetos es reclamado por un sistema de gestión de almacenamiento automático (conocido como recolector de basura); Los objetos nunca se desasignan explícitamente. La máquina virtual Java no asume ningún tipo particular de sistema de gestión de almacenamiento automático

(el énfasis es mío; la finalización de BTW se menciona en §12.6 de la especificación de Java, y un modelo de memoria está en §17.4 de la especificación de Java)

Entonces (en Java) no debería importarle cuando un objeto se elimina , y podría codificar como si no sucediera (razonando en una abstracción donde ignora eso). Por supuesto, hay que preocuparse por el consumo de memoria y el conjunto de objetos que viven, que es una diferente pregunta. En varios casos simples (piense en un programa de "hola mundo") puede probar, o convencerse, de que la memoria asignada es bastante pequeña (por ejemplo, menos de un gigabyte), y luego no le importa en absoluto eliminación de objetos individuales . En más casos, puedes convencerte de que los objetos vivos(o alcanzables, que es un superconjunto, más fácil de razonar sobre los vivos) nunca excede un límite razonable (y luego confía en GC, pero no le importa cómo y cuándo ocurre la recolección de basura). Lea sobre la complejidad del espacio .

Supongo que en varias implementaciones de JVM que ejecutan un programa Java de corta duración como hello world, el recolector de basura no se activa en absoluto y no se produce ninguna eliminación. AFAIU, tal comportamiento se ajusta a las numerosas especificaciones de Java.

La mayoría de las implementaciones de JVM usan técnicas de copia generacional (al menos para la mayoría de los objetos Java, aquellos que no usan finalización o referencias débiles ; y no se garantiza que la finalización suceda en poco tiempo y podría posponerse, por lo que es solo una característica útil que su código no debería dependerá mucho de ello) en el que la noción de eliminar un objeto individual no tiene ningún sentido (ya que un gran bloque de memoria -que contiene zonas de memoria para muchos objetos-, quizás varios megabytes a la vez, se libera a la vez).

Si la especificación JVM requiere que cada objeto se elimine exactamente lo antes posible (o simplemente imponga más restricciones a la eliminación de objetos), se prohibirían las técnicas de GC generacionales eficientes, y los diseñadores de Java y de la JVM han sido sabios al evitar eso.

Por cierto, podría ser posible que una JVM ingenua que nunca elimina objetos y no libera memoria se ajuste a las especificaciones (la letra, no el espíritu) y ciertamente pueda ejecutar una cosa de hola mundo en la práctica (tenga en cuenta que la mayoría los programas Java pequeños y de corta duración probablemente no asignen más de unos pocos gigabytes de memoria). Por supuesto, no vale la pena mencionar tal JVM y es solo una cosa de juguete (como es esta implementación de mallocC). Vea el Epsilon NoOp GC para más información. Las JVM de la vida real son piezas de software muy complejas y combinan varias técnicas de recolección de basura.

Además, Java no es lo mismo que JVM, y tiene implementaciones de Java ejecutándose sin JVM (por ejemplo , compiladores Java anticipados , tiempo de ejecución de Android ). En algunos casos (en su mayoría académicos), puede imaginarse (llamadas técnicas de "recolección de basura en tiempo de compilación") que un programa Java no asigna o elimina en tiempo de ejecución (por ejemplo, porque el compilador de optimización ha sido lo suficientemente inteligente como para usar solo el pila de llamadas y variables automáticas ).

¿Por qué no se eliminan los objetos Java inmediatamente después de que ya no se hace referencia a ellos?

Porque las especificaciones Java y JVM no requieren eso.


Lea el manual de GC para obtener más información (y la especificación JVM ). Observe que estar vivo (o útil para el cálculo futuro) de un objeto es una propiedad de todo el programa (no modular).

Objective-C favorece un enfoque de conteo de referencia para la gestión de la memoria . Y que también tiene trampas (por ejemplo, el Objective-C programador tiene que preocuparse por las referencias circulares por explicitar referencias débiles, pero una JVM maneja referencias circulares muy bien en la práctica sin necesidad de atención por parte del programador de Java).

No hay Silver Bullet en la programación y el diseño del lenguaje de programación (tenga en cuenta el problema de detención ; ser un objeto vivo útil es indecidible en general).

También puede leer SICP , Pragmática del lenguaje de programación , El libro del dragón , Lisp en piezas pequeñas y Sistemas operativos: tres piezas fáciles . No se trata de Java, pero le abrirán la mente y deberían ayudarlo a comprender qué debe hacer una JVM y cómo podría funcionar prácticamente (con otras piezas) en su computadora. También podría pasar muchos meses (o varios años) estudiando el código fuente complejo de implementaciones JVM de código abierto existentes (como OpenJDK , que tiene varios millones de líneas de código fuente).

Basile Starynkevitch
fuente
20
"podría ser posible que una ingenua JVM que nunca elimina objetos y no libera memoria se ajuste a las especificaciones" ¡Sin duda se ajusta a las especificaciones! Java 11 en realidad está agregando un recolector de basura no operativo para, entre otras cosas, programas de muy corta duración.
Michael
66
"No debería importarle cuando se elimina un objeto" No estoy de acuerdo. Por un lado, debe saber que RAII ya no es un patrón factible, y que no puede depender finalizede ninguna gestión de recursos (de controladores de archivos, conexiones db, recursos gpu, etc.).
Alexander
44
@Michael Tiene mucho sentido para el procesamiento por lotes con un límite de memoria usado. El sistema operativo solo puede decir "¡toda la memoria utilizada por este programa ya no está!" después de todo, que es bastante rápido. De hecho, muchos programas en C se escribieron de esa manera, especialmente en el mundo de Unix temprano. Pascal tenía el maravillosamente horrible "restablecer el puntero de pila / montón a un punto de control previamente guardado" que le permitía hacer lo mismo, aunque era bastante inseguro: marcar, iniciar subtarea, restablecer.
Luaan
66
@Alexander en general fuera de C ++ (y algunos lenguajes que se derivan intencionalmente de él), suponiendo que RAII funcionará basado solo en finalizadores es un antipatrón, uno que debe advertirse y reemplazarse con un bloque explícito de control de recursos. El objetivo de GC es que la vida útil y los recursos están desacoplados, después de todo.
Leushenko
3
@Leushenko Estoy totalmente en desacuerdo con que "la vida útil y los recursos están desacoplados" es el "punto central" de GC. Es el precio negativo que paga por el punto principal de GC: administración de memoria fácil y segura. "Asumir que RAII funcionará basado solo en finalizadores es un antipatrón" ¿En Java? Quizás. Pero no en CPython, Rust, Swift o Objective C. "advirtió y reemplazó con un bloque de control de recursos explícito" No, estos son estrictamente más limitados. Un objeto que gestiona un recurso a través de RAII le da un control para pasar la vida de ámbito. Un bloque de prueba con recursos está limitado a un solo alcance.
Alexander
23

Para usar la terminología de Objective-C, todas las referencias de Java son inherentemente "fuertes".

Eso no es correcto: Java tiene referencias débiles y suaves, aunque se implementan a nivel de objeto en lugar de palabras clave del lenguaje.

En Objective-C, si un objeto ya no tiene referencias fuertes, el objeto se elimina inmediatamente.

Eso tampoco es necesariamente correcto: algunas versiones de Objective C de hecho usaron un recolector de basura generacional. Otras versiones no tenían recolección de basura en absoluto.

Es cierto que las versiones más nuevas de Objective C usan el conteo automático de referencias (ARC) en lugar de un GC basado en trazas, y esto (a menudo) hace que el objeto se "elimine" cuando ese recuento de referencias llega a cero. Sin embargo, tenga en cuenta que una implementación de JVM también podría ser compatible y funcionar exactamente de esta manera (diablos, podría ser compatible y no tener GC en absoluto).

Entonces, ¿por qué la mayoría de las implementaciones de JVM no hacen esto, y en su lugar usan algoritmos GC basados ​​en rastreo?

En pocas palabras, ARC no es tan utópico como parece:

  • Debe aumentar o disminuir un contador cada vez que una referencia se copia, modifica o queda fuera del alcance, lo que genera una sobrecarga de rendimiento obvia.
  • ARC no puede eliminar fácilmente las referencias cíclicas, ya que todas tienen una referencia entre sí, por lo que su recuento de referencias nunca llega a cero.

ARC tiene ventajas, por supuesto: es simple de implementar y la recopilación es determinista. Pero las desventajas anteriores, entre otras, son la razón por la cual la mayoría de las implementaciones de JVM usarán un GC generacional basado en rastreo.

berry120
fuente
1
Lo curioso es que Apple cambió a ARC precisamente porque vieron que, en la práctica, supera ampliamente a otros GC (en particular los generacionales). Para ser justos, esto es principalmente cierto en las plataformas con memoria limitada (iPhone). Pero contradeciría su afirmación de que "ARC no es tan utópico como parece" al decir que los GC generacionales (y otros no deterministas) no son tan utópicos como parecen: la destrucción determinista es probablemente una mejor opción en el La gran mayoría de los escenarios.
Konrad Rudolph
3
@KonradRudolph, aunque también soy un fanático de la destrucción determinista, no creo que "la mejor opción en la gran mayoría de los escenarios" se mantenga. Sin duda, es una mejor opción cuando la latencia o la memoria son más importantes que el rendimiento promedio, y en particular cuando la lógica es razonablemente simple. Pero no es que no haya muchas aplicaciones complejas que requieran muchas referencias cíclicas, etc. y requieran una operación promedio rápida, pero en realidad no les importa la latencia y tienen mucha memoria disponible. Para estos, es dudoso si ARC es una buena idea.
Leftaroundabout
1
@leftaroundabout En "la mayoría de los escenarios", ni el rendimiento ni la presión de la memoria son un cuello de botella, por lo que no importa de ninguna manera. Su ejemplo es un escenario específico. De acuerdo, no es extremadamente raro, pero no iría tan lejos como para afirmar que es más común que otros escenarios en los que ARC es más adecuado. Además, ARC puede manejar bien los ciclos. Solo requiere una intervención manual simple por parte del programador. Esto lo hace menos ideal pero difícilmente un factor decisivo. Sostengo que la finalización determinista es una característica mucho más importante de lo que pretendes.
Konrad Rudolph,
3
@KonradRudolph Si ARC requiere una intervención manual simple por parte del programador, entonces no se trata de ciclos. Si comienza a usar listas doblemente vinculadas, ARC se transfiere a la asignación manual de memoria. Si tiene grandes gráficos arbitrarios, ARC lo obliga a escribir un recolector de basura. El argumento de GC sería que los recursos que necesitan destrucción no son el trabajo del subsistema de memoria, y para rastrear los relativamente pocos de ellos, deben finalizarse determinísticamente a través de una intervención manual simple por parte del programador.
prosfilaes
2
@KonradRudolph ARC y los ciclos conducen fundamentalmente a pérdidas de memoria si no se manejan manualmente. En sistemas suficientemente complejos, pueden ocurrir fugas importantes si, por ejemplo, algún objeto almacenado en un mapa almacena una referencia a ese mapa, un cambio que podría realizar un programador que no esté a cargo de las secciones de código que crean y destruyen ese mapa. Los grandes gráficos arbitrarios no significan que los punteros internos no sean fuertes, que está bien que desaparezcan los elementos vinculados. Si tratar con algunas pérdidas de memoria es un problema menor que tener que cerrar archivos manualmente, no lo diré, pero es real.
prosfilaes
5

Java no especifica con precisión cuándo se recolecta el objeto porque eso le da a las implementaciones la libertad de elegir cómo manejar la recolección de basura.

Existen muchos mecanismos diferentes de recolección de basura, pero aquellos que garantizan que puede recolectar un objeto inmediatamente se basan casi por completo en el recuento de referencias (no conozco ningún algoritmo que rompa esta tendencia). El recuento de referencias es una herramienta poderosa, pero tiene el costo de mantener el recuento de referencias. En el código de subproceso único, eso no es más que un incremento y una disminución, por lo que asignar un puntero puede costar un costo del orden de 3 veces más en el código contado de referencia que en el código contado sin referencia (si el compilador puede hacer que todo se reduzca a máquina) código).

En código multiproceso, el costo es mayor. Requiere aumentos / decrementos atómicos o bloqueos, los cuales pueden ser costosos. En un procesador moderno, una operación atómica puede ser del orden de 20 veces más costosa que una simple operación de registro (obviamente varía de procesador a procesador). Esto puede aumentar el costo.

Entonces, con esto, podemos considerar las compensaciones hechas por varios modelos.

  • Objective-C se centra en ARC: conteo de referencias automatizado. Su enfoque es utilizar el recuento de referencias para todo. No hay detección de ciclos (que yo sepa), por lo que se espera que los programadores eviten que ocurran ciclos, lo que cuesta tiempo de desarrollo. Su teoría es que los punteros no se asignan con tanta frecuencia, y su compilador puede identificar situaciones en las que el aumento / disminución de los recuentos de referencia no puede causar la muerte de un objeto, y eludir esos incrementos / decrementos por completo. Por lo tanto, minimizan el costo del recuento de referencias.

  • CPython utiliza un mecanismo híbrido. Usan recuentos de referencia, pero también tienen un recolector de basura que identifica los ciclos y los libera. Esto proporciona los beneficios de ambos mundos, a costa de ambos enfoques. CPython debe mantener recuentos de referencia yhacer la contabilidad para detectar ciclos. CPython se sale con la suya de dos maneras. El puño es que CPython realmente no es completamente multiproceso. Tiene un bloqueo conocido como GIL que limita el subprocesamiento múltiple. Esto significa que CPython puede usar incrementos / decrementos normales en lugar de los atómicos, que es mucho más rápido. CPython también se interpreta, lo que significa que las operaciones como la asignación a una variable ya toman un puñado de instrucciones en lugar de solo 1. El costo adicional de hacer los incrementos / decrementos, que se realiza rápidamente en el código C, es un problema menor porque nosotros ' Ya he pagado este costo.

  • Java sigue el enfoque de no garantizar un sistema contado de referencia en absoluto. De hecho, la especificación no dice nada sobre cómo se gestionan los objetos, aparte de que habrá un sistema de gestión de almacenamiento automático. Sin embargo, la especificación también sugiere fuertemente la suposición de que esto será basura recolectada de una manera que maneje los ciclos. Al no especificar cuándo caducan los objetos, Java obtiene la libertad de usar colectores que no pierden el tiempo aumentando / disminuyendo. De hecho, los algoritmos inteligentes, como los recolectores de basura generacionales, incluso pueden manejar muchos casos simples sin siquiera mirar los datos que se están reclamando (solo tienen que mirar los datos que todavía se están haciendo referencia).

Entonces podemos ver que cada uno de estos tres tuvo que hacer compensaciones. La mejor opción depende en gran medida de la forma en que se pretende utilizar el idioma.

Cort Ammon
fuente
4

Aunque finalizefue respaldado en el GC de Java, la recolección de basura en su núcleo no está interesada en los objetos muertos, sino en los vivos. En algunos sistemas GC (posiblemente incluyendo algunas implementaciones de Java), lo único que distingue un grupo de bits que representa un objeto de un grupo de almacenamiento que no se utiliza para nada puede ser la existencia de referencias a los primeros. Si bien los objetos con finalizadores se agregan a una lista especial, otros objetos pueden no tener nada en cualquier parte del universo que indique que su almacenamiento está asociado con un objeto, excepto las referencias contenidas en el código de usuario. Cuando se sobrescribe la última referencia de este tipo, el patrón de bits en la memoria dejará de ser reconocible inmediatamente como un objeto, independientemente de si algo en el universo es consciente de ello.

El propósito de la recolección de basura no es destruir objetos a los que no existen referencias, sino más bien lograr tres cosas:

  1. Invalide las referencias débiles que identifican objetos que no tienen ninguna referencia de alto alcance asociada con ellos.

  2. Busque en la lista de objetos del sistema con finalizadores para ver si alguno de ellos no tiene referencias de gran alcance asociadas con ellos.

  3. Identifique y consolide regiones de almacenamiento que no estén siendo utilizadas por ningún objeto.

Tenga en cuenta que el objetivo principal del GC es el # 3, y cuanto más espere antes de hacerlo, más oportunidades de consolidación tendrá. Tiene sentido hacer el n. ° 3 en los casos en que uno tendría un uso inmediato para el almacenamiento, pero de lo contrario tiene más sentido diferirlo.

Super gato
fuente
55
En realidad, gc solo tiene un objetivo: simular memoria infinita. Todo lo que nombró como objetivo es una imperfección en la abstracción o un detalle de implementación.
Deduplicador
@Deduplicator: las referencias débiles ofrecen una semántica útil que no se puede lograr sin la asistencia de GC.
supercat
Claro, las referencias débiles tienen semántica útil. ¿Pero se necesitaría esa semántica si la simulación fuera mejor?
Deduplicador
@Dupuplicator: Sí. Considere una colección que define cómo interactuarán las actualizaciones con la enumeración. Tal colección puede necesitar contener referencias débiles a cualquier enumerador en vivo. En un sistema de memoria ilimitada, una colección que se repitió repetidamente tendría su lista de enumeradores interesados ​​crecer sin límite. La memoria requerida para esa lista no sería un problema, pero el tiempo requerido para recorrerla degradaría el rendimiento del sistema. Agregar GC puede significar la diferencia entre un algoritmo O (N) y O (N ^ 2).
supercat
2
¿Por qué desearía notificar a los enumeradores, en lugar de agregarlos a una lista y dejarlos buscar por sí mismos cuando se usan? Y de todos modos, cualquier programa que dependa de que la basura se procese de manera oportuna en lugar de depender de la presión de la memoria está viviendo en un estado de pecado, si es que se mueve.
Deduplicador
4

Permítanme sugerir una nueva redacción y generalización de su pregunta:

¿Por qué Java no ofrece garantías sólidas sobre su proceso de GC?

Con eso en mente, recorra rápidamente las respuestas aquí. Hay siete hasta ahora (sin contar este), con bastantes hilos de comentarios.

Esa es tu respuesta.

GC es difícil. Hay muchas consideraciones, muchas compensaciones diferentes y, en última instancia, muchos enfoques muy diferentes. Algunos de esos enfoques hacen factible GC un objeto tan pronto como no es necesario; otros no. Al mantener el contrato suelto, Java ofrece a sus implementadores más opciones.

Hay una compensación incluso en esa decisión, por supuesto: al mantener el contrato suelto, Java en su mayoría * elimina la capacidad de los programadores de confiar en los destructores. Esto es algo que los programadores de C ++ en particular a menudo omiten ([cita requerida];)), por lo que no es una compensación insignificante. No he visto una discusión sobre esa meta-decisión en particular, pero presumiblemente la gente de Java decidió que los beneficios de tener más opciones de GC superaban los beneficios de poder decirle a los programadores exactamente cuándo se destruirá un objeto.


* Existe el finalizemétodo, pero por varias razones que están fuera del alcance de esta respuesta, es difícil y no es una buena idea confiar en él.

yshavit
fuente
3

Existen dos estrategias diferentes para manejar la memoria sin código explícito escrito por el desarrollador: recolección de basura y conteo de referencias.

La recolección de basura tiene la ventaja de que "funciona" a menos que el desarrollador haga algo estúpido. Con el recuento de referencias, puede tener ciclos de referencia, lo que significa que "funciona", pero el desarrollador a veces tiene que ser inteligente. Entonces eso es una ventaja para la recolección de basura.

Con el recuento de referencias, el objeto desaparece inmediatamente cuando el recuento de referencias baja a cero. Esa es una ventaja para el recuento de referencias.

Speedwise, la recolección de basura es más rápida si cree en los fanáticos de la recolección de basura, y el conteo de referencias es más rápido si cree en los fanáticos del conteo de referencias.

Son solo dos métodos diferentes para lograr el mismo objetivo, Java eligió un método, Objective-C eligió otro (y agregó una gran cantidad de soporte del compilador para cambiarlo de una molestia a algo que es poco trabajo para los desarrolladores).

Cambiar Java de recolección de basura a conteo de referencias sería una tarea importante, porque se necesitarían muchos cambios de código.

En teoría, Java podría haber implementado una mezcla de recolección de basura y conteo de referencias: si el conteo de referencias es 0, entonces el objeto es inalcanzable, pero no necesariamente al revés. Por lo tanto, puede mantener los recuentos de referencias y eliminar objetos cuando su recuento de referencias es cero (y luego ejecutar la recolección de basura de vez en cuando para atrapar objetos dentro de ciclos de referencia inalcanzables). Creo que el mundo se divide 50/50 en personas que piensan que agregar el conteo de referencias a la recolección de basura es una mala idea, y las personas que piensan que agregar la recolección de basura al conteo de referencias es una mala idea. Entonces esto no va a suceder.

Por lo tanto, Java podría eliminar objetos inmediatamente si su recuento de referencia se convierte en cero, y eliminar objetos dentro de ciclos inalcanzables más adelante. Pero esa es una decisión de diseño, y Java decidió no hacerlo.

gnasher729
fuente
Con el recuento de referencias, la finalización es trivial, ya que el programador se encargó de los ciclos. Con gc, los ciclos son triviales, pero el programador debe tener cuidado al finalizar.
Deduplicador
@Deduplicator En Java, también es posible crear fuertes referencias a objetos que están siendo finalizados ... En Objective-C y Swift, una vez que la cuenta de referencia es cero, el objeto va a desaparecer (a menos que poner un bucle infinito en dealloc / deísta).
gnasher729
Acabo de notar estúpido corrector ortográfico reemplazando deinit con deist ...
gnasher729
1
Hay una razón por la que la mayoría de los programadores odian la corrección ortográfica automática ... ;-)
Deduplicator
jajaja ... Creo que el mundo está dividido 0.1 / 0.1 / 99.8 entre las personas que piensan que agregar el conteo de referencias a la recolección de basura es una mala idea, y las personas que piensan que agregar la recolección de basura al conteo de referencias es una mala idea, y las personas que seguir contando los días hasta que llegue la recolección de basura, porque esa tonelada ya está recibiendo maloliente de nuevo ...
leftaroundabout
1

Todos los otros argumentos de rendimiento y discusiones sobre la dificultad de comprensión cuando ya no hay referencias a un objeto son correctos, aunque otra idea que creo que vale la pena mencionar es que hay al menos una JVM (azul) que considera algo como esto en el sentido de que implementa un gc paralelo que esencialmente tiene un hilo vm que verifica constantemente las referencias para intentar eliminarlas, lo que no actuará de manera completamente diferente de lo que está hablando. Básicamente, mirará constantemente el montón e intentará recuperar cualquier memoria a la que no se haga referencia. Esto tiene un costo de rendimiento muy leve, pero conduce a tiempos de GC esencialmente cero o muy cortos. (Eso es a menos que el tamaño del montón en constante expansión exceda la RAM del sistema y luego Azul se confunda y luego haya dragones)

TLDR Algo así existe para la JVM, solo es una jvm especial y tiene inconvenientes como cualquier otro compromiso de ingeniería.

Descargo de responsabilidad: no tengo vínculos con Azul, solo lo usamos en un trabajo anterior.

prefecto vado
fuente
1

Maximizar el rendimiento sostenido o minimizar la latencia gc están en tensión dinámica, que es probablemente la razón más común por la cual la GC no ocurre de inmediato. En algunos sistemas, como las aplicaciones de emergencia 911, no alcanzar un umbral de latencia específico puede comenzar a desencadenar procesos de conmutación por error del sitio. En otros, como un sitio de banca y / o arbitraje, es mucho más importante maximizar el rendimiento.

barmid
fuente
0

Velocidad

Por qué todo esto está sucediendo es en última instancia debido a la velocidad. Si los procesadores eran infinitamente rápidos, o (para ser prácticos) cercanos, por ejemplo, 1,000,000,000,000,000,000,000,000,000,000,000 operaciones por segundo, entonces puede tener cosas increíblemente largas y complicadas entre cada operador, como asegurarse de que se eliminen los objetos desreferenciados. Como ese número de operaciones por segundo no es actualmente cierto y, como la mayoría de las otras respuestas explican que en realidad es complicado y requiere muchos recursos para resolver esto, la recolección de basura existe para que los programas puedan enfocarse en lo que realmente están tratando de lograr en un Manera rápida.

Michael Durrant
fuente
Bueno, estoy seguro de que encontraríamos formas más interesantes de usar los ciclos adicionales que eso.
Deduplicador