【源码解读】Android虚拟机的启动到第一个java类的执行

// 本文涉及的源文件:
system/core/init/init.cpp
system/core/rootdir/init.rc
system/core/rootdir/init.zygote64.rc
framework/cmds/app_process/app_main.cpp
framework/core/jni/AndroidRuntime.cpp
libnativehelper/JniInvocation.cpp
dalvik2/vm/Jni.cpp
dalvik2/vm/init.cpp
dalvik2/vm/InitRefs.cpp
dalvik2/vm/oo/Class.cpp
dalvik2/vm/interp/Stack.cpp
dalvik2/vm/interp/Interp.cpp
dalvik2/vm/mterp/out/InterpC-portable.cpp
art/runtime/jni/java_vm_ext.cc

概要

本文主要从源码的角度,分析Android虚拟机是如何启动并创建java世界的,包括虚拟机的创建、第一个Java类的加载、代码的执行。
看下文之前,不妨先思考几个问题:

  1. android虚拟机是谁创建的?
  2. android虚拟机的java入口类是哪个?
  3. 虚拟机是如何找到入口类的?
  4. 一个类从加载到执行都有哪些步骤?
  5. 类静态方法块是何时开始执行的?

天字第一号进程init进程启动后,会解析init.rc中的脚本,然后按照特定顺序依次执行其中的命令。init.rc中引用了init.zygote64.rc,其中有条命令如下:

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server

这条命令会调用app_process64启动zygote。
app_process64对应的源文件为framework/cmds/app_process/app_main.cpp。

//framework/cmds/app_process/app_main.cpp
int main(int argc, char *argv[]) { 
  ... 
  AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
  ...
  if (zygote) {
     runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
  } else if (className) {
    runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
  }

创建AppRuntime,然后启动“com.android.internal.os.ZygoteInit”类。

//framework/core/jni/AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) {
  ...
  JniInvocation jni_invocation;
  jni_invocation.Init(NULL); //初始化jni调用
  JNIEnv* env;
   if (startVm(&mJavaVM, &env, zygote) != 0) { //启动虚拟机
       return;
   }
   onVmCreated(env);
  ...
  //找到要启动的java类,然后调用其main方法
  jclass startClass = env->FindClass(slashClassName);
  jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");
  if (startMeth == NULL) {
      ALOGE("JavaVM unable to find main() in '%s'\n", className);
  } else {
      env->CallStaticVoidMethod(startClass, startMeth, strArray);
  }
}

主要流程是:

  1. 初始化Jni调用
  2. 启动虚拟机
  3. 找到要启动的java类
  4. 执行main方法

android系统在4.4以前是dalvik虚拟机,之后增加了art虚拟机,为了对上层使用屏蔽虚拟机的不同实现,通过JniInvocation对其进行封装。dalvi和art都实现了同样的虚拟机接口,并编译成了动态链接库so,JNIInvocation.Init的作用就是根据参数选择一个so,然后找到其中创建虚拟机的方法。这其实就是面向对象中继承多态的思想。
JniInvocation.Init执行完,会给三个函数指针赋值,分别是

  • JNI_GetDefaultJavaVMInitArgs_
  • JNI_CreateJavaVM_
  • JNI_GetCreatedJavaVMs_

然后我们看一下dalvik虚拟机是如何实现JNI_CreateJavaVM的。

//dalvik2/vm/Jni.cpp
jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  ...
  JavaVMExt* pVM = (JavaVMExt*) calloc(1, sizeof(JavaVMExt));
  pVM->funcTable = &gInvokeInterface;
  pVM->envList = NULL;
  ...
  gDvmJni.jniVm = (JavaVM*) pVM;
  JNIEnvExt* pEnv = (JNIEnvExt*) dvmCreateJNIEnv(NULL);
  //启动虚拟机
  dvmStartup(argc, argv.get(), args->ignoreUnrecognized, (JNIEnv*)pEnv);
  *p_env = (JNIEnv*) pEnv;
  *p_vm = (JavaVM*) pVM;
}

主要就是创建虚拟机对象JavaVMExt和Jni环境对象JNIEnvExt,然后启动虚拟机。
创建的过程就是为其中各种函数指针赋值,比如FindClass、NewObject等,这些都是java语法的实现。
启动虚拟机比较复杂,有垃圾回收、native调用、类相关的初始化工作。。

//dalvik2/vm/init.cpp
dvmStartup(int argc, const char* const argv[],
        bool ignoreUnrecognized, JNIEnv* pEnv) {
    ...
  dvmAllocTrackerStartup();
  dvmGcStartup();
  dvmThreadStartup();
  dvmInlineNativeStartup;
  ...
  dvmClassStartup();
  dvmFindRequiredClassesAndMembers();
  dvmStringInternStartup();
  ..
}

上面的dvmClassStartup会创建class对象以及基础数据类型对象(Void、Boolean、Byte等)
dvmFindRequiredClassesAndMembers,调用initClassReferences,该方法会初始化一些java语言的一些基本类比如Object、Throwable、Class、ClassLoader、String,以及反射相关的类。初始化方法为:

static bool initClassReference(ClassObject** pClass, const char* name) {
    ClassObject* result;
   if (name[0] == '[') {
        result = dvmFindArrayClass(name, NULL);
    } else {
        result = dvmFindSystemClassNoInit(name);
    }
   ...
    *pClass = result;
    return true;
}

对于数组则调用dvmFindArrayClass,否则调用dvmFindSystemClassNoInit

//dalvik2/vm/oo/Class.cpp
ClassObject* dvmFindSystemClassNoInit(const char* descriptor)
{
    return findClassNoInit(descriptor, NULL, NULL);
}

static ClassObject* findClassNoInit(const char* descriptor, Object* loader,
    DvmDex* pDvmDex)
{
  clazz = dvmLookupClass(descriptor, loader, true);
  if (clazz == NULL) {
    if (pDvmDex == NULL) {
          pDvmDex = searchBootPathForClass(descriptor, &pClassDef);
      } else {
          pClassDef = dexFindClass(pDvmDex->pDexFile, descriptor);
      }
     ...
     clazz = loadClassFromDex(pDvmDex, pClassDef, loader);
    ...
    dvmAddClassToHash(clazz);
    ...
    dvmLinkClass(clazz)
    ...
    return clazz;
  }
}

上面的查找类的过程是带缓存的,如果缓存中存在则直接返回,否则需要执行查找、加载、链接的过程,然后将类添加到缓存中。

  1. 类的查找
  • 系统启动类
    系统类需要根据环境变量BOOTCLASSPATH指定的路径去查找,BOOTCLASSPATH是在init.environ.rc文件中设置的,可以在shell中查看。
capricorn:/ $ echo $BOOTCLASSPATH
/system/framework/core-oj.jar:/system/framework/core-libart.jar:/system/framework/conscrypt.jar:/system/framework/okhttp.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/apache-xml.jar:/system/framework/org.apache.http.legacy.boot.jar:/system/framework/tcmiface.jar:/system/framework/telephony-ext.jar:/system/framework/WfdCommon.jar:/system/framework/oem-services.jar:/system/framework/qcom.fmradio.jar:/system/framework/qcmediaplayer.jar:/system/app/miui/miui.apk:/system/app/miuisystem/miuisystem.apk

通过遍历BOOTCLASSPATH中的所有jar包或apk,找到第一个包含指定类的。

  • 普通类
    普通类则去指定的dex文件中去查找。
  1. 类的加载
    类加载就是根据dex中描述的类的信息,创建对应的c++对象ClassObject。
    主要包括父类信息、接口信息、类变量、成员变量、直接方法和虚方法。需要注意的是,此时只是记录了父类在dex中的索引,并没有加载父类的详细信息,详细信息需要链接的时候才去做。
  2. 类的链接
    首先递归的对父类和实现的接口进行解析。
    然后创建虚方法表,接口方法表,创建一些特殊目的的方法桩。
    计算对象的大小(需要考虑变量的地址对齐),方便以后分配内存。

此时我们已经找到了ZygoteInit类,接下来获取其中的main方法。

// dalvik2/vm/Jni.cpp
static jmethodID GetStaticMethodID(JNIEnv* env, jclass jclazz, const char* name, const char* sig) {
  if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
        assert(dvmCheckException(ts.self()));
        return NULL;
    }
  ...
}

在获取静态方法时,如果发现类还没有初始化,则调用dvmInitClass进行初始化。

// dalvik2/vm/oo/Class.cpp
bool dvmInitClass(ClassObject* clazz) {
  ...
  dvmVerifyClass(clazz);
  ...
  if (!IS_CLASS_FLAG_SET(clazz, CLASS_ISOPTIMIZED) &&   !gDvm.optimizing) {
    dvmOptimizeClass(clazz);
  }

  ...
  if (clazz->super != NULL && clazz->super->status != CLASS_INITIALIZED) {
    dvmInitClass(clazz->super);
  }
  ...
  initSFields(clazz);
  ...
  method = dvmFindDirectMethodByDescriptor(clazz, "<clinit>", "()V");
  dvmCallMethod(self, method, NULL, &unused);
}

主要包括四步:

  1. verify验证
    验证主要是对方法的字节码进行静态安全检查,
  2. optimizie优化
    主要是对字节码进行优化,用一些更高效的指令替换旧的指令。
  3. 初始化类静态变量
  4. 执行类初始化代码

类初始化之后,则可以进行方法调用了。

//dalvik2/vm/interp/Stack.cpp
void dvmCallMethodV(Thread* self, const Method* method, Object* obj,
    bool fromJni, JValue* pResult, va_list args) {
  ...
  //准备工作:创建栈帧
  clazz = callPrep(self, method, obj, false);
 // 如果不是静态方法,则将this当做参数入栈
    if (!dvmIsStaticMethod(method)) {
#ifdef WITH_EXTRA_OBJECT_VALIDATION
        assert(obj != NULL && dvmIsHeapAddress(obj));
#endif
        *ins++ = (u4) obj;
        verifyCount++;
    }
  // 其他参数入栈
  ...
  //native方法使用nativeFunc指针
  if (dvmIsNativeMethod(method)) {
        TRACE_METHOD_ENTER(self, method);
        /*
         * Because we leave no space for local variables, "curFrame" points
         * directly at the method arguments.
         */
        (*method->nativeFunc)((u4*)self->interpSave.curFrame, pResult,
                              method, self);
        TRACE_METHOD_EXIT(self, method);
    } else {
      //解释执行
        dvmInterpret(self, method, pResult);
    }
  //出栈
  dvmPopFrame(self);
}

指令的解释执行如下

// dalvik2/vm/interp/Interp.cpp
void dvmInterpret(Thread* self, const Method* method, JValue* pResult) {
  ...
   Interpreter stdInterp;
    if (gDvm.executionMode == kExecutionModeInterpFast)
        stdInterp = dvmMterpStd;
#if defined(WITH_JIT)
    else if (gDvm.executionMode == kExecutionModeJit ||
             gDvm.executionMode == kExecutionModeNcgO0 ||
             gDvm.executionMode == kExecutionModeNcgO1)
        stdInterp = dvmMterpStd;
#endif
    else
        stdInterp = dvmInterpretPortable;

    // Call the interpreter
    (*stdInterp)(self);

    *pResult = self->interpSave.retval;
}

dalvik有fast、jit、protable三种执行模式。

  • fast会针对本地平台进行优化,可以更快速的执行Java代码
  • jit会将Java代码动态编译为Native代码,然后执行
  • protable则是可移植模式,也就是通用模式
    如果想看某个指令的具体实现,便可以去相应模式的解释器去看代码,比如new instance的实现。
// dalvik2/vm/mterp/out/InterpC-portable.cpp
HANDLE_OPCODE(OP_NEW_INSTANCE /*vAA, class@BBBB*/)
    {
        ...
        clazz = dvmDexGetResolvedClass(methodClassDex, ref);
        if (clazz == NULL) {
            clazz = dvmResolveClass(curMethod->clazz, ref, false);
        }

        if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz))
            GOTO_exceptionThrown();
        ...
        newObj = dvmAllocObject(clazz, ALLOC_DONT_TRACK);
        if (newObj == NULL)
            GOTO_exceptionThrown();
        SET_REGISTER(vdst, (u4) newObj);
    }
    FINISH(2);
OP_END

代码很清晰,先从缓存查找,是否已经解析过该类,如果没有则去解析。解析完,如果没有初始化,则对类进行初始化,最后为对象分配内存。

结束

本文只是粗略的给出了虚拟机从创建到执行的一个条主线,很多细节没有深入,比如虚拟机的内存布局、垃圾回收、线程机制、jni的实现、指令优化等等。不过有了这条主线,对于代码研究是个很好的开头,接下来我会继续研究JNI的实现原理。

ps:现在你可以回答一下开头的问题了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容