java garbage-collection jvm heap-memory jvm-hotspot

java - ¿Qué se considera exactamente una raíz de recolección de basura y cómo se encuentran en la JVM de HotSpot?



garbage-collection heap-memory (2)

Introducción:

En la universidad, uno aprende que las raíces de recolección de basura típicas en Java (y lenguajes similares) son variables estáticas de clases cargadas, variables de subprocesos locales de subprocesos que se están ejecutando actualmente, "referencias externas" como los manejadores JNI y datos específicos de GC como " old-to" -los punteros jóvenes durante los GC de menor importancia de un recolector de basura generacional. En teoría, esto no suena tan duro.

Problema:

Estoy leyendo el código fuente de HotSpot y me interesó cómo se detectan estas raíces de recolección de basura dentro de la VM, es decir, qué métodos se utilizan dentro del código fuente de JVM para visitar todas las raíces.

Investigación:

Encontré varios archivos (por ejemplo, psMarkSweep.cpp ), pertenecientes a varias implementaciones de GC, que contienen construcciones muy similares.

A continuación se muestra el método PSMarkSweep::mark_sweep_phase1 de psMarkSweep.cpp que creo que cubre las raíces fuertes:

ParallelScavengeHeap::ParStrongRootsScope psrs; Universe::oops_do(mark_and_push_closure()); JNIHandles::oops_do(mark_and_push_closure()); // Global (strong) JNI handles CLDToOopClosure mark_and_push_from_cld(mark_and_push_closure()); MarkingCodeBlobClosure each_active_code_blob(mark_and_push_closure(), !CodeBlobToOopClosure::FixRelocations); Threads::oops_do(mark_and_push_closure(), &mark_and_push_from_cld, &each_active_code_blob); ObjectSynchronizer::oops_do(mark_and_push_closure()); FlatProfiler::oops_do(mark_and_push_closure()); Management::oops_do(mark_and_push_closure()); JvmtiExport::oops_do(mark_and_push_closure()); SystemDictionary::always_strong_oops_do(mark_and_push_closure()); ClassLoaderDataGraph::always_strong_cld_do(follow_cld_closure()); // Do not treat nmethods as strong roots for mark/sweep, since we can unload them. //CodeCache::scavenge_root_nmethods_do(CodeBlobToOopClosure(mark_and_push_closure()));

El siguiente código de psScavenge.cpp parece agregar tareas para los diferentes tipos de raíces GC:

if (!old_gen->object_space()->is_empty()) { // There are only old-to-young pointers if there are objects // in the old gen. uint stripe_total = active_workers; for(uint i=0; i < stripe_total; i++) { q->enqueue(new OldToYoungRootsTask(old_gen, old_top, i, stripe_total)); } } q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::universe)); q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::jni_handles)); // We scan the thread roots in parallel Threads::create_thread_roots_tasks(q); q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::object_synchronizer)); q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::flat_profiler)); q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::management)); q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::system_dictionary)); q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::class_loader_data)); q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::jvmti)); q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::code_cache));

Mirando ScavangeRootsTask , vemos un código familiar similar al código en psMarkSweep :

void ScavengeRootsTask::do_it(GCTaskManager* manager, uint which) { assert(Universe::heap()->is_gc_active(), "called outside gc"); PSPromotionManager* pm = PSPromotionManager::gc_thread_promotion_manager(which); PSScavengeRootsClosure roots_closure(pm); PSPromoteRootsClosure roots_to_old_closure(pm); switch (_root_type) { case universe: Universe::oops_do(&roots_closure); break; case jni_handles: JNIHandles::oops_do(&roots_closure); break; case threads: { ResourceMark rm; CLDClosure* cld_closure = NULL; // Not needed. All CLDs are already visited. Threads::oops_do(&roots_closure, cld_closure, NULL); } break; case object_synchronizer: ObjectSynchronizer::oops_do(&roots_closure); break; case flat_profiler: FlatProfiler::oops_do(&roots_closure); break; case system_dictionary: SystemDictionary::oops_do(&roots_closure); break; case class_loader_data: { PSScavengeKlassClosure klass_closure(pm); ClassLoaderDataGraph::oops_do(&roots_closure, &klass_closure, false); } break; case management: Management::oops_do(&roots_closure); break; case jvmti: JvmtiExport::oops_do(&roots_closure); break; case code_cache: { MarkingCodeBlobClosure each_scavengable_code_blob(&roots_to_old_closure, CodeBlobToOopClosure::FixRelocations); CodeCache::scavenge_root_nmethods_do(&each_scavengable_code_blob); } break; default: fatal("Unknown root type"); } // Do the real work pm->drain_stacks(false); }

Visión:

Estas listas de raíces de GC en el código fuente parecen bastante más grandes de lo que escribí inicialmente en mi primera oración, así que trato de enumerarlas a continuación, con algunos comentarios:

  • Universo : está bien, el universo. Sobre todo espejos de ciertas clases.
  • Manijas de JNI : también claras, manijas creadas a través de JNI que mantienen vivos los objetos.
  • Hilos : Éste visita las raíces locales de los thead. Pero aquí vemos la primera diferencia. psMarkSweep.cpp usa un CLDToOopClosure y hace algo con código blobs, mientras que psScavange.cpp no lo hace. Afaik, CLD significa datos del cargador de clases , pero no sé por qué se usa en un caso, pero no en otro. Las mismas cuentas para blobs de código.
  • Object Synchronizer : Monitores utilizados para la sincronización.
  • Perfil plano : un generador de perfiles que forma parte de Hotspot, mantiene vivo su cargador de clases.
  • Administración : objetos que se mantienen vivos para ciertos servicios de administración, como MemoryPoolMXBean, etc.
  • JVMTI : mantiene vivos los puntos de interrupción de JVMTI y los objetos asignados por JVMTI.
  • Diccionario del sistema : mantiene vivas todas las clases que se cargan con el cargador de clases del sistema Java y, por lo tanto, los objetos a los que hacen referencia los campos estáticos de las clases.
  • Gráfico de datos del cargador de clases : este es un poco confuso para mí. Creo que estos son los cargadores de clases que no son el cargador de clases del sistema Java, es decir, ¿esto cubre las clases (y sus campos estáticos) que han sido cargados por diferentes cargadores de clases?
  • Caché de código : el caché de código contiene ciertos blobs de código, pero aún no estoy seguro de qué son exactamente estos blobs de código. Parece que un blob de código representa información sobre marcos de código (compilados), ¿tengo razón sobre esto? Pero aún no entiendo por qué estos blobs de código a veces se visitan mientras atraviesan las pilas de subprocesos (como se hace en psMarkSweep.cpp ) y en ocasiones se visitan con el CodeCache (como se hace en psScavenge.cpp ).
  • (Solo para GC menor) Raíces antiguas hasta jóvenes : claras.

Preguntas:

Si bien se pueden encontrar muchas cosas en el código fuente, todavía me cuesta entender algunas de estas raíces de GC, o cómo se encuentran estas raíces de GC.

  • ¿Qué es un blob código? ¿Qué raíces GC contiene que no están cubiertas al visitar un hilo con un cierre de oop? ¿Qué es el caché de código?
  • Para reunir todas las raíces de subprocesos: ¿Cuál es la diferencia entre usar un CLDToOopClosure y un MarkingCodeBlobClosure en combinación con Threads::oops_do (como se hace en psMarkSweep.cpp ), en comparación con visitar los hilos con un cierre de oop y además ejecutando ClassLoaderDataGraph::oops_do y CodeCache::scavenge_root_nmethods_do (como lo usa psScavenge.cpp ).
  • ¿Qué es el gráfico de datos del cargador de clases (comparado con el diccionario del sistema)? ¿Es la colección de los cargadores de clase de aplicaciones?
  • ¿Qué pasa con las cadenas internas, cómo sobreviven a los GC? ¿Residen en algún lugar fuera del montón, donde la recolección de basura no les afecta?
  • ¿Otros recolectores de basura, por ejemplo, el G1 GC, introducen nuevos tipos de punteros de raíz? (No creo que este sea el caso)

Observación:

Sé que esta es una pregunta muy larga con varias subpreguntas, pero creo que hubiera sido difícil dividirla en varias. Aprecio cada respuesta publicada, incluso si no cubre una respuesta a todas las preguntas formuladas anteriormente, incluso las respuestas a algunas de ellas me ayudarán. ¡Gracias!


Como esta publicación tiene varias preguntas, me atrevo a responder algunas de ellas:

1) Un blob de código es un código JIT. Puede contener punteros de objeto codificados (como ensamblador inmediato) (por ejemplo, para clasificar objetos o para finales estáticos). Si se mueve el objeto, se ajusta lo inmediato en el código.

2) ni idea

3) los datos del cargador de clases son objetos nativos de metadatos (que no están dentro del montón) que pueden contener referencias (por ejemplo) a clases cargadas.

4) las cadenas internadas residen en el montón como objetos normales (en máquinas virtuales antiguas en el gen perm). La única diferencia es que (por la razón de internar y almacenar en caché), por lo general, nunca se recopilan y siempre están implícitamente vivos. 5) Según mi conocimiento, un GC no debe introducir una nueva categoría de raíces GC, después de todo, una raíz GC como concepto que es independiente de GC. Sin embargo, cada GC puede almacenarlos y manejarlos de manera diferente.

EDITAR:

Solo pensé en otra cosa:

2) la máquina virtual hace un uso intensivo de los cierres, lo que básicamente significa llamadas virtuales. Sin embargo, las llamadas virtuales pueden ser caras (especialmente cuando las hace con frecuencia, por ejemplo, para cada objeto y cada puntero del montón), por lo que en lugar de combinar cierres existentes, la VM a menudo implementa cierres especializados para evitar llamadas virtuales innecesarias. Esto puede ser una razón para ello.

1 '') Me acabo de dar cuenta de que uno podría interpretar mi respuesta como una raíz redundante (porque los objetos de clase y las finales estáticas siempre son referenciados desde otro lugar). Primero, no son redundantes desde la perspectiva del GC porque los inmediatos aún deben ajustarse si se mueve el objeto. Y segundo, el JIT puede decidir codificar el puntero a CUALQUIER objeto, si por ejemplo detecta que al interpretar, una llamada específica SIEMPRE devuelve un puntero al mismo objeto. Por lo tanto, una raíz de blob de código puede ser la única raíz para un objeto específico.


Como ya descubrió, <Subsystem>::oops_do() es un mecanismo típico en HotSpot JVM para visitar las raíces de GC de <Subsystem> . Buen análisis, por cierto. Simplemente continúe con las fuentes de VM y encontrará las respuestas, ya que hay muchos comentarios útiles en el código.

Tenga en cuenta que el propósito de oops_do no es solo marcar objetos alcanzables, sino también procesar las propias referencias, en particular, para reubicarlos durante la compactación.

CodeBlob es una pieza de código generado. Cubre no solo los métodos JITted (también nmethods como nmethods ) sino también varios stubs y rutinas de VM generados en tiempo de ejecución.

// CodeBlob - superclass for all entries in the CodeCache. // // Suptypes are: // nmethod : Compiled Java methods (include method that calls to native code) // RuntimeStub : Call to VM runtime methods // DeoptimizationBlob : Used for deoptimizatation // ExceptionBlob : Used for stack unrolling // SafepointBlob : Used to handle illegal instruction exceptions

Estos fragmentos de código pueden contener referencias incrustadas a objetos de Heap, por ejemplo, cadenas / clase / método, manejo de literales y constantes finales estáticas.

El propósito de CLDToOopClosure en Threads::oops_do es marcar los objetos a los que se hace referencia a través de los punteros del método que no están marcados de otra manera:

// The method pointer in the frame might be the only path to the method''s // klass, and the klass needs to be kept alive while executing. The GCs // don''t trace through method pointers, so typically in similar situations // the mirror or the class loader of the klass are installed as a GC root. // To minimze the overhead of doing that here, we ask the GC to pass down a // closure that knows how to keep klasses alive given a ClassLoaderData. cld_f->do_cld(m->method_holder()->class_loader_data());

De manera similar, MarkingCodeBlobClosure se usa para marcar objetos a los que se hace referencia solo desde nmethods activos :

// In cases where perm gen is collected, GC will want to mark // oops referenced from nmethods active on thread stacks so as to // prevent them from being collected. However, this visit should be // restricted to certain phases of the collection only. The // closure decides how it wants nmethods to be traced. if (cf != NULL) cf->do_code_blob(_cb);

Tenga en cuenta que CodeCache::scavenge_root_nmethods_do no se llama durante la fase de marcado:

// Do not treat nmethods as strong roots for mark/sweep, since we can unload them. //CodeCache::scavenge_root_nmethods_do(CodeBlobToOopClosure(&mark_and_push_closure));

SystemDictionary es responsable principalmente de resolver nombres simbólicos de clases. No sirve como una raíz de GC para el marcado (excepto para los cargadores de clases Bootstrap y System). Por otro lado, ClassLoaderDataGraph mantiene el conjunto de enlaces completo de las entidades del cargador de clases. Sirve como raíz GC y es responsable de la descarga de clases.

// A ClassLoaderData identifies the full set of class types that a class // loader''s name resolution strategy produces for a given configuration of the // class loader. // Class types in the ClassLoaderData may be defined by from class file binaries // provided by the class loader, or from other class loader it interacts with // according to its name resolution strategy. // ... // ClassLoaderData carries information related to a linkset (e.g., // metaspace holding its klass definitions). // The System Dictionary and related data structures (e.g., placeholder table, // loader constraints table) as well as the runtime representation of classes // only reference ClassLoaderData. // // Instances of java.lang.ClassLoader holds a pointer to a ClassLoaderData that // that represent the loader''s "linking domain" in the JVM.

Las cadenas internas no necesitan sobrevivir a GC. No son raíces GC. Está bien descargar cadenas internas inalcanzables de lo contrario, y HotSpot realmente lo hace.

Un recolector de basura no introduce nuevos tipos de raíces, pero puede usar algoritmos y estructuras de datos que afectan el significado de "accesibilidad". Por ejemplo, los recolectores concurrentes pueden tratar todas las referencias modificadas entre la marca inicial y el comentario final como accesibles, incluso si no lo son.