Análisis de código fuente JVM del cargador de clases zombie (irrecuperable) bajo JDK8

Este artículo proviene de: PerfMa Technology Community

Sitio web oficial de PerfMa (Stupid Network)

Resumen

Este artículo se basa en un problema que se investigó recientemente y a nuestro equipo le tomó mucho tiempo resolver el problema. El fenómeno es que algunos cargadores de clase se colocan como claves en WeakHashMap, pero después de muchas veces de gc completo, todavía son fuertes. Almacenados en la memoria, pero desde el punto de vista del código, estos cargadores de clases deben reciclarse, ya que no hay una referencia fuerte a estos cargadores de clases, por lo que hicimos un volcado de memoria, analizamos la memoria y descubrimos que además de WeakHashMap No hay otra forma de GC ROOT para llegar a estos cargadores de clase, por lo que después de muchas veces FULL GC ciertamente puede reciclarse, pero el hecho no es el caso, para que este problema suene mejor entendido, o como de costumbre. Esta demostración simula completamente este escenario.

Manifestación

Primero, creamos dos clases AAA y AAB, que se empaquetan en dos frascos diferentes, como AAA.jar y AAB.jar. Existe una relación entre estas dos clases. Hay un atributo en AAA que es del tipo AAB. Nota No coloque estos dos frascos en el classpath para permitir que se cargue appClassLoader:

public class AAA {
        private AAB aab;
        public AAA(){
                aab=new AAB();
        }
        public void clear(){
                aab=null;
        }
}

public class AAB {}
复制代码

Luego creamos una clase para cargar TestLoader, que contiene un WeakHashMap, específicamente para almacenar TestLoader, y anulamos el método loadClass, si es para cargar la clase AAB, cree un nuevo TestLoader para cargar esta clase desde AAB.jar

import java.net.URL;
import java.net.URLClassLoader;
import java.util.WeakHashMap;

public class TestLoader extends URLClassLoader {
        public static WeakHashMap<TestLoader,Object> map=new WeakHashMap<TestLoader,Object>();
        private static int count=0;
        public TestLoader(URL[] urls){
                super(urls);
                map.put(this, new Object());
        }
        @SuppressWarnings("resource")
        public Class<?> loadClass(String name) throws ClassNotFoundException {
                if(name.equals("AAB") && count==0){
                        try {
                                count=1;
                    URL[] urls = new URL[1];
                    urls[0] = new URL("file:///home/nijiaben/tmp/AAB.jar");
                    return new TestLoader(urls).loadClass("AAB");
                }catch (Exception e){
                    e.printStackTrace();
                }
                }else{
                        return super.loadClass(name);
                }
                return null;
        }
}
复制代码

Mirando nuestra clase principal TTest, algunas instrucciones están escritas en la clase:

import java.lang.reflect.Method;
import java.net.URL;

/**
 * Created by nijiaben on 4/22/16.
 */
public class TTest {
    private Object aaa;
    public static void main(String args[]){
        try {
            TTest tt = new TTest();
            //将对象移到old,并置空aaa的aab属性
            test(tt);
            //清理掉aab对象
            System.gc();
            System.out.println("finished");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @SuppressWarnings("resource")
        public static void test(TTest tt){
        try {
            //创建一个新的类加载器,从AAA.jar里加载AAA类
            URL[] urls = new URL[1];
            urls[0] = new URL("file:///home/nijiaben/tmp/AAA.jar");
            tt.aaa=new TestLoader(urls).loadClass("AAA").newInstance();
            //保证类加载器对象能进入到old里,因为ygc是不会对classLoader做清理的
            for(int i=0;i<10;i++){
                System.gc();
                Thread.sleep(1000);
            }
            //将aaa里的aab属性清空掉,以便在后面gc的时候能清理掉aab对象,这样AAB的类加载器其实就没有什么地方有强引用了,在full gc的时候能被回收
            Method[] methods=tt.aaa.getClass().getDeclaredMethods();
            for(Method m:methods){
                if(m.getName().equals("clear")){
                        m.invoke(tt.aaa);
                        break;
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
复制代码

Al ejecutar, ejecute bajo JDK8, establezca un punto de interrupción System.out.println("finished")y luego realice un volcado de memoria.

Por el ejemplo anterior, sabemos que TTest es cargado por el cargador de clases AppClassLoader, el tipo de objeto de su atributo aaa se carga desde AAA.jar a través de TestLoader, y el atributo aab en aaa es de un nuevo cargador de clases TestLoader Cargado desde AAB.jar, cuando hacemos GC del sistema varias veces, estos objetos se moverán a viejos, después de hacer el último GC, el objeto aab se eliminará de la memoria y su cargador de clases ya no estará en este momento. Hay una referencia fuerte en cualquier lugar, y solo un WeakHashMap se refiere a ella. En teoría, debe reciclarse al hacer GC, pero de hecho, el cargador de clases de este AAB no ha sido reciclado. De los resultados del análisis, la ruta de RAÍZ de GC es WeakHashMap .

Metaspace en JDK8

Un concepto que debo mencionar aquí es el metaespacio en JDK8. Es para reemplazar la permanente. En cuanto a los beneficios, personalmente siento que no es tan obvio. Es un sentimiento un poco ingrato. El código ha cambiado mucho, pero los beneficios reales no son obvios. Se dice que es el resultado de las luchas internas de Oracle.

Aunque no hay una permanente en JDK8, la información de klass todavía necesita ser almacenada en algún lugar. En la jvm se asignan dos piezas de memoria, una está al lado del montón, al igual que la permanente, se usa específicamente para almacenar información de klass, El -XX:CompressedClassSpaceSizetamaño puede establecerse por , y la otra pieza no está necesariamente conectada a ellos. Principalmente almacena otra información que no sea klass, como un grupo constante o algo, que puede -XX:InitialBootClassLoaderMetaspaceSizeestablecerse. Al mismo tiempo, también podemos -XX:MaxMetaspaceSizeestablecer el umbral que desencadena la recuperación de metaspace.

Cada cargador de clases tomará algo de metaChunk del metaspace global para administrarlo. Cuando hay una definición de clase, en realidad se asigna de estas memorias. Cuando no es suficiente, asignará un bloque en el metaspace global y lo administrará.

Esta situación específica puede seguirse escribiendo un artículo para presentar, incluida la estructura de la memoria, la asignación de memoria, el GC, etc.

ClassLoaderDataGraph en JDK8

Cada cargador de clases corresponderá a una estructura de datos ClassLoaderData, que almacenará, por ejemplo, objetos específicos del cargador de clases, klass cargado, metaspace de gestión de memoria, etc. Es una estructura de cadena que se vinculará con el próximo ClassLoaderData, gc Al atravesar estos ClassLoaderData a través de ClassLoaderDataGraph, el primer ClassLoaderData de ClassLoaderDataGraph es bootstrapClassLoader

class ClassLoaderData : public CHeapObj<mtClass> {
  ...
  static ClassLoaderData * _the_null_class_loader_data;

  oop _class_loader;          // oop used to uniquely identify a class loader
                              // class loader or a canonical class path
  Dependencies _dependencies; // holds dependencies from this class loader
                              // data to others.

  Metaspace * _metaspace;  // Meta-space where meta-data defined by the
                           // classes in the class loader are allocated.
  Mutex* _metaspace_lock;  // Locks the metaspace for allocations and setup.
  bool _unloading;         // true if this class loader goes away
  bool _keep_alive;        // if this CLD is kept alive without a keep_alive_object().
  bool _is_anonymous;      // if this CLD is for an anonymous class
  volatile int _claimed;   // true if claimed, for example during GC traces.
                           // To avoid applying oop closure more than once.
                           // Has to be an int because we cas it.
  Klass* _klasses;         // The classes defined by the class loader.

  JNIHandleBlock* _handles; // Handles to constant pool arrays

  // These method IDs are created for the class loader and set to NULL when the
  // class loader is unloaded.  They are rarely freed, only for redefine classes
  // and if they lose a data race in InstanceKlass.
  JNIMethodBlock*                  _jmethod_ids;

  // Metadata to be deallocated when it's safe at class unloading, when
  // this class loader isn't unloaded itself.
  GrowableArray<Metadata*>*      _deallocate_list;

  // Support for walking class loader data objects
  ClassLoaderData* _next; /// Next loader_datas created

  // ReadOnly and ReadWrite metaspaces (static because only on the null
  // class loader for now).
  static Metaspace* _ro_metaspace;
  static Metaspace* _rw_metaspace;

  ...

}
复制代码

Aquí hay algunos atributos:

  • _class_loader : Es el objeto del cargador de clases correspondiente

  • _keep_alive : Si este valor es verdadero, este cargador de clases se considerará vivo y se usará como parte de GC ROOT, y no se reciclará cuando gc

  • _unloading : Indica si esta carga de clase debe descargarse

  • _is_anonymous : Ya sea anónimo, este ClassLoaderData se usa principalmente en expresiones lambda, hablaré de esto en detalle más adelante

  • _next : Señale el siguiente ClassLoaderData, fácil de recorrer en gc

  • _dependencies : Este atributo también es el foco de este artículo, que se detallará más adelante.

Echemos un vistazo al constructor nuevamente:

ClassLoaderData::ClassLoaderData(Handle h_class_loader, bool is_anonymous, Dependencies dependencies) :
  _class_loader(h_class_loader()),
  _is_anonymous(is_anonymous),
  // An anonymous class loader data doesn't have anything to keep
  // it from being unloaded during parsing of the anonymous class.
  // The null-class-loader should always be kept alive.
  _keep_alive(is_anonymous || h_class_loader.is_null()),
  _metaspace(NULL), _unloading(false), _klasses(NULL),
  _claimed(0), _jmethod_ids(NULL), _handles(NULL), _deallocate_list(NULL),
  _next(NULL), _dependencies(dependencies),
  _metaspace_lock(new Mutex(Monitor::leaf+1, "Metaspace allocation lock", true)) {
    // empty
}
复制代码

Se puede ver que _keep_ailveel valor del atributo se basa en _is_anonymoussi el cargador de clases actual es bootstrapClassLoader.

_keep_alive¿Dónde se usa exactamente? De hecho, es durante el GC decidir si usar Closure o qué Closure usar para escanear el ClassLoaderData correspondiente.

void ClassLoaderDataGraph::roots_cld_do(CLDClosure* strong, CLDClosure* weak) {
  //从最后一个创建的classloader到bootstrapClassloader  
  for (ClassLoaderData* cld = _head;  cld != NULL; cld = cld->_next) {
    //如果是ygc,那weak和strong是一样的,对所有的类加载器都做扫描,保证它们都是活的 
    //如果是cms initmark阶段,如果要unload_classes了(should_unload_classes()返回true),则weak为null,那就只遍历bootstrapclassloader以及正在做匿名类加载的类加载  
    CLDClosure* closure = cld->keep_alive() ? strong : weak;
    if (closure != NULL) {
      closure->do_cld(cld);
    }
  }
复制代码

¿Cuándo se recicla el cargador de clases?

Si el cargador de clases necesita ser reciclado, de hecho, depende de si el objeto del cargador de clases está vivo, el llamado live es cualquier clase cargada por el cargador de clases o los objetos de estas clases son muy accesibles, por supuesto, incluida esta clase El cargador en sí es parte de GC ROOT o hay un camino accesible desde GC ROOT, entonces este cargador de clase definitivamente no se reciclará.

De varias situaciones de GC:

  • Si es YGC, el cargador de clases se usa como GC ROOT, es decir, no se reciclará

  • Si es Full GC, se reciclará mientras esté muerto

  • Si se trata de un GC de CMS, el proceso de GC de CMS también se marcará (este es el valor predeterminado, pero puede modificarse mediante algunos parámetros), pero no se realizará una limpieza real. La acción de limpieza real se producirá cuando se ingrese el punto de seguridad la próxima vez.

Cómo generar el cargador de clases zombie

Si el cargador de clases realmente depende del objeto RAÍZ GC, este objeto de cargador de clases está vivo y bien. Podemos analizarlo a través de zprofiler o mat, y podemos dibujar el enlace, pero hay dos casos. Excepción

carga de clase anónima lambda

La carga de clases anónimas de Lambda es un método inseguro defineAnonymousClass, este método corresponde al siguiente método en vm

UNSAFE_ENTRY(jclass, Unsafe_DefineAnonymousClass(JNIEnv *env, jobject unsafe, jclass host_class, jbyteArray data, jobjectArray cp_patches_jh))
{
  instanceKlassHandle anon_klass;
  jobject res_jh = NULL;

  UnsafeWrapper("Unsafe_DefineAnonymousClass");
  ResourceMark rm(THREAD);

  HeapWord* temp_alloc = NULL;

  anon_klass = Unsafe_DefineAnonymousClass_impl(env, host_class, data,
                                                cp_patches_jh,
                                                   &temp_alloc, THREAD);
  if (anon_klass() != NULL)
    res_jh = JNIHandles::make_local(env, anon_klass->java_mirror());

  // try/finally clause:
  if (temp_alloc != NULL) {
    FREE_C_HEAP_ARRAY(HeapWord, temp_alloc, mtInternal);
  }

  // The anonymous class loader data has been artificially been kept alive to
  // this point.   The mirror and any instances of this class have to keep
  // it alive afterwards.
  if (anon_klass() != NULL) {
    anon_klass->class_loader_data()->set_keep_alive(false);
  }

  // let caller initialize it as needed...

  return (jclass) res_jh;
}
UNSAFE_END
}
复制代码

Se puede ver que después de la creación exitosa de la clase anónima, la _keep_alivepropiedad ClassLoaderData correspondiente se establecerá en falso, ¿eso significa que la _keep_alivepropiedad es verdadera antes de esto? El siguiente parse_streammétodo es el método que se transferirá del método anterior.

Klass* SystemDictionary::parse_stream(Symbol* class_name,
                                      Handle class_loader,
                                      Handle protection_domain,
                                      ClassFileStream* st,
                                      KlassHandle host_klass,
                                      GrowableArray<Handle>* cp_patches,
                                      TRAPS) {
  TempNewSymbol parsed_name = NULL;

  Ticks class_load_start_time = Ticks::now();

  ClassLoaderData* loader_data;
  if (host_klass.not_null()) {
    // Create a new CLD for anonymous class, that uses the same class loader
    // as the host_klass
    assert(EnableInvokeDynamic, "");
    guarantee(host_klass->class_loader() == class_loader(), "should be the same");
    guarantee(!DumpSharedSpaces, "must not create anonymous classes when dumping");
    loader_data = ClassLoaderData::anonymous_class_loader_data(class_loader(), CHECK_NULL);
    loader_data->record_dependency(host_klass(), CHECK_NULL);
  } else {
    loader_data = ClassLoaderData::class_loader_data(class_loader());
  }

  instanceKlassHandle k = ClassFileParser(st).parseClassFile(class_name,
                                                             loader_data,
                                                             protection_domain,
                                                             host_klass,
                                                             cp_patches,
                                                             parsed_name,
                                                             true,
                                                             THREAD);
...

}

ClassLoaderData* ClassLoaderData::anonymous_class_loader_data(oop loader, TRAPS) {
  // Add a new class loader data to the graph.
  return ClassLoaderDataGraph::add(loader, true, CHECK_NULL);
}

ClassLoaderData* ClassLoaderDataGraph::add(Handle loader, bool is_anonymous, TRAPS) {
  // We need to allocate all the oops for the ClassLoaderData before allocating the
  // actual ClassLoaderData object.
  ClassLoaderData::Dependencies dependencies(CHECK_NULL);

  No_Safepoint_Verifier no_safepoints; // we mustn't GC until we've installed the
                                       // ClassLoaderData in the graph since the CLD
                                       // contains unhandled oops

  ClassLoaderData* cld = new ClassLoaderData(loader, is_anonymous, dependencies);

...
}
复制代码

A partir del código anterior, siempre que se tome el método inseguro, se creará un objeto ClassLoaderData para el cargador de clases actual y su _is_anonymous se establece en verdadero, lo que también significa que la propiedad de _keep_alive es verdadera y se agrega a ClassLoaderDataGraph .

Imagínese si la clase anónima creada no tiene éxito, es decir, anon_klass () == null, entonces la propiedad _keep_alive nunca se puede establecer en falso, lo que significa que el objeto ClassLoader correspondiente a este ClassLoaderData siempre será parte del GC ROOT, No se puede reciclar, esta situación es un verdadero cargador de clases de zombies, pero aún no he simulado esta situación, los estudiantes interesados ​​pueden intentarlo, si se puede simular, definitivamente está en el JDK Se puede enviar un error a la comunidad.

Causado por las dependencias del cargador de clases

La dependencia del cargador de clases mencionada aquí no significa la relación de dependencia establecida por el padre en el ClassLoader. Si se trata de esta relación, en realidad se puede analizar mediante herramientas como mat o zprofiler, pero aún existe una situación. , Esas herramientas no se pueden analizar, esta relación se deriva del atributo _dependencies en ClassLoaderData, por ejemplo, si el atributo _dependencies del cargador de clase A registra el cargador de clase B, entonces cuando el GC atraviesa la clase A El cargador también atravesará el cargador de clase B y lo marcará en vivo, incluso si el cargador de clase B se puede reciclar, puede consultar el siguiente código

void ClassLoaderData::oops_do(OopClosure* f, KlassClosure* klass_closure, bool must_claim) {
  if (must_claim && !claim()) {
    return;
  }

  f->do_oop(&_class_loader);
  _dependencies.oops_do(f);
  _handles->oops_do(f);
  if (klass_closure != NULL) {
    classes_do(klass_closure);
  }
}
复制代码

Surge la pregunta, ¿cómo se registra esta dependencia? De hecho, nuestra demostración anterior simula esta situación. Puede echar un vistazo más de cerca. También describiré esta demostración. Por ejemplo, después de cargar AAA con TestLoader, un cargador de clases que carga AAA y crear objetos AAA, verá un El tipo es un atributo de AAB. En este momento, se analizará el tipo en el grupo constante. Cuando veamos el método loadClass de TestLoader, juzgamos. Si se trata de una carga de clase de tipo AAB, cree un nuevo cargador de clases El objeto se carga desde AAB.jar. Cuando la carga regresa, dicha capa de dependencia se registra en realidad en el jvm. Se considera que el cargador de clases AAA depende del cargador de clases AAB y se registra, pero mirando a todos El código de punto de acceso no tiene un lugar para limpiar esta relación de dependencia, es decir, mientras se establezca esta relación de dependencia, continuará hasta que se recicle el cargador de clases AAA y se recicle el cargador de clases AAB. Así que este es un cargador de clase pseudo-zombie. Aunque no depende de la relación de dependencia (como la acción de limpieza del atributo aab de AAA en la demostración), el GC siempre Se cree que tienen una relación de dependencia y continuarán existiendo durante un período de tiempo, dependiendo del cargador de clases AAA.

En vista de esta situación, personalmente creo que se necesita una estrategia de GC similar al recuento de referencias. Cuando ciertos cargadores de dos clases no tienen dependencias, se eliminarán de esta relación de dependencia. Se estima que hay muchos lugares para implementar dichos cambios. No es tan simple, por lo que el diseñador en ese momento probablemente no lo hizo debido a esto. Creo que esto es el resultado de un compromiso vago. Por supuesto, esto es solo mi suposición.

Estudiemos juntos :

Parámetros JVM del curso de la serie PerfMa KO [Memoria]

Diseñe bloqueos distribuidos de alta disponibilidad a nivel empresarial basados ​​en el modelo CAP

Supongo que te gusta

Origin juejin.im/post/5e9eb2d6e51d4547153d2834
Recomendado
Clasificación