虚拟机从应用安装PMS说起

应用在安装的时候,主要由PMS(PackageManagerService)进行处理,大概就是通过守护进程installed调用一个工具dexopt对相关文件进行处理。对于Android 4.4以上的,会对Apk包中的所有dex文件进行dex2oat操作,最终形成本地机器语言代码,这种就是我们常说的AOT(AheadOfTime)、既运行前编译,得到一个ELF格式的oat文件,以.odex后缀结束,保存在/data/dalvik-cache目录中。而安装包在安装的时候就已经开始了编译并保存处理后的代码文件,因此在程序启动的时候就可以直接执行了。对于Android 4.4以下的,在Apk包安装过程中只对main dex进行dexopt操作,dexopt的作用是对dex文件进行verfication和optimization,对dex文件的字节码进行优化,优化得到的文件也是保存在/data/dalvik-cache文件中。我们在开发中遇到过多dex打包的情况,改情况主要是为了解决在4.4以下,在安装包的时候,不能超过65535个方法的问题,因此需要进行分包。而在安装apk的时候只对主dex进行dexopt,因此在程序首次启动的时候,在4.4以下系统的设备上,需要人为的干预进行dexopt,而这也是我们开发中经常看到的Multidex的原因。当然,谷歌提供的解决这个问题性能上存在一些问题。解决Multidex在首次启动的时候的性能问题,会在性能优化系列中讲到。

我们知道,Android运用是用Java编写的,因此我们自然而然想到肯定涉及到虚拟机,在Android4.4以下我们比较好理解,因为仍然是字节码的形式表现,所以Dalvik虚拟机就类似Java虚拟机。但是,Android4.4以上,对相关的dex文件都dex2oat成本地机器码了,为什么还使用一个称为ART的虚拟机。实际上,应用程序本身仍然是使用Java语言编写,在dex2oat的时候,实际上会形成一个OAT文件,而通过OAT文件找到某个类的方法的本地机器指令流程如下:

根据类签名信息从包含在OAT文件里面的DEX文件中查找目标Class的编号,然后再根据这个编号找到在OAT文件中找到对应的OatClass。接下来再根据方法签名从包含在OAT文件里面的DEX文件中查找目标方法的编号,然后再根据这个编号在前面找到的OatClass中找到对应的OatMethod。有了这个OatMethod之后,我们就根据它的成员变量begin_和code_offset_找到目标类方法的本地机器指令了。而关于如何从DEX文件中根据签名找到类和方法的编号要求对DEX文件进行解析,这就涉及到虚拟机类加载和方法查找了,因此这也是称为ART虚拟机的原因。

说到虚拟机,我们就不得不需要了解堆以及内存回收。而涉及堆就涉及堆的分配,涉及内存回收就涉及到垃圾回收算法。针对Android4.4为分界线,首先先分析下Dalvik虚拟机垃圾收集机制核心的一个数据结构图:

Dalvik堆主要由Active和Zygote堆组成,其中Zygote堆用来管理Zygote进程在启动过程中预加载和创建的对象吗,而Active堆是在Zygote进程fork第一个子进程之前创建的。之后无论是Zygote进程还是其子进程,都在Active堆上进行创建和释放。之所以保持Zygote堆不变,是为了无论是Zygote进程还是Zygote子进程,都可以最大限度的共享Zygote分配的内存,不必因为动态分配和释放内存所影响。

涉及到堆自然涉及到垃圾回收,涉及到垃圾回收自然就需要垃圾回收算法,那么Dalvik虚拟机使用的是Mark-Swap算法进行垃圾回收。Dalvik虚拟机使用几个辅助数据结构来管理Java堆。我们从分析垃圾回收算法如何回收堆对象说起,首先Mark阶段,会从根对象开始标记被引用的对象,标记完成进入Swap阶段,Swap阶段就是回收没有被标记的对象占用的内存。而标记就是用了Heap Bitmap的辅助数据结构来进行。从上图我们知道,分别有Live Bitmap和Mark Bitmap。显然,Live Bitmap是用来标记上一次GC时被引用的对象,既没有被回收的对象,而Mark Bitmap用来标记当前GC有被引用的对象。那么通过这两个Bitmap就可以判断哪些对象需要被回收(Live Bitmap标记为1,Mark bitmap标记为0的)。一般情况下,进行垃圾标记时,需要停止其他线程进行(Stop The World),但这种情况在移动并不可取,因为标记是相对耗时的,会造成卡顿情况。因此在Mark阶段是允许其他线程进行的,这就是Concurrent GC。因此,需要对Mark阶段分步骤进行,第一步,只负责标记根对象(被全局变量,栈变量和寄存器引用的对象)(这一步停止其他线程进行,只有垃圾收集线程),从根对象可以追溯下去哪些对象被引用,第二步,标记被根对象引用的对象。而第二步,因为其他线程可能修改了对象,因此需要对这个对象进行记录,Card Table用来记录在垃圾回收过程中记录非垃圾收集堆对象对垃圾收集堆对象的引用。我们知道,在Mark阶段是从根对象追上下去的,而Card Table只是记录非垃圾堆对象对垃圾收集堆对象的引用,那么在追溯过程中,需要Mark Stack来配合哪些对象标记了。通过Stack来递归追溯。

Dalvik为新创建的对象分配堆的过程如下:

在不改变java堆大小的情况尝试分配内存,若成功直接返回分配成功的地址;

1. 若上一步失败,则进行不回收软引用的GC;

2. GC完毕,继续尝试轻量级的内存分配操作,成功直接返回;

3. 若第三部失败了,将Java堆的当前大小设置为虚拟机启动指定的Java堆最大值,再进行内存分配;

4. 若分配内存成功,直接返回;

5. 若上一步分配失败,调用GC在进行一次回收,这个时候需要回收软一样;

6. GC完毕之后,再进行一次内存分配,若失败则跑出分配内存分配失败异常。

上面在分配内存时,我们看到在尝试分配内存失败的情况下,会进行GC操作,那么Dalvik虚拟机会在哪些情况下进行GC呢?Dalvik虚拟机有以下几种类型会触发GC:

GC_FOR_MALLOC: 表示在堆上分配内存不足触发的GC。

GC_CONCURRENT: 表示在已分配内存达到一定量之后触发的GC。

GC_BEFORE_OOM: 表示是在准备抛OOM异常之前的最后努力而触发的GC。

GC_EXPLICIT: 表示是应用程序调用System.gc 、VMRuntime.gc接口或者受到SIGUSR1信号触发的GC。

其中,前三种都是在分配内存的时候可能执行的。

那么Dalvik虚拟机的执行GC过程是怎样的?并行和非并行GC的步骤如下:

第1步到第3步用于并行和非并行GC:

1.  调用函数dvmSuspendAllThreads挂起所有的线程,以免它们干扰GC。

2.  调用函数dvmHeapBeginMarkStep初始化Mark Stack,并且设定好GC范围。

3.  调用函数dvmHeapMarkRootSet标记根集对象。

第4到第6步用于并行GC:

4.  调用函数dvmClearCardTable清理Card Table。因为接下来我们将会唤醒第1步挂起的线程。并且使用这个Card Table来记录那些在GC过程中被修改的对象。

5.  调用函数dvmUnlock解锁堆。这个是针对调用函数dvmCollectGarbageInternal执行GC前的堆锁定操作。

6.  调用函数dvmResumeAllThreads唤醒第1步挂起的线程。

第7步用于并行和非并行GC:

7.  调用函数dvmHeapScanMarkedObjects从第3步获得的根集对象开始,归递标记所有被根集对象引用的对象。

第8步到第11步用于并行GC:

8.  调用函数dvmLockHeap重新锁定堆。这个是针对前面第5步的操作。

9.  调用函数dvmSuspendAllThreads重新挂起所有的线程。这个是针对前面第6步的操作。

10. 调用函数dvmHeapReMarkRootSet更新根集对象。因为有可能在第4步到第6步的执行过程中,有线程创建了新的根集对象。

11. 调用函数dvmHeapReScanMarkedObjects归递标记那些在第4步到第6步的执行过程中被修改的对象。这些对象记录在Card Table中。

第12步到第14步用于并行和非并行GC:

12. 调用函数dvmHeapProcessReferences处理那些被软引用(Soft Reference)、弱引用(Weak Reference)和影子引用(Phantom Reference)引用的对象,以及重写了finalize方法的对象。这些对象都是需要特殊处理的。

13. 调用函数dvmHeapSweepSystemWeaks回收系统内部使用的那些被弱引用引用的对象。

14. 调用函数dvmHeapSourceSwapBitmaps交换Live Bitmap和Mark Bitmap。执行了前面的13步之后,所有还被引用的对象在Mark Bitmap中的bit都被设置为1。而Live Bitmap记录的是当前GC前还被引用着的对象。通过交换这两个Bitmap,就可以使得当前GC完成之后,使得Live Bitmap记录的是下次GC前还被引用着的对象。

第15步和第16步用于并行GC:

15. 调用函数dvmUnlock解锁堆。这个是针对前面第8步的操作。

16. 调用函数dvmResumeAllThreads唤醒第9步挂起的线程。

第17步和第18步用于并行和非并行GC:

17. 调用函数dvmHeapSweepUnmarkedObjects回收那些没有被引用的对象。没有被引用的对象就是那些在执行第14步之前,在Live Bitmap中的bit设置为1,但是在Mark Bitmap中的bit设置为0的对象。

18. 调用函数dvmHeapFinishMarkStep重置Mark Bitmap以及Mark Stack。这个是针对前面第2步的操作。

第19步用于并行GC:

19. 调用函数dvmLockHeap重新锁定堆。这个是针对前面第15步的操作。

第20步用于并行和非并行GC:

20. 调用函数dvmHeapSourceGrowForUtilization根据设置的堆目标利用率调整堆的大小。

第21步用于并行GC:     

21. 调用函数dvmBroadcastCond唤醒那些等待GC执行完成再在堆上分配对象的线程。

第22步用于非并行GC:

22. 调用函数dvmResumeAllThreads唤醒第1步挂起的线程。

第23步用到并行和非并行GC:

23. 调用函数dvmEnqueueClearedReferences将那些目标对象已经被回收了的引用对象增加到相应的Java队列中去,以便应用程序可以知道哪些引用引用的对象已经被回收了。

以上是关于Dalvik虚拟机,Android4.4以上使用的是ART虚拟机。和垃圾收集器相关的辅助结构和Dalvik虚拟机的类似。

ART虚拟机堆主要由四个空间组成,其中Image Space、Zygote Space、Allocation Space是连续空间的,而Large Object Space是一些离散地址的集合。在Image Space和Zygote Space之间隔着一段用来映射system@framework@boot.art@classes.oat 文件的内存,该文件是从系统启动类路径中的所有dex文件翻译得到,而Image Space就包含这些需要预加载的系统类对象,因此,这些系统类对象保存在system@framework@boot.art@classes.dex中,可直接将文件system@framework@boot.art@classes.dex映射到内存中。Zygote Space和Allocation Space和Dalvik虚拟机中的Zygote堆和Active堆一样,Zygote进程一开始只有一个Image Space和Zygote Space,在Zygote进程fork第一个子进程之前,Zygote Space就会一分为二,新的叫Allocation Space,后面的对象都在Allocation Space分配。

和Dalvik虚拟机类似的,也包括一个Card Table、Live Bitmap和Mark Bitmap, 作用分别和Dalvik一致,Card Table用来标记非垃圾堆对垃圾堆中对象的引用,Live Bitmap比较上一次GC存活的对象,Mark Bitmap标记当前GC有被应用的对象。ART虚拟机针对非连续空间的Large Space, 同样也用了两个辅助结构Live Object Map和Mark Object Map用于辅助垃圾回收。另外,使用了三个object Stack来辅助垃圾回收,这几个stack的作用和Dalvik虚拟机的作用类似,用于递归标记对象的引用。另外为了充分利用多核能力,在GC阶段, Card Table和Mod Union Table分三步处理,第一步是调用辅助结构ModUnionTable类的成员函数ClearCards清理Card Table里面的Dirty Card,并且将这些Dirty Card记录在Mod Union Table中。第二步是调用Mod UnionTable类的成员函数Update将遍历记录在Mod Union Table里面的Dirty Card,并且找到对应的被修改对象,然后将被修改对象引用的其他对象记录起来,第三步是调用Mod Union Table类的成员函数MarkReferences标记前面第二步那些被被修改对象引用的其他对象。这样就使得Card table在标记阶段重复使用。

ART新对象创建分配的过程和Dalvik虚拟机类似,不同点在于垃圾收集的方式和策略不同。

Dalvik虚拟机在不回收软引用回收垃圾之后,尝试分配内存不成功的情况下,会将Java堆的当前大小设置为虚拟机启动指定的Java堆最大值。而ART会尝试进行从小力度到大力度的垃圾回收之后,若还无法成功,则在允许范围内增加堆大小进行分配,若仍然无法成功,则进行Full GC,回收只被软引用对象引用的对象,然后在允许范围内增加堆大小进行分配,这次如果仍然失败,就说明内存不足了。

ART的垃圾收集过程如下:

非并发GC

1. 调用子类实现的成员函数InitializePhase执行GC初始化阶段;

2. 挂起所有的ART运行时线程;

3. 调用子类实现的成员函数MarkingPhase执行GC标记阶段;

4. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段;

5. 恢复第2步挂起的ART运行时线程;

6. 调用子类实现的成员函数FinishPhase执行GC结束阶段。

并发GC

1. 调用子类实现的成员函数InitializePhase执行GC初始化阶段;

2. 获取用于访问Java堆的锁;

3. 调用子类实现的成员函数MarkingPhase执行GC标记阶段;

4. 释放用于访问Java堆的锁;

5. 挂起所有的ART运行时的线程;

6. 调用子类实现的成员函数HandleDirtyObjectsPhase处理在GC并行标记阶段被修改的对象;

7. 恢复第4步挂起的ART运行时线程;

8. 重复第5步到第7步,直到所有在GC并行阶段被修改的对象都处理完成。

9. 获取用于访问Java堆的锁;

10. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段;

11. 释放用于访问Java堆的锁;

12. 调用子类实现的成员函数FinishPhase执行GC结束阶段。

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

推荐阅读更多精彩内容