打开ART大门|青训营笔记

86 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的第7天,上篇聊完了性能优化,这一片谈谈ART,Android运行时。 首先来几张图介绍一下ART的演进和整体价构: image.png 一开始Android没有自己的运行时,使用dvlvik,但是dalvik撑不起大梁,随着Android的更新迭代,ART不断引入新的特性,愈加完善。ART的整体结构如图所示:

image.png 分为两层,执行层和运行时,执行层负责对直接书写的代码进行处理,运行时提供Java语言机制的支持,本篇文章也将按照这两个层次进行介绍。 在这里先抛出两个问题,对象是如何分配出来的?虚拟机为了保证代码高效执行,需要提供哪些机制? 首先我们来介绍对象,一言不合就上图:

image (39).png 图里向我们展示了一个对象的生命旅程。要想回答第一个问题,我们要了解类管理的概念。类管理可以决定一个对象的大小和行为。类主要描述一个对象的内存布局和函数信息,而内存布局体现在类成员的大小,类型和排布上。函数信息则主要是虚表的信息(某个函数定义在当前函数表的第几个位置。因为Java支持继承,因此其类的内存布局和函数虚表需要全部展开以后才能真正确认(及动态性的来源))。接着是基类Object的秘密,通过下面这张图我们可以看到积累是如何定义的

image (40).png

image (41).png

介绍完基类,就需要了类加载,类分配的对象大小,由继承链决定,JAVA类只有在第一次使用时才会进行加载。下面多图预警:下面通过继承链来展示一个完整的对象内存布局:

image (42).png 谈到了继承就不得不提双亲继承,本质上双亲委派就是人为规定的为了确保系统内同一个类一致性的规矩。 合理的抽象不是图方便把功能全添加到顶层基类上。 接着来说说内存分配:

image.png 如图APP的Java对象内存分配上是托管到虚拟机上来处理的,不会直接向操作系统申请,对操作系统的内存占用和布局是由VM控制的(预留-扩展),不同分配器有独特的特点,如图:

image.png TLAB是每个线程的局部cache,体积小,分配快,ROSallocator直接从虚拟内存堆区取得tlab

image.png 如图以实例形式介绍了不同分配器的适用场景和特点。 对于小规模的财产可以直接从自己支配,而大额的内存就需要到父母(操作系统去取)

image (43).png ART内存分配的根本远离还是给使用者在最优的范围内找到大小合适的连续内存。这样产生的问题就是内存碎片化,会造成内存资源的浪费。因此就需要对这些碎片进行回收,就是常说的内存回收,思路主要有两种。一种是我们常见的GC,垃圾回收,定期查找系统不用的对象,并释放占用的内存。另一种则是引用计数RC,对一个对象进行计数,多一个引用者数字+1,少一个就减一,为0就释放。

image (44).png 其中RC的问题如图,认不出环引用,因此又引入了弱引用和手动标记,如图所示:

image (45).png 那么ART中内存引用是怎么划分的呢,分为三种,强引用(直接持有的引用),软引用(内存不够的时候就会回收),弱引用(只要触发GC就会回收)。那么GC只会在内存不足的时候触发吗,显然不是,触发GC的情况有两种,如图所示

image.png 如果不想被超出预期的GC影响导致卡顿,可以预留适当的内存;大小有上限可预期的情况下,大数组可能比分配到一堆容器要好。 也正是由于不同的内存回收机制,才使得相同情况下IOS设备需要的内存更少,对于IOS而言使用RC机制,不需要经常使用GC导致卡顿,每个APP内存空间里只有内存。而安卓有GC机制,但又不是时时触发,每个APP内存空间里积累的垃圾就会占掉一部分内存。gc主要有三种方法,tracing GC(从root出发所有标记的对象都是有持有者的,未标记的都被清理掉),cpoyingGC,将内存分为两份,其中一份空间被占满后,将有用的复制到的另一半,然后直接清空这部分)ART的做法如图所示:

image.png finalize方法一般用来跟随对象的生命周期,清理掉绑定的native资源,流程如图:

image.png 要注意,一个对象的finalize只会执行一次,再次激活后的对象不会触发finalize。 谈完对象,我们聊聊代码执行,具体流程如图:

image.png 解释执行由解释器完成,JIT和OAT采用编译之后直接执行的指令。 JIT的执行流程如图:

image.png 先通过解释执行选出最受欢迎的函数,再由JIT执行,其中OSR是栈上替换,在某些分支上可以采用更激进的优化即可以替换成优化后,也可以替换成优化前。OAT或者叫AOT与JIT不同,其在运行对APK中的函数进行编译,与程序是否运行无关;不以函数为单位,而是以dex为单位,结果会持久化。 接着是一个重要的特性:延迟绑定,绑定的越迟,动态性越好,性能越差,绑定的越早,动态性越差,性能就越好。实例如图:

image.png 接着是栈管理:ART对于解释执行和编译后指令采用两种不同的策略,对解释执行,栈托管到虚拟机执行;对于编译后的,压栈处理和native代码是一样的,遵从对应指令集的约定。

image.png ART采用trampoline-bridge机制实现AOT,JIT到解释执行,或者反之的调用来实现。 简单总结一下;压栈和出栈的速度不同,解释执行的速度慢,解释执行的栈结构是托管的,编译执行的栈结构是遵从虚拟机规则的;解释执行传递参数有额外的空间成本,编译执行没有,不同执行方式之间调用切换采用trampoline-bridge机制实现。 然后是异常处理,当拿到一个异常的时候,会逐级回栈做两个事情,回一级栈看要不要,如果不要就跳出