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?
java
garbage-collection
moonman239
fuente
fuente
Respuestas:
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.
fuente
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.
fuente
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 é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
malloc
C). 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 ).
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).
fuente
finalize
de ninguna gestión de recursos (de controladores de archivos, conexiones db, recursos gpu, etc.).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.
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:
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.
fuente
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.
fuente
Aunque
finalize
fue 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:
Invalide las referencias débiles que identifican objetos que no tienen ninguna referencia de alto alcance asociada con ellos.
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.
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.
fuente
Permítanme sugerir una nueva redacción y generalización de su pregunta:
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
finalize
mé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.fuente
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.
fuente
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.
fuente
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.
fuente
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.
fuente