这是我参与「第四届青训营 」笔记创作活动的第12天
第八节:打开 ART 虚拟机的大门
ART(Android Runtime)
下图中黄色的部分,就是ART,给APP和system server进程提供运行环境,包括JAVA语法支持,托管,性能优化,debug等等一系列的能力。
ART架构
ART可以粗略的划分成两个层级,执行层和runtime层,执行层负责直接面向java代码的产物,来翻译或者编译执行。runtime层则是提供java语法特性和其他一些支撑运行的底层机制。
执行层(compiler & interpreter): 解释器、JIT、AOT
运行层(runtime):内存管理、异常、线程、类管理、String、JNI、intrinsic、调试、monitor等等
课程包含两条主线:
- 我们的对象是怎么分配出来的?
- 虚拟机为了保证我们的代码高效顺利执行,需要提供哪些机制?
1.对象
- 对象是什么 - 类的管理
- 对象怎么分配的 - 内存分配
- 对象怎么回收 - 内存回收
类的管理
类信息
类主要描述的是一个对象的内存布局和函数信息
内存布局:类成员的大小,类型,和排布
函数信息:主要是虚表的信息,某个函数定义在当前类函数表的第几个位置
因为 Java 是支持继承的,因此类的内存布局和函数虚表需要做继承链全展开以后才能真正确认(这也是动态性的来源)
Object类的定义和结构
类加载
第一次分配对象大小,是由继承链决定的
Java 的类,是在第一次使用的时候,才会进行加载
一个类的直接父类和间接父类和自己的所有成员的大小就是这个类的大小
内存布局
双亲委派
双亲委派是人为划定的一个规矩,目的是为了保证系统内同一个类的一致性。
内存分配
APP 的 Java 对象内存分配上是托管到 VM 来处理的,并不会直接向操作系统去申请,实际上对 OS 内存的占用和内存布局,是 VM 控制的(预留 - 扩展)
应用:APP
堆:ART vm heap、native heap
操作系统:linux kernel 内存管理
分配器
TLAB 是每个线程一个局部的 cache ,体积小,分配快
ROSallocator 直接从虚拟机的堆区去取(VM mem pool)
LOSallocator 从操作系统层(Linux)取
应用场景
(bitmap 在Android 高版本里被隐藏到了 native,不直接使用虚拟堆)
买一根冰棍 微信零钱 少量内存:临时变量 -> TLAB
买一部手机 存款,理财 中等内存:数组/容器 -> ROSallocator
买一栋房子 父母 大量内存:bitmap 存储图片 -> LOSallocator
内存也是一样的道理,少量的零钱,讲的是方便,使用快捷,但是量少,而大块的内存动用起来慢,要从大池子里面取,集中管理。
内存碎片
ART 内存分配的根本原理,是给使用者在最优的范围内找到一块大小符合的连续内存
实例:36吨的坦克只能用运输带运送,36堆的沙子既可以用运输带也可以用快递小车
动动手:在testapp中,分配10KB * 100000,和100M * 10,看看是否都能顺利申请到?
val mylist: LinkedList<ByteArray> = LinkedList()
for (i in 0..99999) {
mylist.add(ByteArray(10 * 1024))
}
for (i in 0..9) {
mylist.add(ByteArray(100 * 1024 * 1024))
}
都申请不下来,报了异常
10KB * 100000
java.lang.OutOfMemoryError: OutOfMemoryError thrown while trying to throw an exception; no stack trace available
100M * 10 打印了一堆栈帧信息
java.lang.OutOfMemoryError: Failed to allocate a 104857616 byte allocation with 8388608 free bytes and 89MB until OOM, target footprint 115793048, growth limit 201326592
分配算法
内存回收
GC:垃圾回收(Garbage Collection),定期查找徐通内不用的对象,并且释放占用的内存
RC:引用技术(Reference Counting),指的是堆一个对象引用进行计数,多一个引用者则计数器+1,少一个就-1,为0就释放,是一种即时回收机制
比如 IOS的 Swift 就用的 RC 进行内存管理
RC 认不出环引用,所以引入了弱引用和手动标记
ART 的引用
强引用:直接持有,不会被回收
软引用:内存不够时会被回收
弱引用:只要触发 GC 就会被回收
触发 GC 的条件
-
没内存了
-
该 GC 了(你妈觉得你冷,系统觉得需要GC)
- VM堆占用达到水位
- 系统内存紧张
- 任性了,想触发了
如果想不被预期外的GC导致卡顿,可以考虑适当的预留内存
大小由上线可预期的情况, new 一个大数组,可能比分配一大堆放到容器里面要好。
苹果手机内存需求少的原因
同样是运行 APP,IOS 每个APP内存空间里只有在用内存(因为RC会对内存进行及时回收)。
而 Android 则因为 GC 是定时触发,不会时刻都在清理,所以每个应用都会维护一个垃圾堆,应用多了,这个垃圾堆就会变得很大。
数据:苹果手机一般 4G 内存就够用了,所以迭代好久都不加,安卓可能 16G 都不一定够用,所以一直提供有加内存的选项
GC 的方式
GC roots 的概念:这么判断哪些内存是有用的哪些是没用的?
四大引用:栈、static 变量、native ref、VM保留
栈 和 static 变量引用 了 对象1和对象2
native ref 引用了对象2,对象2 引用了 对象4
对象3 没有对象指向(被回收)
tracing GC
从 roots 开始遍历,所有 mark 对象都是有 holder 的,释放掉没用 holder 的 object
copying GC
从 roots 遍历,把有用的对象拷贝到另一个区域,然后集中地释放掉当前区域地内存
案例:搬家地两种方式
- 找出有用的,烧了没用的 (copying GC)
- 找出没用的,烧了(tracing GC)
两种 GC 的比较
| 前台GC | 后台GC | |
|---|---|---|
| 使用场景 | 应用在前台的进程 | 非前台应用进程,service/push 进程 |
| 算法 | tracing GC | copying GC |
| 速度 | 快 | 慢 |
| 内存碎片 | 有 | 无 |
| 额外空间 | 不需要 | 需要 |
案例:保存一个班的学生信息
虽然一般的八股文里会说如果增删多,使用链表,具有可扩展性,如果查询较多,使用数组性能好,但是增删慢。
但是从内存的角度看,离散的分配十次对象和将十个对象放在一起分配,是不经济的,还可能加剧内存碎片化的情况。
回收之后
finalize 方法一般用来跟随对象的生命周期,清理掉绑定的 native 资源
一个对象的 finalize 方法只会执行一次,再次激活只会的对象是不会触发 fianlized 的
动动手:能分配一个弱引用,一个软引用,然后强制gc,观察是否为空
String softString = new String("soft");
SoftReference<String> mySoftRef = new SoftReference<>(softString);
String weakString = new String("weak");
WeakReference<String> myWeakRef = new WeakReference<>(weakString);
System.gc();
//get看看mySoftRef myWeakRef
2.执行
- 虚拟机的执行方式 - JIT/AOT/解释
- 开始和结束 -- 栈管理
- 高效的执行 -- 多线程
虚拟机的执行方式
三种:解释执行,JIT ,AOT
解释执行由解释器完成,JIT 和 AOT 则在编译之后执行,此时直接执行的已经是指令了
JIT (Just in time)
JIT 的 OSR 即栈上替换,某些分支还能采用更激进的优化,处理不来还能回退
类似选秀节目,先通过解释执行选出最受欢迎的函数,交给导师组特训(优化)
AOT
AOT(或者叫 OAT)是在程序运行之前,堆APK中的函数进行编译
- 和程序是否允许无关
- 编译的范围不是以函数为单位,而是以 dex 为单位的
- 结果会持久化
为什么 AOT 一定要在端侧编译?为什么不在 APK 发布的时候,就直接发布 AOT 以后的应用?
- 不需要额外维持一块内存来保存 JIT 的结果
- 不需要预热
- 但是不能动态调整,没有 OSR 来支持栈上的替换
延迟绑定
绑定得越迟,动态性越好,性能越差
绑定得越早,动态性越差,性能越好
安卓生态比较糟糕,不同手机用得安卓版本不同(比如一个是安卓 11 一个是安卓10),无法事先确定,只能在手机端执行。往往只有在端侧(即在手机运行端上)才能获取到自己类的完整信息。
栈管理
ART 对于解释执行和编译后指令采用两种不同的策略
- 对于解释执行,栈托管到虚拟机完成
- 对于编译后的,压栈处理和native代码是一样的,遵从对应指令集的约定
压栈、参数、返回、弹栈
异常
触发弹栈除了返回,还有抛异常
多线程
shadow monitor
如果对一个 sync 这个对象会生成一个 lock
瘦锁
spinlock
打游戏等技能好
胖锁
问作业写完没
mutelock
QA
- 强引用怎么样都不会被 GC ?
有隐含条件,不能是孤悬的(两个强引用互指),得从 roots 能遍历到的对象才不会被回收
-
有虚拟机的语言相比编译型语言优势和劣势
- 主要是动态性不一样,有虚拟机动态性高
- 内存和性能
- 能看到的作用域有限,无法像 C++ 一样做全展开
- ART 和 JVM(hotspot)
原理上比较像,但是关系不大
ART 是给Android 定制的虚拟机,底层做了很多黑科技