JVM源码阅读-Dalvik类的加载
前言本文主要研究Android dalvik虚拟机加载类的流程和机制。目的是了解Android中DEX文件结构,虚拟机如何从DEX文件中加载一个Java Class,以及到最终如何初始化这个类直至可被正常使用。[Java]类的加载在Java的世界里,所有类的加载,都由 java.lang.ClassLoader 来负责。ClassLoader是一个抽象类,它有多个实现类,
前言
本文主要研究Android dalvik虚拟机加载类的流程和机制。目的是了解Android中DEX文件结构,虚拟机如何从DEX文件中加载一个Java Class,以及到最终如何初始化这个类直至可被正常使用。
[Java]类的加载
在Java的世界里,所有类的加载,都由 java.lang.ClassLoader
来负责。ClassLoader是一个抽象类,它有多个实现类,例如 BootClassLoader
, SystemClassLoader
以及虚拟机的具体实现,例如在dalvik虚拟机里的实现为 DexClassLoader
。
需要注意的每个虚拟机对于类的加载的逻辑并不十分相同,例如hotspot虚拟机和dalvik虚拟机加载一个类的过程基本上完全不同,hotspot主要是从class文件从加载类,而dalvik是从dex文件里去加载一个类,所以这里只讨论的是dalvik虚拟机里的实现机制。
双亲委派机制
不管虚拟机的具体实现,但虚拟机spec定义的关于类的加载规范必须被实现,例如最基础的双亲委派机制。
它规定每个ClassLoader都得有一个父亲ClassLoader,以此形成一个父子的多层级关系,利用这个层级关系实现了类的双亲委派机制。
在dalvik里的ClassLoader层级关系如下:
-
Bootstrap ClassLoader
-
System ClassLoader
-
Dex ClassLoader
在任何一个ClassLoader加载一个类的时候,都会先委托其父ClassLoader来负责加载这个类,一直递归到最顶层的ClassLoader,这个设计主要应该是为了安全考虑。以保障上层的类被上层的ClassLoader来加载,而避免系统类被下层的ClassLoader给替换掉了,而引发安全问题。
例如 DexClassLoader
加载类的时候,会先委托 SystemClassLoader
来加载该类,而 SystemClassLoader
又会先让它的父亲 BootClassLoader
先来加载,如果其所有祖父们都不加载该类,才会由这个ClassLoader去加载。
具体的代码实现为:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 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 } } return c; }
源码来至 : /java/lang/ClassLoader.java
源码简单解读:
- 先检查这个类是否被加载过,如果已经被加载过,则直接返回,不重新加载该类。(注意这里已经加过的类的列表并没有存储到Java层面,而是直接去问native层这个类是否被加载过,它维护了所有加载过的类)
- 递归委派父ClassLoader去加载。
- 如果所有父ClassLoader都不加载,自己才有权利去加载该类。
- 先去找到该Class(具体的findClass逻辑由子类实现)
类加载器(ClassLoader)
ClassLoader作为基类,关键的方法都交给子类具体去实现了,但它定义了类的加载过程:
load -> find -> define -> resolve
Class<?> loadClass(String name, boolean resolve); Class<?> findClass(String name); Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain); void resolveClass(Class<?> c);
SystemClassLoader
SystemClassLoader是一个ClassLoader默认的parent,即在创建一个ClassLoader时,不传入parent参数,则默认会使用这个SystemClassLoader作为其parent。
它其实是ClassLoader的一个静态内部类,里面包含了一个默认的ClassLoader,也是由ClassLoader的静态方法来创建的:
static private class SystemClassLoader { public static ClassLoader loader = ClassLoader.createSystemClassLoader(); } /** * Encapsulates the set of parallel capable loader types. */ private static ClassLoader createSystemClassLoader() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); } public static ClassLoader getSystemClassLoader() { return SystemClassLoader.loader; }
由代码可以看出 SystemClassLoader其实是一个全局静态的单例类,并且它的parent为 BootClassLoader。
BootClassLoader
BootClassLoader也定义在ClassLoader类,但外部不能访问到。也是一个单例类。它是ClassLoader的子类。它是唯一一个没有parent的ClassLoader。
class BootClassLoader extends ClassLoader { private static BootClassLoader instance; ("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED") public static synchronized BootClassLoader getInstance() { if (instance == null) { instance = new BootClassLoader(); } return instance; } public BootClassLoader() { super(null); } // ... }
其中loadClass里,先查找已经加载的类,由以下方法来实现,该方法是native方法 :
VMClassLoader#findLoadedClass(ClassLoader cl, String name);
然后findClass是由 Class.classForName
来实现的,也是一个native方法。
[Android]类的加载
加载Dex(DexClassLoader)
在dalvik里面的ClassLoader主要是由 DexClassLoader
和 PathClassLoader
来完成,这两个类属于 android/platform/libcore
项目,源码可查看AOSP:
https://android.googlesource.com/platform/libcore/+/master/dalvik/src/main/java/dalvik/system/
DexClassLoader 继承 dalvik.system.BaseDexClassLoader
,只提供一个构造器,并没有实现代码,主要代码还是在父类里面。
我们现在看 BaseDexClassLoader
,它继承于 ClassLoader. 先来看它的构造器:
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null); }
这里需要传四个参数,也基本能看出这个类的主要结构。
- dexPath: 这个参数的名字虽然是一个path结尾的,但不是传一个目录,而是传
.dex
.zip
,.apk
,.jar
的文件绝对路径,但可以一次传多个,用:
作为分隔即可。 - optimizedDirectory: 这个目录是dex的优化目录,必须是当前用户,并且可写.(也可以为
null
,系统会使用默认目录,一般是 /data/app/dalvik-cache) - librarySearchPath: native库的搜索目录,可以用
:
作为分隔符传入多个目录,也可以传入null
. - parent: parent ClassLoader
这里面关键的参数都传给了 DexPathList
对象了,可见大多数逻辑都会交给它来处理。
findClass
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; }
如推测的一样,具体的逻辑都交给 DexPathList 去实现了。那么接下来我们就来研究它。需要注意的是 BaseDexClassLoader
只重写了 findClass
这个方法,而没有重写 loadClass
, defineClass
,resolveClass
!
Dex列表(DexPathList)
这个类时传入的 dexPath 的抽象,因为dexPath可能会传入用 :
分隔的多个apk文件,而每个apk文件中又可能有多个dex文件,因此 DexPathList 包含了所有apk文件里面的所有dex文件的封装。并将每个Dex文件抽象成 DexFile
对象,包裹在 DexPathList#Element
列表中。
private Element[] dexElements;
在处理dexPath的时候,首先从 :
分隔符split成数组,然后遍历这些文件,将其解析成 DexFile
对象,并封装到 Element 中。
for (File file : files) { // ... if (name.endsWith(DEX_SUFFIX)) { // .dex DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements); if (dex != null) { elements[elementsPos++] = new Element(dex, null); } } else { // .zip .jar // ... DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements); // ... } // ... }
在调用 findClass
的时候,会遍历这些DexFile文件,从Dex中寻找具体的class:
for (Element element : dexElements) { Class<?> clazz = element.findClass(name, definingContext, suppressed); if (clazz != null) { return clazz; } }
除了传入一个dexPath,还可以传入一个 ByteBuffer
数组,每个byteBuffer里面包含了一个dex文件的字节流。并且 optimizedDirectory
参数是可以为 NULL
的。
除了dex文件,DexPathList还负责管理所有的native库,并将其也维护在一个列表中:
/** List of native library path elements. */ private final NativeLibraryElement[] nativeLibraryPathElements;
nativeLibrary的searchPath可以是一个普通路径,也可以是一个zip的路径。
当查找一个library的时候,会去从这些目录下寻找文件,例如:findLibrary(String filename) ,那么会去遍历这些目录,直到找到 path/filename 存在(并可读)的时候,则返回该library文件(如so文件)
寻找Class文件(DexFile)
dalvik.system.DexFile
这个类是对 Dex文件的抽象,具体在Dex文件中寻找Class定义的工作,则是由这个类来处理。
首先它会将dex文件打开,并读取成VM cookie object对象(具体的读取dex逻辑是由native方法实现)。
public Class loadClass(String name, ClassLoader loader) { String slashName = name.replace('.', '/'); return loadClassBinaryName(slashName, loader, null); } private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile) throws ClassNotFoundException, NoClassDefFoundError;
至于真正的 loadClass()
逻辑其实还是在defineClassNative()
这个native方法里完成的。
到此为止,Java层的关于关于class的加载逻辑基本已经了解,具体的很多工作都是native层是去完成,因此接下来我们来研究native层的。
[c++]类的加载
读取DEX文件(openDexFile)
这里我们接着上面 DexFile 的 native 方法来看,c++的源码在:
\dalvik2\vm\native\dalvik_system_DexFile.cpp
首先在native层会将dex文件打开并映射到内存中:
static void Dalvik_dalvik_system_DexFile_openDexFile(const u4* args, JValue* pResult) { // ... DexOrJar* pDexOrJar = NULL; // ... if (hasDexExtension(sourceName) && dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) { ALOGV("Opening DEX file '%s' (DEX)", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = true; pDexOrJar->pRawDexFile = pRawDexFile; pDexOrJar->pDexMemory = NULL; } // ... RETURN_PTR(pDexOrJar); }
读取dex文件的逻辑在 dvmRawDexFileOpen
函数中,它会去读取dex文件,并将其内部数据映射到一块只读的共享内存中去。具体负责内存映射的逻辑在 dexFileParse
函数中。
加载类的流程
首先来看,虚拟机对于一个类的加载流程,分为如下几个状态,从中大概能看到整个流程都经过了什么。
类的加载状态 ClassStatus :
name | value | note |
---|---|---|
CLASS_ERROR | -1 | |
CLASS_NOTREADY | 0 | |
CLASS_IDX | 1 | loaded, DEX idx in super or interfaces |
CLASS_LOADED | 2 | DEX idx values resolved |
CLASS_RESOLVED | 3 | part of linking |
CLASS_VERIFYING | 4 | in the process of being verified |
CLASS_VERIFIED | 5 | logically part of linking, done pre-init |
CLASS_INITIALIZING | 6 | class init in process |
CLASS_INITIALIZED | 7 | ready to go |
这个函数比较关键,它主要负责在dex文件去中查找和加载class,但也比较长,因此在此做省略操作,只保留关键部分,我们从中提取关键点进行分析:
static void Dalvik_dalvik_system_DexFile_defineClass(const u4* args, JValue* pResult) { // ... if (pDexOrJar->isDex) pDvmDex = dvmGetRawDexFileDex(pDexOrJar->pRawDexFile); else pDvmDex = dvmGetJarFileDex(pDexOrJar->pJarFile); // ... clazz = dvmDefineClass(pDvmDex, descriptor, loader); // ... }
这个函数前面做了一些转换工作就不分析了,首先它会去拿到可以映射到内存的dex文件,关键的一句在于调用 dvmDefineClass
函数,该函数会从dex里去查找和加载class。
该函数定义在 dalvik2/vm/oo/Class.cpp
文件中,具体的函数为:
static ClassObject* findClassNoInit(const char* descriptor, Object* loader, DvmDex* pDvmDex) { // 在已经加载过的类中去寻找,如果已经加载过,则不用再进行加载了 clazz = dvmLookupClass(descriptor, loader, true); if (clazz == null) { // 在Dex文件中寻找Class定义 pClassDef = dexFindClass(pDvmDex->pDexFile, descriptor); // 找到了Class的定义后,将其加载成ClassObject对象 ClassObject* clazz = loadClassFromDex(pDvmDex, pClassDef, loader); // ... // 记录该类到已加载过类的hash table中,便于下次检查 dvmAddClassToHash(clazz); // ... // 链接这个Class dvmLinkClass(clazz); } }
这个函数则是最为核心的逻辑,我们详细来分析:
首先,查找已经加载过的类,所有加载过的类都会将其classDescriptor(类的描述,类似于 Ljava.lang.Object;
这种字符串),将这个类的描述符进行hash,并作为加载的ClassObject的键丢到一个hash table里面去,每次加载一个类之前,都会先去这个hash table里面去找一下,看之前有没有加载过该类,如果加载过则不重复加载,直接从这个hash table里面返回ClassObject对象。否则才会去加载该类。
第一步:寻找
如果这个类从来没有加载过,则从Dex文件中去寻找Class的定义,这个过程是交给 DexFile.cpp
源码里的 dexFindClass
函数去完成的。
这个类也比较关键,也不太好理解,因为贴出完整的代码,我们仔细分析:
/* * 通过类描述符查找一个类的定义 * * 类描述符"descriptor"应该例如:"Landroid/debug/Stuff;". */ const DexClassDef* dexFindClass(const DexFile* pDexFile, const char* descriptor) { const DexClassLookup* pLookup = pDexFile->pClassLookup; u4 hash; int idx, mask; hash = classDescriptorHash(descriptor); mask = pLookup->numEntries - 1; idx = hash & mask; /* * 遍历DexClassLookup,直到找到Class的定义 */ while (true) { int offset; offset = pLookup->table[idx].classDescriptorOffset; if (offset == 0) return NULL; // 先比对类描述符的hash值 if (pLookup->table[idx].classDescriptorHash == hash) { const char* str; str = (const char*) (pDexFile->baseAddr + offset); // hash值匹配后,再比对类描述符的字符串 if (strcmp(str, descriptor) == 0) { // 找到匹配指定描述符的类 return (const DexClassDef*) (pDexFile->baseAddr + pLookup->table[idx].classDefOffset); } } idx = (idx + 1) & mask; } }
该函数主要描述了如何从一个dex文件中去寻找一个类的定义。
第二步:装载
根据找到的类的定义ClassDef,加载这个类的信息,去dex文件中去创建一个 ClassObject 对象,并将其存在已经加载过的类的hash table里面,下次find这个class的时候,就不会重复去加载这个class了,直接从hash table里面拿。
这个过程由 loadClassFromDex0
函数负责:
/* * Helper for loadClassFromDex, which takes a DexClassDataHeader and * encoded data pointer in addition to the other arguments. */ static ClassObject* loadClassFromDex0(DvmDex* pDvmDex, const DexClassDef* pClassDef, const DexClassDataHeader* pHeader, const u1* pEncodedData, Object* classLoader) { newClass = (ClassObject*) dvmMalloc(size, ALLOC_NON_MOVING); dvmSetClassSerialNumber(newClass); // 类的签名 newClass->descriptor = descriptor; // 类的状态 -> Loaded newClass->status = CLASS_IDX; // 父类 newClass->super = (ClassObject*) pClassDef->superclassIdx; // 接口列表 pInterfacesList = dexGetInterfacesList(pDexFile, pClassDef); // 静态变量(并设置为默认值0或null) newClass->sfieldCount = count; for (i = 0; i < count; i++) { dexReadClassDataField(&pEncodedData, &field, &lastIndex); loadSFieldFromDex(newClass, &field, &newClass->sfields[i]); } // 成员变量 for (i = 0; i < count; i++) { dexReadClassDataField(&pEncodedData, &field, &lastIndex); loadIFieldFromDex(newClass, &field, &newClass->ifields[i]); } dvmLinearReadOnly(classLoader, newClass->ifields); // 成员方法 newClass->directMethodCount = count; newClass->directMethods = (Method*) dvmLinearAlloc(classLoader, count * sizeof(Method)); for (i = 0; i < count; i++) { dexReadClassDataMethod(&pEncodedData, &method, &lastIndex); loadMethodFromDex(newClass, &method, &newClass->directMethods[i]); } dvmLinearReadOnly(classLoader, newClass->directMethods); // 虚方法(父类方法) newClass->virtualMethodCount = count; newClass->virtualMethods = (Method*) dvmLinearAlloc(classLoader, count * sizeof(Method)); for (i = 0; i < count; i++) { dexReadClassDataMethod(&pEncodedData, &method, &lastIndex); loadMethodFromDex(newClass, &method, &newClass->virtualMethods[i]); } dvmLinearReadOnly(classLoader, newClass->virtualMethods); // 字节码 newClass->sourceFile = dexGetSourceFile(pDexFile, pClassDef); }
注意这里的classLoader就是用来加载类的,一般都是java层传过来的。如果loader是null的话,则会去加载系统class,例如 java.lang.Class
类。
这里会在 heap 里分配一块内存(使用 dvmMalloc 函数),来创建一个 ClassObject 对象,用于放置类的信息。包括:
- descriptor 类的签名
- 类的字段对象 fieldObject
- 类的状态为:CLASS_IDX
- 类的父类
- 类的接口列表
- 类的静态成员变量信息(设置标准默认值:null和0)
- 类的成员变量信息
- 类的方法
- 类的虚方法(父类方法)
- 类的字节码
第三步:链接
bool dvmLinkClass(ClassObject* clazz) { }
link的过程又分为:
- 加载 LOADED,父类和接口都为NULL
- 链接 RESOLVED,将父类和接口的dex idx链接替换成真正的引用
- 验证 VERIFIED,检查一个类的,例如:
- 如果该类是 java.lang.Object,则不能再有父类
- 如果不是java.lang.Object, 则必须有父类
- 不能继承final类
- 不能继承interface
- 只能继承public的类或同一个包下的类
- 等等
所有这些逻辑都在 java_lang_Class.cpp里的:
第四步:初始化
bool dvmInitClass(CLassObject* clazz) { }
初始化之前,必须确保该类已经被验证过(VERIFIED)如果没有,则立即先验证它。
如果没有优化过类,则先优化类(optimize),
然后做一些验证工作,和线程安全的操作(因为可能多个线程同时引发初始化某个类,所以会使用当前类锁对初始化过程加锁)。
接下来就开始初始化过程:
- 先递归的初始化父类 super class,它的父类的父类,直到它们都先被初始化了。
- 初始化静态常量 static final,它会从dex里面将静态变量的值拿出去(根据偏移量),赋值到类的静态变量上。
- 执行静态区的代码 static {},所有写在静态区的代码都会合并到静态方法里面,该方法的名字为
<clinit>
,签名为()V
。它被当做一个正常的静态方法被调用。dvmCallMethod()
- 到此,如果没有遇到异常,则该类被视为初始化完毕,可以正常使用,状态也变为
CLASS_INITIALIZED
.
DexFile文件结构
我们对虚拟机加载一个类的整个过程基本有了一定了解,因为在加载的过程中,基本都是在和 DexFile 文件在打交道,因此作为扩展知识,我们也顺便了解 DexFile 的文件结构。
首先要明确 Android 之所以使用 Dex 文件来代替Java里的Jar包文件,主要是为了解决在手机这种存储空间有限的设备上能更进一步的压缩空间的考虑。
传统的jar包文件,里面存储了一个个分散的class文件,而dex文件其实是将一个个分散的class文件合并成一个文件。
因此dex的文件结构基本上和class的文件结构非常相关或相似,所有先来看见看class文件的结构:
一个class文件基本上可以分为几块内容:
- 基本信息(如magic,minor version, major version, access flags, this class, super class等信息)
- 常量池 Constant Pool,它包含了所有基本类型,string,类名,字段名,方法名,类型等常量
- 接口列表 Interfaces ,它是该类的所有实现接口的列表,包含了所有接口的class,其中className也是指向常量池里的一个classInfo
- 字段列表 fields, 它包括该类所有字段的列表,每个字段包括三个信息:Name字段名,Descriptor字段类型签名,access flags访问权限(public/private/protected/static/final等)
- 方法列表 methods, 它包括该类的所有定义的方法的列表,每个方法包括:Name方法名,Descriptor方法签名,access flags访问权限,以及具体的字节码code.
接下来我们先来看 DexFile 的定义:
struct DexFile { /* directly-mapped "opt" header */ const DexOptHeader* pOptHeader; // DEX文件头指针,包含所有指针的偏移量和长度 const DexHeader* pHeader; // 字符串列表指针,UTF-16编码 const DexStringId* pStringIds; // 类型列表指针 const DexTypeId* pTypeIds; // 字段列表指针 const DexFieldId* pFieldIds; // 方法列表指针 const DexMethodId* pMethodIds; // 函数原型数据指针,方法声明的字符串,返回类型和参数列表 const DexProtoId* pProtoIds; // 类的定义列表指针,类的信息,包括接口,超类,类信息,静态变量偏移量等 const DexClassDef* pClassDefs; // 静态连接数据 const DexLink* pLinkData; /* * These are mapped out of the "auxillary" section, and may not be * included in the file. */ const DexClassLookup* pClassLookup; const void* pRegisterMapPool; // RegisterMapClassPool /* points to start of DEX file data */ const u1* baseAddr; /* track memory overhead for auxillary structures */ int overhead; /* additional app-specific data structures associated with the DEX */ //void* auxData; };
可以看出来基本一个Dex就是将多个Class的信息整合到一起,这样的好处是,例如常量池这些都可以共享,从而减少了整体的存储空间。
结语
看完dalvik虚拟机对于类的加载流程的相关源码以后,对一个类是如何被加载到虚拟机有了一个新的认知,并从中看到Android dalvik是如何实现Java虚拟机spec的等一些细节,和hotspot的实现还是有比较大的区别的,对Dex文件,Class文件的格式也有了一个更直观的了解。下一步需要了解的应该是虚拟机内存相关的部分。
源码索引:
java/lang/ClassLoader.java loadClass(findClass) defineClass resolveClass dalvik.system.BaseDexClassLoader.java findClass dalvik2/vm/oo/class.cpp loadMethodFromDex dalvik2/vm/native.cpp dvmResolveNativeMethod unregisterJNINativeMethods dalvik2/vm/jni.cpp dvmRegisterJNIMethod
参考资料:
- Dalvik2源码
- Dalvik libcore源码
- Dalvik类加载机制分析
- Android-Dex文件格式解析
- Dalvik虚拟机中DexClassLookup结构解析
- Android安全–从defineClassNative看类的加载过程
- Dex动态加载的C语言部分
- Android安全–Dex文件格式详解
更多推荐
所有评论(0)