Java class loader lock synchronization troubleshoot and repair

Responsible for their own app in the testing phase Home Start probability of black and white (the actual performance is anr) stuck to issue final positioning problems like load synchronization lock caused. After the problem is resolved, we look at what the real cause of the problem is figuring out the ins and outs help us to understand the truth. Fight next encounter similar problems can react quickly find the problem.


First we will app in question occurred abstracted, with simple code to reproduce what this scene:

public class Test {

    public static class A {

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

        public static void test() {
            System.out.println("method test called in class A");
        }
    }

    public static class B {

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

        public static void test() {
            System.out.println("method test called in class B");
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                A.test();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                B.test();
            }
        }).start();
    }
}复制代码

Then we ran at the code and see what the results of this code is:





Note Look, I posted a total of two shots of the results, the first figure of the Log clearly seen, class initialization class A and class B are not complete. This program is equal to the class loading phase occurs deadlock wait. The results of the implementation of the second shot is normal.

Cause of the problem:

When the class is loaded into the virtual machine, it will actually load the behavior class were locked, so-called lock is at the same time only one thread to initialize a class (of course, can allow different threads simultaneously load different class, but only by a thread while loading the same class). If there are n threads, then then there is only one thread can be implemented, the remaining thread can wait.

Back to the beginning of the article is an example:

Assume thread thread number 123 load class A, class A at this time this was added a lock, no other thread can not come in, 123 class A loaded when found inside this thread code calls class B, then this thread class numbered 123, and went to load class B.

But luck is not very good, at No. 123 threads to load a class B before, there were already a number of threads 456 threads to load the class B, then thread 456 found that I need to load class A class B when loading the results 456 to load class a class a also found the time to be locked.

So in the end 123 is waiting for 456 B loaded to complete their loading on the A's, waiting for the results of 456 123 A loaded to complete the loading of the B's, you wait for me I'll wait for you, eventually leading to deadlock. And this problem with the order of execution capacity, not necessarily now


So the question is, the load class of the process since the lock, then lock in what? Somewhere added. With this issue, we have to read the source code reading. We classLoader A print out this:

System.out.println(A.class.getClassLoader());


复制代码




We continue to see, where we will AppClassLoader the loadClass source Tieshanglai

public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
    int var3 = var1.lastIndexOf(46);
    if (var3 != -1) {
        SecurityManager var4 = System.getSecurityManager();
        if (var4 != null) {
            var4.checkPackageAccess(var1.substring(0, var3));
        }
    }

    if (this.ucp.knownToNotExist(var1)) {
        Class var5 = this.findLoadedClass(var1);
        if (var5 != null) {
            if (var2) {
                this.resolveClass(var5);
            }

            return var5;
        } else {
            throw new ClassNotFoundException(var1);
        }
    } else {
        return super.loadClass(var1, var2);
    }
}复制代码


Ultimately, we found that the actual course load class is to his parent to accomplish:




//这里实际上对于普通的jvm应用来说,并没有遵循双亲委派的类加载模型,对于普通应用来说,class的加载都交给各自应用自己的classloader的loadclass方法来加载。比如那些著名的java后台服务器tomcat,jboss,jetty等,这些服务器可以装载n个不同的应用,每个应用
//都有自己的classloader,这样可以避免不同应用之间出现相同名字类的时候出现加载错乱的问题。
//所以针对我们上面的这个例子,以及百分之99网上针对此问题的例子最终类在加载的时候走的还是loadClass的方法,并不会走到下面的findclass去加载类。这点一定要注意。
//但是不管最终走的是findclass还是loadclass去加载,我们这里都能看出来,这里是有一个同步锁的,而且锁的对象是根据传入的类的名字来的。
//这就证明了两件事:1.jvm支持多线程同时加载不同的class,否则可以想到我们的应用会有多慢。2.jvm不支持多个线程同时加载同一个class。这里代码很简单不过多分析了。大家知道意思就好
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}




protected Object getClassLoadingLock(String className) {
    Object lock = this;
    if (parallelLockMap != null) {
        Object newLock = new Object();
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) {
            lock = newLock;
        }
    }
    return lock;
}复制代码



Here come in to clear the right, there are synchronization lock, but the truth of the matter really is far yet? Look here where I marked the screenshot, there are many red line, we are actually in front of the code is running jvm inside, and did not run in a virtual machine inside android, classloader jvm also standard here, not of android.

While we jvm simulate the problems we wallet android, but in the end there is no underlying android lock, how to add, we still chase into a look.




First we get classloader android loaded in ordinary classes (Note that the following source is too long, I enclose the source code in the code section and will be subject to omit nothing of this article)

 */
public class PathClassLoader extends BaseDexClassLoader {
  
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}复制代码

We found that this was not loadclass classloader behavior, then went to his father look like


public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if (reporter != null) {
            reportClassLoaderChain();
        }
    }
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

    /**
     * @hide
     */
    public void addDexPath(String dexPath) {
        pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
    }

  
}复制代码

Seem android loaded class are carried out by findclass, not through jvm in loadclass, then we continue with findclass method basedexclassloader is ultimately accomplished by loading classes findclass method of DexPathList, Dex should we all know, android at compile time will jvm compiled class files through some specific optimization and packaged them into a final result is packaged with one of dex files essence. We can simply understood as the dex file is a collection of many class files.

Take a look at findclass method of DexPathList

 public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }


        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }复制代码


The static inner classes this Element is actually a DexPathList, we look at the focus of his method findClass


 public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }复制代码


So ultimately DexFile of loadClassBinaryName way to complete a load classes, we continue to look at DexFile code:

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }


    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }


private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
                                                  DexFile dexFile)复制代码


最终我们发现android中加载一个类,最终是通过defineClassNative这个jni方法来完成的。也就是说起码在java层面,android的classloader并没有加锁,但是jvm中却是在java层面加了锁。所以我们猜想,既然android中也暴露出来了类加载的问题,所以android的类加载过程也是一定会有锁的,只是这个锁并不在java层面来完成,那么就只能在c++层面来完成了,so,这里我们继续跟,看看到底是不是在c++层面完成的加锁操作。

最终我们来到DexFile.cc 这个文件来看看我们c++代码 是怎么加载类的。

//注意看这里参数列表 可以明确看出来这是一个jni方法
static jclass DexFile_defineClassNative(JNIEnv* env,
                                        jclass,
                                        jstring javaName,
                                        jobject javaLoader,
                                        jobject cookie,
                                        jobject dexFile) {
  std::vector<const DexFile*> dex_files;
  const OatFile* oat_file;
  if (!ConvertJavaArrayToDexFiles(env, cookie, /*out*/ dex_files, /*out*/ oat_file)) {
    VLOG(class_linker) << "Failed to find dex_file";
    DCHECK(env->ExceptionCheck());
    return nullptr;
  }


  ScopedUtfChars class_name(env, javaName);
  if (class_name.c_str() == nullptr) {
    VLOG(class_linker) << "Failed to find class_name";
    return nullptr;
  }
  const std::string descriptor(DotToDescriptor(class_name.c_str()));
  const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
  for (auto& dex_file : dex_files) {
    const DexFile::ClassDef* dex_class_def =
        OatDexFile::FindClassDef(*dex_file, descriptor.c_str(), hash);
    if (dex_class_def != nullptr) {
      ScopedObjectAccess soa(env);
      ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(
          hs.NewHandle(soa.Decode<mirror::ClassLoader>(javaLoader)));
      ObjPtr<mirror::DexCache> dex_cache =
          class_linker->RegisterDexFile(*dex_file, class_loader.Get());
      if (dex_cache == nullptr) {
        // OOME or InternalError (dexFile already registered with a different class loader).
        soa.Self()->AssertPendingException();
        return nullptr;
      }
      ObjPtr<mirror::Class> result = class_linker->DefineClass(soa.Self(),
                                                               descriptor.c_str(),
                                                               hash,
                                                               class_loader,
                                                               *dex_file,
                                                               *dex_class_def);
      // Add the used dex file. This only required for the DexFile.loadClass API since normal
      // class loaders already keep their dex files live.
      class_linker->InsertDexFileInToClassLoader(soa.Decode<mirror::Object>(dexFile),
                                                 class_loader.Get());
      //其实这个result就是我们的class了,这里看出来我们通过上面class_linker的defineclass方法可以得到一个真正的class对象,然后在这里通过类型转换以后返回一个jni对象给java层
		if (result != nullptr) {
        VLOG(class_linker) << "DexFile_defineClassNative returning " << result
                           << " for " << class_name.c_str();
        return soa.AddLocalReference<jclass>(result);
      }
    }
  }
  VLOG(class_linker) << "Failed to find dex_class_def " << class_name.c_str();
  return nullptr;
}复制代码


最后我们来看看class_liner的DefineClass方法

//首先我们注意看他的参数,第一个参数就是传递的一个线程对象
mirror::Class* ClassLinker::DefineClass(Thread* self,
                                        const char* descriptor,
                                        size_t hash,
                                        Handle<mirror::ClassLoader> class_loader,
                                        const DexFile& dex_file,
                                        const DexFile::ClassDef& dex_class_def) {


//然后继续看关键代码:


//注意看这里就是一把锁,一旦有线程进来 那么只要锁没释放那么其余线程走到这里来就会被阻塞。
 ObjectLock<mirror::Class> lock(self, klass);
  klass->SetClinitThreadId(self->GetTid());
  // Make sure we have a valid empty iftable even if there are errors.
  klass->SetIfTable(GetClassRoot(kJavaLangObject)->GetIfTable());


  // Add the newly loaded class to the loaded classes table.
  ObjPtr<mirror::Class> existing = InsertClass(descriptor, klass.Get(), hash);
  if (existing != nullptr) {
    // We failed to insert because we raced with another thread. Calling EnsureResolved may cause
    // this thread to block.
    return EnsureResolved(self, descriptor, existing);
  }

  // Load the fields and other things after we are inserted in the table. This is so that we don't
  // end up allocating unfree-able linear alloc resources and then lose the race condition. The
  // other reason is that the field roots are only visited from the class table. So we need to be
  // inserted before we allocate / fill in these fields.
  //看名字 我们也能猜到这里是真正加载class对象的地方了
  LoadClass(self, *new_dex_file, *new_class_def, klass);




void ClassLinker::LoadClass(Thread* self,
                            const DexFile& dex_file,
                            const DexFile::ClassDef& dex_class_def,
                            Handle<mirror::Class> klass) {
  const uint8_t* class_data = dex_file.GetClassData(dex_class_def);
  if (class_data == nullptr) {
    return;  // no fields or methods - for example a marker interface
  }
  LoadClassMembers(self, dex_file, class_data, klass);
}
//所以最终我们是通过loadClassMembers这个方法来完成对类的加载的,其实这个方法里面就是把类加载的完整过程给走了一遍,其中当然包括我们的静态代码块的执行过程。
 而这个函数执行的最后一句话就是  self->AllowThreadSuspension()  ,也就是将锁释放掉。

复制代码


所以最终我们就得到了一个结论,对于类加载过程的锁机制来说,jvm是将这个锁放到了java层自己处理,而android则是放在了c层进行处理。虽然处理方式大相径庭,但还是保持了虚拟机的运行规则。产生问题以后的表现都是一致的。


最后对于android程序来讲,如果你的应用程序确实存在某些类的初始化过程被多线程调用且这些类的初始化过程还存在相互嵌套的情况,那么可以在程序的入口处,先将一个class手动初始化。例如我们可以在android的application的onCreate方法里面添加:

Class.forName("your class name ")复制代码

这样,优先在主线程里手动触发一个class的加载,则可以完美避开我们例子中的问题。相对应的钱包类似的问题也就迎刃而解了。毕竟一时半会我们要修改原来相互嵌套的逻辑也不是一件容易的事。用这种方法既可以避免bug的产生,也可以给足时间让他人将错误的写法修改完毕。


Guess you like

Origin juejin.im/post/5df2dfbff265da33997a2d62