Java中的 ClassLoader
铺垫
ClassLoader就是类加载器。类加载子系统的主要作用是通过多种类加载器来查找和加载Class文件到Java虚拟机当中。
类别
-
java中的类加载器有两种类型:系统类加载器和自定义类加载器。
系统类加载器有三种,分别是Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader。
-
Bootstrap ClassLoader(引导类加载器)
用于加载指定的JDK的核心类库,比如说java.lang.、java.uti. 这些系统类。Java虚拟机的启动就是通过Bpptstrap ClassLoader创建一个初始类来完成的。
它用来加载以下目录中的类库:
- $JAVA_HOME/jre/lib目录。
- -Xbootclasspath参数指定的目录。
特点:
使用C/C++语言实现,不能被Java代码访问到
并不是继承自java.lang.ClassLoader
可以通过打印
System.getProperty("sun.boot.class.path")
来获取引导类加载器所加载的目录,可以发现基本上都是$JAVA_HOME/jre/lib目录中的jar包。 -
Extensions ClassLoader(拓展类加载器)
可以简称为ExtClassLoader,用于加载Java中的拓展类,提供除了系统类之外的额外功能。
ExtClassLoader用来加载以下目录中的类库:
- 加载$JAVA_HOME/jre/lib/ext目录。
- 系统属性java.ext.dir所指定的目录。
-
Application ClassLoader(应用程序类加载器)
可以简称为AppClassLoader,同时也可以称作System ClassLoader(系统类加载器),这是因为AppClassLoader可以通过ClassLoader的getSystemClassLoader方法获取到。它用来加载以下目录中的类库:
- 当前程序的ClassPath目录
- 系统属性java.class.path指定的目录
-
Custom ClassLoader(自定义类加载器)
通过继承java.lang.ClassLoader类的方式来实现自己的类加载器。Extensions ClassLoader和App ClassLoader也是继承自java.lang.ClassLoader类。
继承关系
ClassLoader的父子关系并不是使用继承来实现的,而是使用组合来实现代码复用的。
ClassLoader的加载过程
双亲委托模式
类加载器的流程是首先查找class文件,然后再把文件加载到虚拟机中;双亲委托模式是首先判断该class是否已经加载,如果没有加载,那么不是自身去查找,而是委托给父加载器进行查找,也就是按照custom-app(application/system)-ext(extensions)-bootstrap的顺序依次向上委托,委托给最顶层的Bootstrap之后,如果它找到了该class,就会直接返回,如果没找到,则继续依次向下查找;如果还没找到则最后会交由自身去查找。
-
为什么要使用双亲委托这种模型呢?
- 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是直接读取已经加载的Class。
- 更加安全,如果不使用这种委托模式,那我们就可以随时使用自定义的String类来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
JVM在搜索类的时候,是如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。
Android中的ClassLoader
区别
- java中的ClassLoader本质上是加载Class文件,但是这一点在Android中并非如此,因为无论是DVM还是ART,它们加载的都不再是Class文件,而是dex文件,这就需要重新设计ClassLoader相关类。
- dex文件:Android程序一般是使用Java语言开发的,但是Dalvik虚拟机并不支持直接执行JAVA字节码,所以会对编译生成的.class文件进行翻译、重构、解释、压缩等处理,这个处理过程是由dx进行处理,处理完成后生成的产物会以.dex结尾,称为Dex文件。
类别
- 系统类加载器
- BootClassLoader
- PathClassLoader
- DexClassLoader
- 自定义加载器
1. BootClassLoader
class BootClassLoader extends ClassLoader {
private static BootClassLoader instance;
public static synchronized BootClassLoader getInstance() {
if (instance == null) {
instance = new BootClassLoader();
}
return instance;
}
public BootClassLoader() {
super(null, true);
} }
Android系统启动的时候会使用BootClassLoader来预加载常用类,但是它和SDK中的Bootstrap ClassLoader不同,它并不是由C/C++代码实现的,而是由java实现的,BootClassLoader是ClassLoader的内部类,并继承自ClassLoader。BootClassLoader是一个单例类。
2. DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
可以加载dex文件以及包含dex的压缩文件(apk和jar文件),不管加载那种,最终都要加载dex文件。
DexClassLoader只是简单的封装了BaseDexClassLoader对象,没有复写父类的任何方法。
其构造方法包括4个参数:
- dexPath: dex相关文件路径集合,多个路径用文件分隔符分隔,默认文件分隔符为“:”。
- optimizedDirectory: 解压的dex文件存储路径
- 包含C/C++库的路径集合
- 父加载器
3. PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
用来加载系统类和应用程序的类;
其构造方法中没有参数optimizedDirectory,这是因为PathClassLoader已经默认了这个参数的值是当前应用程序的私有路径、内部存储路径:/data/dalvik-cache, 所以它通常是用来加载已经安装的apk的dex文件的(安装的apk的dex文件会存储在/data/dalvik-cache中。
-
dexPath
: 包含 dex 的 jar 文件或 apk 文件的路径集,多个以文件分隔符分隔,默认是“:” -
libraryPath
: 包含 C/C++ 库的路径集,多个同样以文件分隔符分隔,可以为空
4. ClassLoader
public abstract class ClassLoader {
private ClassLoader parent; //记录父类加载器
protected ClassLoader() {
this(getSystemClassLoader(), false);
}
protected ClassLoader(ClassLoader parentLoader) {
this(parentLoader, false);
}
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
//父类的类加载器为空,则抛出异常
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
return new PathClassLoader(classPath, BootClassLoader.getInstance());
}
继承关系
- ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。BootClassLoader是它的内部类,在Android系统启动的时候用于预加载常用类。
- SecureClassLoader类和JDK8中的同名类的代码是一样的,它继承自抽象类ClassLoader。但是它并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
- URLClassLoader类和JDK8中的同名类的代码是一样的,他继承自Secure ClassLoader,用来通过URL路径从jar文件和文件夹中加载类和资源。
- InMemoryDexClassLoader是Android 8.0新增的类加载器,继承自BaseDexClassLoader,用于加载内存中的dex文件。
- BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader、DexClassLoader和InMemoryDexClassLoader都继承自它。
ClassLoader的加载过程
Android的ClassLoader同样遵循了双亲委托模式,抽象类ClassLoader中定义了ClassLoader的加载方法loadClass,如下所示:
public abstract class ClassLoader {
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//判断当前类加载器是否已经加载过指定类,若已加载则直接返回
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
try{
if(parent!=null){
//如果没有加载过,则调用parent的类加载递归加载该类,若已加载则直接返回
clazz = parent.loadClass(className, false);
}
else{
clazz=findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){}
if (clazz == null) {
//还没加载,则调用当前类加载器来加载
clazz = findClass(className);
}
}
return clazz;
}
}
大致流程是首先通过findLoadedClass() 判断当前类加载器是否已经加载过指定类,如果已经加载就返回该类,如果没有加载就判断有没有父加载器,然后调用父加载器的loadClass方法,如果不存在父加载器就调用findBootstrapClassOrNull方法,这个方法会直接返回null。如果最后还是没找到,那么说明向上委托流程没有检查出类已经被加载,会调用当前的类加载器来进行加载。
总结一下:
- 判断当前类加载器是否已经加载过指定类,若已加载则直接返回,否则继续执行。
- 调用parent的类加载递归加载该类,检测是否加载,若已加载则直接返回,否则继续执行。
- 调用当前类加载器,通过findClass加载。
findClass方法如下所示:
protected Class<?> findClass(String name)throws ClassNotFoundException{
throw ClassNotFoundException(name);
}
在findClass方法中直接抛出了异常,这说明findClass方法需要子类来实现,BaseDexClassLoader(是ClassLoader的具体实现类)的代码如下所示:
...
public BaseDexClassLoader(String dexPath,String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
...
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions); //1
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;
}
...
}
在BaseDexClassLoader的构造方法中创建了DexPathList,在注释1处调用了DexPathList的findClass方法:
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
在注释1处遍历Element数组dexElements,在注释2处调用Elements的findClass方法,其中Element是DexPathList的静态内部类:
/*package*/ static class Element {
private final File path;
private final Boolean pathIsDirectory;
private final DexFile dexFile;
private ClassPathURLStreamHandler urlHandler;
private boolean initialized;
public Element(DexFile dexFile, File dexZipPath) {
this.dexFile = dexFile;
this.path = dexZipPath;
}
public Element(DexFile dexFile) {
this(dexFile, null);
}
public Element(File path) {
this(null, path);
}
...
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;//1
}
从Element的构造方法可以看出,其内部封装了DexFile,它用于加载dex。
在注释1处如果DexFile不为空,就调用DexFile的loadClassBinaryName方法:
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
在loadClassBinaryName方法中调用了DexFile的defineClass方法:
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);//1
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
在注释1处又调用了defineClassNative方法来记载dex相关文件,这个方法是Native方法。
static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,
jobject cookie) {
std::unique_ptr<std::vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
if (dex_files.get() == nullptr) {
return nullptr; //dex文件为空, 则直接返回
}
ScopedUtfChars class_name(env, javaName);
if (class_name.c_str() == nullptr) {
return nullptr; //类名为空, 则直接返回
}
const std::string descriptor(DotToDescriptor(class_name.c_str()));
const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str())); //将类名转换为hash码
for (auto& dex_file : *dex_files) {
const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str(), hash);
if (dex_class_def != nullptr) {
ScopedObjectAccess soa(env);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
class_linker->RegisterDexFile(*dex_file);
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
//获取目标类
mirror::Class* result = class_linker->DefineClass(soa.Self(), descriptor.c_str(), hash,
class_loader, *dex_file, *dex_class_def);
if (result != nullptr) {
// 找到目标对象
return soa.AddLocalReference<jclass>(result);
}
}
}
return nullptr; //没有找到目标类
}
其主要内容是创建目标类的对象并添加到虚拟机列表。
总结:ClassLoader的加载过程就是遵循着双亲委托模式,如果委托流程没有检查到此前加载过传入的类,就调用当前类加载器的findClass方法,Java层最终会调用DexFile的defineClassNative方法来执行查找流程。