Javaのクラスローダロック同期トラブルシューティングと修理

黒と白のテストフェーズホームスタートの確率で自分のアプリを担当(実際のパフォーマンスはANRである)による負荷の同期ロックのような最終的な位置決めの問題を発行することが立ち往生。問題が解決された後、我々は問題の本当の原因は、インとアウトは、私たちは真実を理解するのに役立ち考え出すされているものを見てください。ファイト次の出会いも同様の問題はすぐに反応し、問題を見つけることができます。


まず、問題のアプリます何このシーンを再現するために簡単なコードで、抽象発生しました:

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();
    }
}复制代码

その後、我々はコードを実行してこのコードの結果が何であるかを参照してください。





注ルック、私はログはっきりと見える、クラスの初期化クラスAとクラスBの最初の数字は完全ではありません、結果の2つのショットの合計を掲載しました。このプログラムは、デッドロック待ちを発生するクラスローディング段階に等しいです。セカンドショットの実装の結果は正常です。

問題の原因:

クラスが仮想マシンにロードされると、それは実際に行動クラスをロードしますロックされていた、いわゆるロックは、クラスを初期化するために同じ時間だけつのスレッドである(もちろん、異なるスレッドが同時に異なるクラスをロードできるようにすることができ、しかし、唯一のスレッドで)同じクラスのロード中。n個のスレッドが存在する場合、1つだけのスレッドを実現することが可能であり、残りのスレッドが待機することができます。

記事の先頭に戻る例を示します。

この時点ではスレッドのスレッド番号123負荷クラスA、クラスAを想定し、これはロックを加え、他のスレッドは、123クラスAに来ることはできませんこのスレッドコードは、この次に、クラスBを呼び出し中に発見時にロードスレッドクラスは、123の番号が付けられ、およびクラスBをロードするために行ってきました

、すでにあった前しかし、運がクラスBをロードするために第123号のスレッドで、非常に良いではありません456スレッド、その後、クラスBをロードするスレッド456件のスレッドの数は、読み込み時に、私は負荷クラスAクラスBに必要なことがわかりましたその結果、負荷クラスへの456クラスAもロックするために時間を見つけました。

だから、エンド123に456 Bを待っている456 123 Aの結果を待って、Aさんに自分のロードを完了するために、ロードされたBさんのロードを完了するために、ロードされた、あなたは私が最終的にデッドロックにつながる、あなたを待ってよ、私を待ちます。そして、実行能力の順で、この問題は、必ずしも今


質問がロックするので、処理の負荷クラスですので、その後、どのようにロックしますか?どこかに追加しました。この問題では、我々はソースコードの読み取りを読まなければなりません。私たちのクラスローダAはこれをプリントアウト:

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


复制代码




我々はにloadClassソースTieshanglaiをAppClassLoaderする場所、表示され続けます

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);
    }
}复制代码


最終的に、我々は実際のコースの負荷クラスを達成するために彼の親にあることがわかりました。




//这里实际上对于普通的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;
}复制代码



ここで権利をクリアするために来て、そこに同期ロックがありますが、問題の真実は、本当に遠くまだありますか?私はスクリーンショットをマークところここを見て、多くの赤い線があり、私たちはコードの前で実際にJVM内部を実行している、とないアンドロイドの、ここではアンドロイド、クラスローダJVMも標準内の仮想マシンで実行されませんでした。

我々は財布は、Androidの問題をシミュレートするJVMが、最終的に何の根本的なアンドロイドのロックはありませんが、どのように追加するために、我々はまだ見に追いかけます。




まず、通常のクラス(次のソースは、私はコードセクションでソースコードを囲み、この記事のオミット何も対象となります、長すぎることに注意してください)にロードされたAndroidのクラスローダを取得します

 */
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);
    }
}复制代码

私たちは、これはのloadClassクラスローダの動作ではありませんでしたことが判明した後のような父を見に行ってきました


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*/);
    }

  
}复制代码

アンドロイドロードされたクラスがないのloadClassでJVMを通して、findClassメソッドによって行われているようで、我々はにfindClassメソッドbasedexclassloaderを続け、最終的には、デックスは、我々はすべて知っている必要があり、DexPathListのクラスをロードするにfindClass方法によって達成されますアンドロイドコンパイル時にいくつかの特定の最適化により、クラスファイルをコンパイルし、JVMうとDEXファイルの本質の一つでパッケージ化され、最終的な結果にそれらをパッケージ化。DEXファイルは、多くのクラスファイルの集合体であると私たちは、単に理解することができます。

DexPathListのにfindClass方法を見てみましょう

 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;
    }复制代码


この要素は、実際にDexPathListある静的な内部クラスは、我々は彼の方法にfindClassの焦点を見て


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


だから、最終的には負荷クラスを完了するためにloadClassBinaryNameの方法のDexFileは、我々はDexFileコードを見て続行します。

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的产生,也可以给足时间让他人将错误的写法修改完毕。


おすすめ

転載: juejin.im/post/5df2dfbff265da33997a2d62