Java 类加载出现死锁? 转 一、前言 二、原因分析 三、一起深入JVM,探个究竟

出处:Java 类加载还会死锁?这是什么情况?

先贴一份测试代码,大家可以先猜测一下,执行结果会是怎样的:

import java.util.concurrent.TimeUnit;

public class TestClassLoading {
    public static class A{
        static {
            System.out.println("class A init");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new B();
        }

        public static void test() {
            System.out.println("aaa");
        }
    }

    public static class B{
        static {
            System.out.println("class B init");
            new A();
        }

        public static void test() {
            System.out.println("bbb");
        }
    }
    public static void main(String[] args) {
        new Thread(() -> A.test()).start();
        new Thread(() -> B.test()).start();
    }
}

不知道,你猜对了没有呢,实际的执行结果会是下面这样的:

Java 类加载出现死锁? 转
一、前言
二、原因分析
三、一起深入JVM,探个究竟

二、原因分析

这里,一开始大家分析的是,和new有关系;但下面的代码和上面的结果完全一致,基本可以排除 new 的嫌疑:

public class TestClassLoadingNew {
    public static class A{
        static {
            System.out.println("class A init");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            B.test();
        }

        public static void test() {
            System.out.println("aaa");
        }
    }

    public static class B{
        static {
            System.out.println("class B init");
            A.test();
        }

        public static void test() {
            System.out.println("bbb");
        }
    }
    public static void main(String[] args) {
        new Thread(() -> A.test()).start();
        new Thread(() -> B.test()).start();
    }
}

这里,问题的根本原因,其实是:

classloader在初始化一个类的时候,会对当前类加锁,加锁后,再执行类的静态初始化块。

所以,上面会发生:

  1、线程1:类A对class A加锁,加锁后,执行类的静态初始化块(在堆栈里体现为<clinit>函数),发现用到了class B,于是去加载B;

  2、线程2:类B对class B加锁,加锁后,执行类的静态初始化块(在堆栈里体现为<clinit>函数),发现用到了class A,于是去加载A;

  3、死锁发生。

  有经验的同学,对于死锁是毫无畏惧的,因为我们有神器,jstack。 jstack 加上 -l 参数,即可打印出各个线程持有的锁的信息。(windows上直接jconsole就行,还能死锁检测):

"Thread-1" #15 prio=5 os_prio=0 tid=0x000000002178a000 nid=0x2df8 in Object.wait() [0x0000000021f4e000]
   java.lang.Thread.State: RUNNABLE
        at com.dmtest.netty_learn.TestClassLoading$B.<clinit>(TestClassLoading.java:32)
        at com.dmtest.netty_learn.TestClassLoading.lambda$main$1(TestClassLoading.java:42)
        at com.dmtest.netty_learn.TestClassLoading$$Lambda$2/736709391.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

"Thread-0" #14 prio=5 os_prio=0 tid=0x0000000021787800 nid=0x2618 in Object.wait() [0x00000000213be000]
   java.lang.Thread.State: RUNNABLE
        at com.dmtest.netty_learn.TestClassLoading$A.<clinit>(TestClassLoading.java:21)
        at com.dmtest.netty_learn.TestClassLoading.lambda$main$0(TestClassLoading.java:41)
        at com.dmtest.netty_learn.TestClassLoading$$Lambda$1/611437735.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

这里,很奇怪的一个原因是,明明这两个线程发生了死锁,为什么没有显示呢?

因为,这是 jvm 内部加了锁,所以,jconsole、jstack都失效了。

三、一起深入JVM,探个究竟

1、单步跟踪

class 的加载都是由 classloader 来完成的,而且部分工作是在 jvm 层面完成,我们可以看到,在 java.lang.ClassLoader#defineClass1 的定义中:

Java 类加载出现死锁? 转
一、前言
二、原因分析
三、一起深入JVM,探个究竟

以上几个方法都是本地方法。

其实际的实现在:/home/ckl/openjdk-jdk8u/jdk/src/share/native/java/lang/ClassLoader.c,

JNIEXPORT jclass JNICALL
Java_java_lang_ClassLoader_defineClass1(JNIEnv *env,
                                        jobject loader,
                                        jstring name,
                                        jbyteArray data,
                                        jint offset,
                                        jint length,
                                        jobject pd,
                                        jstring source)
{
    jbyte *body;
    char *utfName;
    jclass result = 0;
    char buf[128];
    char* utfSource;
    char sourceBuf[1024];

    if (data == NULL) {
        JNU_ThrowNullPointerException(env, 0);
        return 0;
    }

    /* Work around 4153825. malloc crashes on Solaris when passed a
     * negative size.
     */
    if (length < 0) {
        JNU_ThrowArrayIndexOutOfBoundsException(env, 0);
        return 0;
    }

    body = (jbyte *)malloc(length);

    if (body == 0) {
        JNU_ThrowOutOfMemoryError(env, 0);
        return 0;
    }

    (*env)->GetByteArrayRegion(env, data, offset, length, body);

    if ((*env)->ExceptionOccurred(env))
        goto free_body;

    if (name != NULL) {
        utfName = getUTF(env, name, buf, sizeof(buf));
        if (utfName == NULL) {
            goto free_body;
        }
        VerifyFixClassname(utfName);
    } else {
        utfName = NULL;
    }

    if (source != NULL) {
        utfSource = getUTF(env, source, sourceBuf, sizeof(sourceBuf));
        if (utfSource == NULL) {
            goto free_utfName;
        }
    } else {
        utfSource = NULL;
    }
    result = JVM_DefineClassWithSource(env, utfName, loader, body, length, pd, utfSource);

    if (utfSource && utfSource != sourceBuf)
        free(utfSource);

 free_utfName:
    if (utfName && utfName != buf)
        free(utfName);

 free_body:
    free(body);
    return result;
}

大家可以跟着标红的代码,我们一起大概看一下,这个方法的实现在/home/ckl/openjdk-jdk8u/hotspot/src/share/vm/prims/jvm.cpp 中,

JVM_ENTRY(jclass, JVM_DefineClassWithSource(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source))
  JVMWrapper2("JVM_DefineClassWithSource %s", name);

  return jvm_define_class_common(env, name, loader, buf, len, pd, source, true, THREAD);
JVM_END

jvm_define_class_common 的实现,还是在  jvm.cpp 中,

// common code for JVM_DefineClass() and JVM_DefineClassWithSource()
// and JVM_DefineClassWithSourceCond()
static jclass jvm_define_class_common(JNIEnv *env, const char *name,
                                      jobject loader, const jbyte *buf,
                                      jsize len, jobject pd, const char *source,
                                      jboolean verify, TRAPS) {
  if (source == NULL)  source = "__JVM_DefineClass__";

  assert(THREAD->is_Java_thread(), "must be a JavaThread");
  JavaThread* jt = (JavaThread*) THREAD;

  PerfClassTraceTime vmtimer(ClassLoader::perf_define_appclass_time(),
                             ClassLoader::perf_define_appclass_selftime(),
                             ClassLoader::perf_define_appclasses(),
                             jt->get_thread_stat()->perf_recursion_counts_addr(),
                             jt->get_thread_stat()->perf_timers_addr(),
                             PerfClassTraceTime::DEFINE_CLASS);

  if (UsePerfData) {
    ClassLoader::perf_app_classfile_bytes_read()->inc(len);
  }

  // Since exceptions can be thrown, class initialization can take place
  // if name is NULL no check for class name in .class stream has to be made.
  TempNewSymbol class_name = NULL;
  if (name != NULL) {
    const int str_len = (int)strlen(name);
    if (str_len > Symbol::max_length()) {
      // It's impossible to create this class;  the name cannot fit
      // into the constant pool.
      THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name);
    }
    class_name = SymbolTable::new_symbol(name, str_len, CHECK_NULL);
  }

  ResourceMark rm(THREAD);
  ClassFileStream st((u1*) buf, len, (char *)source);
  Handle class_loader (THREAD, JNIHandles::resolve(loader));
  if (UsePerfData) {
    is_lock_held_by_thread(class_loader,
                           ClassLoader::sync_JVMDefineClassLockFreeCounter(),
                           THREAD);
  }
  Handle protection_domain (THREAD, JNIHandles::resolve(pd));
  Klass* k = SystemDictionary::resolve_from_stream(class_name, class_loader,
                                                     protection_domain, &st,
                                                     verify != 0,
                                                     CHECK_NULL);

  if (TraceClassResolution && k != NULL) {
    trace_class_resolution(k);
  }

  return (jclass) JNIHandles::make_local(env, k->java_mirror());
}

resolve_from_stream 的实现在 SystemDictionary 类中,下面我们看下:

Klass* SystemDictionary::resolve_from_stream(Symbol* class_name,
                                             Handle class_loader,
                                             Handle protection_domain,
                                             ClassFileStream* st,
                                             bool verify,
                                             TRAPS) {

  // Classloaders that support parallelism, e.g. bootstrap classloader,
  // or all classloaders with UnsyncloadClass do not acquire lock here
  bool DoObjectLock = true;
  if (is_parallelCapable(class_loader)) {
    DoObjectLock = false;
  }

  ClassLoaderData* loader_data = register_loader(class_loader, CHECK_NULL);

  // Make sure we are synchronized on the class loader before we proceed
  Handle lockObject = compute_loader_lock_object(class_loader, THREAD);
  check_loader_lock_contention(lockObject, THREAD);
  ObjectLocker ol(lockObject, THREAD, DoObjectLock);

  TempNewSymbol parsed_name = NULL;

  // Parse the stream. Note that we do this even though this klass might
  // already be present in the SystemDictionary, otherwise we would not
  // throw potential ClassFormatErrors.
  //
  // Note: "name" is updated.

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

  const char* pkg = "java/";
  size_t pkglen = strlen(pkg);
  if (!HAS_PENDING_EXCEPTION &&
      !class_loader.is_null() &&
      parsed_name != NULL &&
      parsed_name->utf8_length() >= (int)pkglen &&
      !strncmp((const char*)parsed_name->bytes(), pkg, pkglen)) {
    // It is illegal to define classes in the "java." package from
    // JVM_DefineClass or jni_DefineClass unless you're the bootclassloader
    ResourceMark rm(THREAD);
    char* name = parsed_name->as_C_string();
    char* index = strrchr(name, '/');
    assert(index != NULL, "must be");
    *index = '