android虚拟机原理---运行流程概述

Posted 小钟视野

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android虚拟机原理---运行流程概述相关的知识,希望对你有一定的参考价值。

前言

android中,系统通过init进程创建出来的zygote进程,为每个应用创建一个进程和复制一个虚拟机实例,而每个应用都运行在一个DVM或AVM实例中;而且每个进程或线程都对应linux中的一个进程或线程

一JVM、DalvikVM、ArtVM区别

Jvm(java虚拟机):标准的虚拟机,java文件编译生成标准的多个java字节码(.class)文件,并打包成ja文件,而 jvm运行的字节码就是从jar和.class文件中获取的;此外jvm是基于栈的,基于栈的机器必须使用指令来载入

DVM:基于寄存器,需要更大的指令;将java编译成.class文件,然后通过dx工具将多个.class优化成一个.dex文件(classes.dex)打包进入apk中,.dex文件时专为app生成的压缩格式;app安装时会有opt工具优化成odex文件,每次运行时都会解释odex生成本机机器码

AVM:和DVM类似,不同的是AVM在安装apk时会将一个或多个dex优化成oat本地机器码文件,运行时不再重新解释,比DVM更快

二VM的沙箱模型

Vm的沙箱模型其实指的就是vm的安全性。沙箱限制一切可能对系统造成破坏的动作指令

 安全性:指的是对一些非本地代码(网络下载的、来源不清楚的等)不可靠代码限制了权限;主要包括
   1、对本地硬盘的操作

   2、进行任何网络连接,但不能连接到提供者

   3、创建新的进程

   4、装载新的动态链接库

沙箱模型中的4个组件:

 1、类加载体系结构

 2、Class文件校验器

   3、内置于java虚拟机(及语言)的特性

   4、安全管理器及javaAPI

前3个的安全特性保持JVM的实例和它正在运行的应用程序的内部完整性,防止恶意代码的侵犯;而第4个安全管理器使得外部资源不受jvm内部恶意代码的破坏,也就是这个安全管理器是运行在jvm中的外部资源权限管理器

三.内存工作模型


Java的多线程并发问题最终都反应在java内存模型上(上图),而所谓的并发问题也就是多线程的安全性;而线程的安全性无非就是要控制多个线程对某个资源的有序访问或修改。所以我们要解决的是:资源的有序性和可见性

Jvm的内存模型中,定义了线程对主内存的操作指令:read、load、use(使用变量)、assign(赋值)、store、write、lock、unlock

Lock和unlock 作用于主内存的变量,即变量被某个线程锁定和解锁

Read作用于工作内存的变量,read将主内存的变量值传入到工作内存

load:将从主存获取变量值放入工作内存的变量副本中

Use:使用工作内存的变量,传递值给执行引擎

Store:工作内存中变量的值传送到主内存中

Write:把store传入的值放入到主内存的变量中(更新)

 

可见性:多个线程之间是不能互相传递数据通信的,他们之间只能通过共享变量来进行。

        共享变量指的就是实例字段、静态字段、和构成数组对象的元素,但不包括局部变量与方法参数,因为这是后者私有的,不会被共享就不会存在竞争安全问题

当一个共享变量在多个线程的工作内存中都有副本时,任何一个线程副本修改了这个共享变量,其它线程都能知道共享变量被修改了;这就是可见性问题

有序性:在线程引用一个变量时不能直接从主内存中引用,若线程的工作内存中没有该变量,则从主内存中复制一个副本到工作内存中(read-load);同一线程再次引用这个字段时,可能重新从主内存中读取也可能继续使用之前的副本也就是说read、load、use顺序是由JVM决定的;

原子性:像数据库的事务一样,要么成功,要是失败

所以主内存是多个线程共享的,当新建一个对象时也是被分配到主内存中的,每个线程都有一个工作内存,工作内存存储了主内存的对象的副本;

线程操作某个对象时执行顺序如下:

  1、  从主内存个中复制变量到当前工作内存(执行read and load指令)

  2、  执行代码,改变共享变量值(useand assign):是在工作内存中修改的

  3、  用工作内存数据刷新主内存相关内容(store and write指令)

顺序是不能改变的,也就是说同一时刻操作一个变量必须锁住。保证有序性

四.内存回收机制

Dvm的垃圾收集器运行过程中,除了垃圾收集器整个dvm的程序都要停止运行,应用程序必须(所以线程)暂时停止,知道运行完成,才能重新启动应用程序运行

先了解几个概念

寄存器:在程序中无法控制,不用管

java栈:存放基本类型的变量数据和对象的引用,对象本身不再栈而是在堆中,栈在线程创建时创建,即为每个线程创建对应的栈内存,线程销毁也会回收栈内存

    有许多栈帧,一个方法对应一个栈帧,保存的是方法的状态

java堆:存放new产生的数据(对象);是由jvm自动垃圾收集器来管理的;堆会分为永久区、新生区、养年区;

         永久区:数据不会被回收

        养老区:用于保存从新生区筛选出来的java对象,一般池的对象都活跃在这里

        新生区:new出来的对象都是保存在这,当此区已满时,垃圾回收器将不回收的对象存放到老年区

静态域:存放在对象中用static定义的静态成员

方法区:常量池:存放常量;包括:类和接口的全限定名、字段名称和描述、方法和名称描述、直接常量(常量池中);常量池存储在方法区中

非RAM存储:硬盘等永久存储空间

栈和堆是不同管理的,虽然栈保存的变量的引用地址指向堆内存真实对象,但当栈的变量不在作用域时会回收栈的变量(比如方法中的临时变量或此方法的线程结束了),但是堆中的对象并不是立马回收,只是没有指向这个对象的引用,系统会把它标志成垃圾对象,当系统触发回收机制时才会回收。

我们都是知道回收机制都是从根集合开始遍历,查找直接或间接的引用的对象打个mark,将引用不到或者没引用的对象回收,但是怎么知道这个对象是不是被引用的?

要理解回收机制,就要知道JVM或DVM使用的是什么算法,算法的原理,谁是根集合。

根集合:所谓的根集合就是基于线程出发的,每个线程都会创建对应栈内存,而栈内存保存;包括寄存器、java类静态字段、局部和全局的JNI引用、jvm每个方法对应的栈帧中的对象引用(如对象引用的局部变量和参数);所有这些引用构成的就是根集合

   从根集合递归就可追踪可访问的对象集合

算法:

  1、  引用计数

  2、  MarkSweep算法(标记和清除)

  3、  SemiSpaceCopy

这里dvm主要用的MarkSweep算法,所以浅谈一下。

MarkSweep:分为Mark阶段、Sweep阶段和可选的Compact阶段(DVM不实现,不用管)

垃圾收集器运行的分为如下步骤

第一:Mark阶段(追踪标志阶段):

标记出所有活动对象,垃圾收集器通过栈来保存根集合,然后对栈中的每一个元素,递归追踪所有可访问的对象,并在markBits位图将对象的内存起始地址的置为1,当遍历完所有栈元素,即所有元素都出栈时,markBits就是所有可访问的对象的集合

第二:Sweep阶段(回收阶段):

回收内存阶段,由于markBits位图可知所有可访问的对象(不会回收的对象),而liveBits位图是所有对象的集合,所以比较两个位图就知道哪些对象时需要回收的;而后会调用free来回收内存


五.JNI (结合网上例子来理解更好)

概述:

JNI:java本地接口:是java和其它语言(c/c++)代码交互的一种接口规范,相当于协议。也作为java和c/c++交互的中间层

Native:.c或.cpp按照规范实现的c/c++方法,在此方法中可以通过env指针实现调用java方法、属性、类,也可以调用c/c++原生方法和java通信(传递数据、修改属性等等)

在native方法中,java的基础类型(float/int/array/class/Object)都有对应的jni的类(比如jfloat/jint/jXXXArray/jclass/jObject等等)也就是java 类中定义的native方法传的参数是java基础类型,到了Native方法(cpp、c)实现中参数类型就是jxxx

在native的实现方法中若是想要将jxxx变量在c/c++方法中使用,必须通过env转成c、c++的正常基础类型的变量比如(int、float/char/指针等等),同理也是一样在native方法实现中想将c的变量的值想要回传给java也是要转成相应的jxxx返回;

总而言之,native方法中操作java的对应基础类型是jxxxx,操作c、c++原生的就用原生的类型。这就是JNI中间层的能力

以下抄自小楠总的博客:https://www.jianshu.com/p/919719964ec4

每个native函数,都至少有两个参数(JNIEnv*,jclass或者jobject)。

当native方法为静态方法时:

 1) jclass 代表native方法所属类的class对象(JniTest.class)。

 2)当native方法为非静态方法时:jobject代表native方法所属的对象。

JNI开发中JNIEnv在C和C++中实现的区别

JNIEnv:JNIEnv里面有很多方法,与Java进行交互,代表Java的运行环境。JNIEnvironment。

在C中:

JNIEnv 结构体指针的别名

env 二级指针

JNIEXPORT jstringJNICALL Java_com_test_JniTest_getStringFromC

(JNIEnv * env,jclass jcls)

    //env是一个二级指针,函数中需要再次传入

    return(*env)->NewStringUTF(env, "String From C");



在C++中:

JNIEnv 是一个结构体的别名

env 一级指针

JNIEXPORT jstringJNICALL Java_com_test_JniTest_getStringFromC

(JNIEnv * env,jclass jcls)

    //env是一个一级指针,函数中不需要再次传入

    returnenv->NewStringUTF("StringFrom C");



jni.h头文件中有下面的预编译代码:

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedefconststructJNINativeInterface_ *JNIEnv;
#endif



 

注册

 静态注册

   其实也就是平时开发中遵循JNI的规范 :映射java方法和c/c++中的方法对应

   再来看在cpp中定义的函数名:Java_com_jni_HelloWorld_helloworld

java文件与cpp文件中函数名的配对定义方式为Java + 包名 +java类名 + 方法/函数名,中间用_分隔;其中两个参数分别是:

比如:

cpp中定义的函数名:Java_com_jni_HelloWorld_helloworld

javapublic void native helloworld();

jni目录下添加一个helloworld.cpp

#include<jni.h>

#include<android/log.h>

#include <string.h>

 

#ifndef_Included_com_jni_HelloWorld // 1

#define _Included_com_jni_HelloWorld

 

#ifdef __cplusplus// 2

extern"C" 

#endif// 2

JNIEXPORT jstringJNICALL Java_com_jni_HelloWorld_helloworld(JNIEnv *, jobject);

#ifdef __cplusplus// 3



#endif// 3

#endif// 1

 

JNIEXPORT jstringJNICALL Java_com_jni_HelloWorld_helloworld(JNIEnv * env,

        jobject obj) 

    return env->NewStringUTF("helloworld");



 

   具体包括方法申明和方法实现两个部分;这个编译链接之后就可以调用了。

可以看出静态注册:是通过c/c++里面方法的名称来映射函数

动态注册                 

可以不用遵循jni注册函数的规范,直接在.cpp动态注册;当System.loadlibrary(”so文件”)时,会执行JNI_Onload()方法,在此方法调用

registerNativeMethods(xxx,“包名/类名”,JNINativeMethod[],methodNum),方法传入JNINativeMethod[]数组实例gmethods[]和这些方法声明在java中的包名/类名,就会将本地函数登记到VM中,这样就可以互相通信了,而且登记到vm中可以加快本地函数调用的效率

应用层级的java类穿过VM呼叫(调用)到本地函数,这个过程是通过vm去寻找“*.so”多个so文件的本地函数,要调用多次就会寻找多次,这会花费很多时间,所以可以向VM登记本地函数到JNINativeMethod[],这样只需要查找这个方法对应表就可以知道调用哪个本地方法了

以下抄自网络的一个例子:

JNINativeMethod 结构体的官方定义

 typedef struct     

   const char* name;    

    const char* signature;    

    void* fnPtr;    

   JNINativeMethod; 

第一个变量nameJava中函数的名字。
第二个变量signature,用字符串是描述了Java中函数的参数和返回值
第三个变量fnPtr是函数指针,指向native函数。前面都要接 (void *)

所以JniClient.c文件里面有下面的代码

JniClient.java文件

public class JniClient 
 static   

          System.loadLibrary("FirstJni");  
       
  public  JniClient()   
    
  public native String getStr();  
  public native int addInt(int a, int b);  

  

3JniClient.c文件(新建一个文件夹jni,然后把这个文件放在jni文件夹里面)

 #define JNIREG_CLASS "com/example/chenyu/test/JniClient"//指定要注册的类  
 jstring get_strstr(JNIEnv* env, jobject thiz)  
  
 return (*env)->NewStringUTF(env, "I am chenyu, 动态注册JNI");  
  

 jint add_int(JNIEnv* env, jobject jobj, jint num1, jint num2)  

 return num1 + num2;  

  

 

/**  

 * 方法对应表  

*/  

static JNINativeMethod gMethods[] =   
   "getStr", "()Ljava/lang/String;", (void*)get_str,  

        "addInt", "(II)I", (void*)add_int,  

 ;  

 

/*  

 * 为某一个类注册本地方法  

 */  

static int registerNativeMethods(JNIEnv* env  , const char* className  
 , JNINativeMethod* gMethods, int numMethods)   

 jclass clazz;  

 clazz = (*env)->FindClass(env, className);  

  if (clazz == NULL)   
      return JNI_FALSE;  
    
 if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0)   
     return JNI_FALSE;  
    
 return JNI_TRUE;  
  

 /*  
 * 为所有类注册本地方法  

*/  

 static int registerNatives(JNIEnv* env)   
  return registerNativeMethods(env, JNIREG_CLASS, gMethods,  
                sizeof(gMethods) / sizeof(gMethods[0]));  
  

 /*  
* System.loadLibrary("lib")时调用  

 * 如果成功返回JNI版本, 失败返回-1  

*/  
 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)   
 JNIEnv* env = NULL;  
jint result = -1;  
  if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK)   
        return -1;  
    
 assert(env != NULL);  
 if (!registerNatives(env)) //注册  
      return -1;  
   

//成功  
 result = JNI_VERSION_1_4;  
 return result;  
  


可以看出在.c文件中的native实现方法中不需要遵循包名_类名_方法名,只需要写自己想要的方法名就好了;

重写JNI_OnLoad()方法这样就会当调用 System.loadLibrary(“XXXX”)方法的时候直接来调用JNI_OnLoad(),这样就达到了动态注册实现native方法的作用。

上面代码中的 JNIEXPORT JNICALL jni 的宏,在 android jni 中不需要,当然写上去也不会有错,因为JNIjava和其它语言通信的一套标准,不是针对android的,所以android会重新定义这两个宏为空

注:由于gMethods[]是一个“<名称,函数指针>”格式,所以可以多次呼叫函数registerNativeMethods()的方式来更换本地函数指针啊

.Binder机制

看书也没理清楚Binder机制中4个组件的通信关系

Binder框架定义了4个组件角色:Server,Client,ServiceManager(以后简称SMgr)以及Binder驱动。其中Server,Client,SMgr运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。

binder驱动提供/dev/binder设备文件与用户空间进行交互,client/server/smgr通过文件操作函数open()/ioctl()与binder驱动进行通信

总结:

  server、client、 smgr都是运行与独立的进程;

  1 、smgr 通过open(),/dev/binder打开驱动设备文件,告知binder驱动自己是binder机制的上下文管理者,并且会创建binder 0号,而此时binder驱动也会为smgr创建一个binder实体,这个binder实体在所有client中可以通过0来获取到它的引用,称作0号引用,一切与我通信的进程都要经过0号引用与我打交道,之后smgr进入循环等待其它进程通信 

  2、server为了提供服务,通过binder驱动以及smgr的0号引用,将server的 binder实体引用与实体名称注册到smgr,想要访问这个服务的就通过这个名称来获取binder实体引用

  3、当client要获取server的服务时,首先通过binder携带server的binder的实体名称,经过binder驱动,binder驱动通过smgr的0号引用和实体名称从smgr获取到server的binder实体引用,这样client和server就建立了一条binder通道,在通道上进行通信

以上是我的理解,有误之处请不吝赐教

以下抄自网络:

3.1 Binder 驱动             

和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的。它工作于内核态,驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。

实现方式和设备驱动程序是一样的:它工作于内核态,提供open()mmap()poll()ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供read()write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()read()Binder驱动的代码位于linux目录的

3.2 ServiceManager 与实名Binder

和DNS类似,SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。

细心的读者可能会发现其中的蹊跷:SMgr是一个进程,Server是另一个进程,Server向SMgr注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:SMgr和其它进程同样采用Binder通信,SMgr是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。SMgr提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成SMgr时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。也就是说,一个Server若要向SMgr注册自己Binder就必需通过0这个引用号和SMgr的Binder通信。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的Client是相对SMgr而言的,一个应用程序可能是个提供服务的Server,但对SMgr来说它仍然是个Client。

3.3 Client 获得实名Binder的引用

Server向SMgr注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向SMgr请求访问某个Binder:我申请获得名字叫张三的Binder的引用。SMgr收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于SMgr中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。通过以上过程可以看出,SMgr象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个Binder的引用。

这里推荐一篇文章,讲的很详细

http://blog.csdn.net/boyupeng/article/details/47011383

七.VM启动流程

android中每个应用都运行在一个DVM实例里,而每个dvm实例都是一个独立的进程空间;当手机启动时,linux内核初始化完成后会创建并运行一个init进程,随后init进程初始化系统外操作以及读取init脚本初始化各种service,zygote进程就是从脚本的service通知init进程创建而来,起初zygote进程只是名为app_process进程,经过一系列的代码修改为zygote进程,从而zygote进程就作为所有app进程的父进程;

zygote本身就是应用程序,zygote进程创建后,启动zygote进程,创建一个虚拟机实例,并注册虚拟机实例中的jni方法、完成系统framework库的加载、预置类库的加载,随后会调用startSystemServer()启动一个后台服务进程,最后会创建socket服务进入循环,等待其他进程服务(比如AMS/WMS/PMS等)的请求;

zygote通过startSystemServer()->Zygote.forkSystemServer()孵化(fork)出system_server进程,随后syetem_server进程启动一个serverThread线程,线程主要负责启动android平台中的各个service比如android核心服务:ActivityManagerService(AMS)、PacketManagerService(PMS)、WindowManagerService(WMS);蓝牙服务;电池服务;通信相关(wifi、telephone)服务;系统相关功能服务Audioservice、usb-service;状态栏、通知服务、剪贴板服务、日志储存管理服务、性能统计管理服务等系统所有的service基本都归于它来启动,而且将service都要注册到serverManager当中这就涉及到binder机制通信了。此外system_server进程通过启动的PMS调用scanDirLI()来扫描手机中的5个目录中的apk文件,目录如下

/system/framework

/system/app

/vendor/app

/data/app

/data/app-private

获取到apk格式的应用程序文件,调用parsePackage()全程解析apk中androidManifest.xml配置文件,将配置文件中activity、service、broadcast、content provider等整个应用程序信息保存到PMS中集合mpkgs(pkgname,pkg),安装完成,此时应用程序相当于在PMS中完成注册,想在手机界面看到应用程序,需要通过Home应用程序从PMS中把安装好的应用程序以快捷图标的方式显示在桌面。

当点击桌面快捷图标打开app时,AMS调用startProcessLocked()将app进程启动参数写到LocalSocket通过LocalSocket连接并通知zygote进程通过Zygote.fofkAndSpecialize()创建一个新的app进程,创建完毕后,RuntimeInit.invokeStaticMian()将ActivityThread作为新程序主入口,这个ActivityThread就是App的主入口也是主线程。整个过程大概就这样的。

init进程是用户空间第一个进程,也是所有应用空间进程的父进程,也就是说应用空间的所有进程都是有init通过folkandspecify()直接或间接fork出来的

zygote进程通过fork(复制)自身,最快速的提供一个系统,对于一些只读的系统库,所有虚拟机实例和zygote共享一块内存区域,所有创建app进程时都是通过zygote进程folk出来的,app进程就有和zygote基本一模一样的虚拟机实例和共享的内存的数据

如果zygote或system_server进程出现异常,则init会重新创建这两个进程

探讨一下,APK的dex文件时什么时候解析,转换成odex或oat文件?

也是通过PMS在安装应用程序时,将APK中的dex文件

DVM环境下:通过opt工具优化主classes.dex成odex文件,准备好之后才算是安装完成,注:除了classes.dex文件其它dex文件不会载入,这也就是multidex的用户

AVM环境下:直接通过dex2oat()方法进而调用DexFile类中方法对所有dex文件进行校验并优化最后生成oat可执行文件,oat文件是本地机器码,所以AVM可以直接执行,不需要像DVM那样每次运行都要解释odex文件生成本地机器码,所以AVM更加快更加节省运行时间

以上是读完《android虚拟机原理》的理解,此处涉及的源码很多,感兴趣的可以入手一本 不对之处,请不吝指正。



以上是关于android虚拟机原理---运行流程概述的主要内容,如果未能解决你的问题,请参考以下文章

Java虚拟机 - 结构原理与运行时数据区域

Android Dalvik虚拟机 对象创建内存分配流程

Android Dalvik虚拟机 对象创建内存分配流程

Android学习之zygote启动流程

Java 虚拟机原理JDK 体系结构 | Java 源码运行原理 | Java 虚拟机内存

安卓art虚拟机在啥位置