虚拟机之DVM

来源:互联网 发布:mac终端更改系统时间 编辑:程序博客网 时间:2024/06/06 07:25

DVM概述:

DVM(Dalvik)也就是Android在5.0之前使用的虚拟机.首先看看他去Java虚拟机JVM之间的关系.

Dalvik(DVM)虚拟机不是Java虚拟机(JVM),他是基于寄存器的,而标准Java虚拟机是基于栈,DVM的执行文件是dex,每个dex文件包含多个类,这样可以节约内存空间。JVM的执行文件是class文件,每一个类编译后都是一个class文件。因为JVM基于栈,他完全屏蔽的底层硬件架构,所以具有很好的可移植性,多个Java应用程序可以运行在同一个JVM实例里面。而DVM基于寄存器,所以需要根据不同的硬件架构做不同的设计,因此DVM设计了不同的运行模式,以达到夸平台运行的目的。DVM主要是针对嵌入式系统设计,每个DVM实例里面只运行一个java程序。DVM和JVM都是解释执行,并支持及时编译JIT。

Android应用程序进程都是由Zygote进程孵化出来的,Zygote进程是在系统启动时启动的,Zygote进程启动时会创建一个DVM实例,并在这个实例里面加载Java核心库。Zygote进程在创建应用进程的时候,通过系统调用fork复制自身。在Zygote进程第一次fork应用进程的时候,会将Java堆分为Zygote堆和Active堆,Zygote堆里面存放的是系统启动时候预加载的系统核心类,而Active堆存放的是后期加载的类和创建的对象。这样新创建的应用进程复制的Zygote进程的DVM,同时与Zygote进程共享Zygote堆,即Java核心类库,android核心类和系统资源。这样既能加快应用进程的创建速度,又能节省内存空间。

====================================================================================================================================

DVM启动过程
1.创建DVM实例
2.加载Java核心类库及其JNI
3.为主线程设置JNI环境
4.注册Android核心类的JNI方法

====================================================================================================================================

DVM的运行过程
解释执行与编译执行,编译执行又分为AOT和JIT,及提前编译和运行时编译。
DVM有三种执行模式
1. portable,可移植模式,这种模式下可以在任何平台上运行,如ARM,X86。
2. fast, 快速模式,这种模式是针对特定平台进行了优化,执行更快。
3. jit,动态编译执行,这种模式在运行过程中动态将java字节码编译成本地机器码直接执行。


DVM的解释器是以模块化方法自动生成,并能够根据一个特定平台进行优化。
模块化方法是指将解释器的实现分为多个模块,每个模块对应多个输入文件,然后用工具(一个python脚本)将所有输入文件组装成一个c或者汇编文件,这个组装的文件就是解释器的实现文件。
有了这种模块化方法后,我们只需要为不同平台输入不同的配置文件,然后用工具就可以自动生成DVM的解释器,如此便能实现跨平台。

====================================================================================================================================

JNI调用流程:
JNI方法是用C/C++写的,然后编译到一个so文件里面。在调用之前需要先加载到应用进程中:
System.loadLibrary("nanosleep"); 

Java方法——>DVM——>Bridge——>libffi库
Java方法调用native方法都是通过JNI的方式实现
一个JNI方法并不是直接被调用的,而是由DVM间接调用,这个间接调用是通过一个称为Bridge的方法进行的。Bridge在调用JNI之前会进行一下初始化工作。例如会把线程状态设置为NATIVE,会为JNI方法准备两个参数:
1.JNIEnv,用来描述当前线程的Java环境,通过他可以反过来调用java对象;
2.jobject,用来描述正在执行JNI调用的java对象。
Bridge也不是直接调用JNI方法,而是通过开源库libffi调用具体的JNI方法。因为DVM是夸平台的,每个平台都可能定义自己的函数调用规范,即ABI。ABI是在二进制级别上定义的一套函数调用规范,例如函数参数是通过寄存器还是堆栈进行传递。

JNI方法的注册本质就是将Bridge函数赋值给需要注册JNI的Java类成员函数。
void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func,  
    const u2* insns)  
{  
    ......  
  
    if (insns != NULL) {  
        /* update both, ensuring that "insns" is observed first */  
        method->insns = insns;  
        android_atomic_release_store((int32_t) func,  
            (void*) &method->nativeFunc);  
    } else {  
        /* only update nativeFunc */  
        method->nativeFunc = func;  
    }  
    ......  
}  
参数method表示要注册JNI方法的Java类成员函数,参数func表示JNI方法的Bridge函数,参数insns表示要注册的JNI方法的函数地址。

====================================================================================================================================

DVM Java堆创建过程:
DVM Java堆实际上是一片匿名共享内存。每个应用进程都有一个系统定义的最大内存值,实际分配的内存往往小于这个值,按需分配,但不能超过该值,不足就会OOM。

DVM将Java堆分为Zygote堆和Active堆两部分,Zygote堆上分配的是在Zygote进程启动过程中预加载的系统类,资源和对象,该部分内存应用进程与Zygote进程共享.而应用进程分配的对象都在Active堆上面,所以GC大多数时候也在该堆上进行。

DVM采用Mark-Sweep即标记—清除算法进行并行GC,即GC线程和工作线程同时运行.
并行GC的标记过程分为3个阶段:
1. 标记GC Root,GC Root就是被静态变量,栈变量和寄存器引用的对象。该阶段时间较短,只能运行GC线程。
2. 根据地址从小到大标记GC Root引用的对象(可达性分析算法)。该阶段时间长,可同时运行工作线程。
3. 重新标记第二阶段中被修改的对象,该阶段时间短,只能运行GC线程。

与GC相关的数据结构有
1. Card Table
   用来标记在并行GC时被修改的对象,一个Card就是一个字节,可以表示128个对象。因为一个字节有8位,2的7次方就是128,所以一个字节也就是一个Card可以表示128个对象。这样可以节约内存。
   通过Card可以获得其表示的对象地址,通过对象地址也可以获取相应Card。
2. Live Heap Bitmap
   标示上一次GC时存活的对象,每一个位表示一个对象。可以通过Java堆的起始地址和对象的偏移地址计算出对象在Bitmap中的位置。
3. Mark Heap Bitmap
   标示本次GC被标记的对象,在Mark Heap Bitmap中标记为0的对象将被清除。
4. MarkStack
   MarkStack本质上是一个object指针数组,用来存储并行gc阶段被引用对象地址小于引用对象地址的对象.比如,对象B引用A, 且A的地址小于B的地址,那么标记完对象A后再将A压入MarkStack中.通过该数据结构可以遍历完java堆中所有被GC Root引用的对象。
DVM JAVA堆的创建其实就是创建上面四个数据结构


====================================================================================================================================

Java对象内存分配

每次分配的大小由相应类的ClassObject的成员变量objectSize表示。

分配过程:
1. 调用dvmHeapSourceAlloc(size)函数分配指定大小的内存,成功则返回内存地址,失败则进入下一步。
2. 触发gc,这次gc不回收软引用对象。
3. 调用dvmHeapSourceAlloc(size)函数再次分配,成功则返回内存地址,失败则进入下一步。
4. 将堆内存调到最大值
5. 调用dvmHeapSourceAlloc(size)函数再次分配,成功则返回内存地址,失败则进入下一步。
6. 再次触发gc,本次gc回收软引用对象。
7. 调用dvmHeapSourceAlloc(size)函数再次分配,成功则返回内存地址,失败则报OOM。

判断逻辑是在tryMalloc(size)函数中,而具体的内存分配是dvmHeapSourceAlloc(size)函数。


====================================================================================================================================

DVM GC实现

DVM采用Mark-sweep算法进行垃圾回收,也就是“标记-清除”算法。sweep是最终调用C库的free方法释放内存,DVM做的工作主要在Mark上面。

DVM有三个gc类型,四个触发gc的场景。
三个gc类型为:
1. isPartial:部分gc,只回收Active堆.
2. isConcurrent: 并发gc
3. doPreserve: 为true时,不回收软引用对象,为false时,回收软引用对象。
四种触发gc的场景分别为:
1. GC_FOR_MALLOC:在堆上分配对象时内存不足触发的gc
2. GC_CONCURRENT:已分配内存超过一定阈值触发的gc
3. GC_EXPLICIT:应用进程调用Runtime.gc(), VMRuntime.gc()触发的gc
4. GC_BEFORE_OOM:OOM前的gc
具体的实现都是在dvmCollectGarbageInternal(GcSpec)函数中执行,传入参数GcSpec告诉他触发gc的场景和gc类型。

GC标记主要分为三个阶段:
第一阶段: GC Root的标记(非并行)
第二阶段: 被GC Root引用的对象的标记(并行)
第三阶段: 第二阶段被修改对象的标记(非并行)

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
第一阶段,标记GC Root:
GC Root定义:
1. 虚拟机Java栈中引用的对象
2. 方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4. JNI引用的对象
5. 虚拟机内部创建的特殊对象,如OutOfMemory异常对象

首先看DVM如何处理JNI引用的对象
在JNI创建或者访问JAVA对象的时候,会将Java对象的引用保存在一个线程相关的JNI Local Reference Table中,这些被JNI调用的Java对象会在调用结束时被自动释放,但是如果JNI函数引用的Java对象数量大于JNI Local Reference Table的容量(512)就会出现错误,导致Java对象不能被自动释放。在标记GC Root的时候需要将所有线程JNI Local Reference Table里面的Java对象进行标记。

GC Root中最主要的就是虚拟机Java栈中的对象,也就是正在使用的对象。
Java栈中所有栈贞引用的对象都被认为是GC Root,都需要被标记。
DVM是基于寄存器,所有函数调用中的过程数据,如参数,局部变量都保持在寄存器里面。但虚拟机的寄存器并不是硬件的寄存器,他本质是映射的java栈贞对应的内存块,所以遍历栈贞就能找出所有正在使用的对象。
寄存器中保存的并不一定是对象引用reference,也可能是int等其他类型的数据。那么如何判断寄存器中的值是否是对象引用呢?
这里有两种方法:
1. 只要寄存器中的数据是在java堆的地址范围内,就认为是对象引用。这个叫保守GC算法,这个算法并不准确,因为其他类型数据也可能落在这个地址范围内。
2. 借用Register Map数据结构来辅助GC,这也是一种位图。
   Register Map记录了每个函数的在所有GC执行点的寄存器情况,每个寄存器在Register Map中对应一个位,如果某个寄存器是引用的对象,那么其对应的位就被标记为1.
Register Map:
在安装apk的时候,dex文件除了被优化为odex文件,还会被验证和分析,验证是为了防止是有非法指令,分析是为了得到指令执行状况,其中就包括每一个GC执行点的寄存器使用情况,最终形成一个Register Map保存到odex文件中。      
GC执行点:
GC在标记GC Root的时候会挂起所有工作线程,而工作线程总是挂起在IF、GOTO、SWITCH、RETURN和THROW等跳转指令中。这些指令点对应的就GC执行点。

找到java栈中所有对象后,就将其在Mark Bitmap中对应的位设置为1。 

第一阶段是非并行的,也就是工作线程处于挂起状态。那么DVM是如何处理线程的挂起和唤醒的呢?
每一个虚拟机线程都有一个Suspend Count变量,每当gc线程发起挂起请求,该值就+1,反之,gc线程发起唤醒通知时,该值就-1. 每当工作线程执行到gc执行点的时候都回去判断该值,如果大于0,则将自己挂起。工作线程挂起后会定期检查Suspend Count,如果等于0,则将自己唤醒。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
第二阶段,标记被GC Root引用的对象:
这个阶段分为两步:
1. 按照地址从小到大遍历Mark Bitmap
   此时GC Root对象在Mark Bitmap中对应的位都已标记为1,所以其实就是遍历所有的GC ROOT对象,将被GC Root对象引用的对象标记为1,如果该对象地址小于引用他的GC Root对象地址,就压入Mark Stack中。
   这个算法有个好处,凡是被引用对象地址大于引用对象地址的对象都会在一次遍历中标记完成,只有那些被引用对象地址小于引用对象地址的对象才需要压入Mark Stark进行进一步处理。效率高。
2. 遍历Mark Stack
   首先弹出栈顶的对象,找到并标记其引用的对象,然后再压入Mark Stack。如果没有引用对象,则弹出下一个栈顶对象。循环执行,直到Mark Stack为空。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
第三阶段,标记在第二阶段被修改的对象
由于第二阶段是并行执行,所以在标记的过程又有对象被重新引用,这些被重新引用的对象对应的card值会被设置为Drity,第三阶段就是根据Drity card标记那些被修改的对象和他们所引用的对象.

DVM对重写了finalize()方法的对象的处理:
DVM里面有一个Finalizer线程,专门执行finalize()方法,重写了finalize()方法的对象的引用类型为FinalizerReference,该引用类型的对象在gc前Finalizer线程会去执行他的finalize()方法,正因为如此,他需要被标记,他所引用的对象也需要被递归标记。Finalizer线程只会执行一次finalize()方法,所以该引用类型的对象有一次拯救自己的机会。
阅读全文
0 0
原创粉丝点击