回顾_打开 ART大门_执行篇|青训营笔记

198 阅读10分钟

这是我参与「第四届青训营 」笔记创作活动的的第7天

经过了上一期对象篇之后,我们对ART中对象的生命旅程有了一定的认识

现在让我们来看看他是怎么工作的吧

搬砖的姿势—虚拟机的执行方式

image-20220830200225955

这里面解释执行就是我们初期的Dalvik虚拟机来操作-Android 2.x

每次运行程序(打开APP)的时候,Dalvik负责将dex翻译为机器码交由系统调用。

这样有一个缺陷

每次执行代码,都需要Dalvik将操作码代码翻译为机器对应的微处理器指令,

然后交给底层系统处理,运行效率很低

为了提升效率又改进了一下这种翻译的方式,

引入 JIT即时编译技术-Android 4.x

当App运行时,每当遇到一个新类,JIT就会对这个类进行即时编译,经过编译后的代码,

会被优化成相当精简的原生型指令码(即native code),这样在下次执行到相同逻辑的时候,速度就会更快。

JIT 编译器可以对执行次数频繁的 dex/odex 代码进行编译与优化

将 dex/odex 中的 Dalvik Code(Smali 指令集)翻译成相当精简的 Native Code 去执行,JIT 的引入使得 Dalvik 的性能提升了 3~6 倍。

[ 如何理解编译和解释?

编译就是,你妈妈做好了一桌子饭菜,而你只需要在房间里面等待你妈妈的呼唤,

解释就是,你去火锅店吃饭,服务员上了一桌子菜,这时候需要你一边煮一边吃 ]

这时候又出问题了

  • 每次启动应用都需要重新编译(dex字节码翻译成本地机器码是发生在应用程序的运行过程中的,)
  • 运行时比较耗电,耗电量大

这时候Google苦不堪言,决定推翻重来!

ART与AOT

所以Google在4.4之后推出了ART,用来替换Dalvik。

使用AOT( Ahead Of Time ) ,

它是静态编译,应用在安装的时候会启动 dex2oat 过程把 dex预编译成 ELF 文件,每次运行程序的时候不用重新编译。

ART的策略与Dalvik不同,在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。之后打开App的时候,不需要额外的翻译工作,直接使用本地机器码运行,因此运行速度提高。

这时候性能好起来了,但是谷歌又发现,

时间从运行的时候转换到了App安装时间时刻,

又想念老大哥的快速安装了,

谷歌就在思考能不能平衡一下,把兄弟伙都照顾到

ART引入了全新的Hybrid模式(Interpreter + JIT + AOT)-Android 7.0/7.1

所以谷歌就采用了如下方式

  1. App在安装时不编译, 所以安装速度快。

  2. 在运行App时, 先走解释器, 然后热点函数会被识别,并被JIT进行编译, 存储在jit code cache, 并产生profile文件(记录热点函数信息)。

  3. 等手机进入充电(charging)和空闲(idle)状态下, 系统会每隔一段时间扫描App目录下profile文件,并执行AOT编译(Google官方称之为profile-guided compilation)。

  4. 不论是jit编译的binary code, 还是AOT编译的binary code, 它们之间的性能差别不大, 因为它们使用同一个optimizing compiler进行编译。

Just-In-Time (JIT)

image-20220830200536026

技术扩展:OSR(On-Stack Replacement)

JIT的OSR就是栈上替换,在某些分支可以采用更激进的优化,处理不了还能回退,既可以替换成优化后的,也能回到优化前

理解例子:他就好像我们现在的真人秀评选节目,评委会根据选手的情况进行打分

Ahead Of Time(AOT)

和JIT不同,是在程序运行之前,对APK中的函数进行编译。

1.和程序是否运行无关

2.编译的范围不是以函数为单位,而是以dex为单位的。

3.结果会持久化。

image-20220830201356435

继续引用老师的例子:

AOT就像歪嘴龙王,默默努力然后惊艳所有人

image-20220830201924776

思考:为什么这种编译会提升速度呢?

这时候就要提及到函数执行的一个提及—延迟绑定

  1. 绑定的越迟,动态性越好,性能越差

  2. 绑定的越早,动态性越差,性能越好

在上一篇中我们提及到,一个对象的生成需要去找他的爸爸找他的爷爷,才能确定大小

那么在我们的编译运行中就提前确定好了大小,我们就可以直接调用对象

image-20220830202422556

这时候大聪明发出疑问,为什么我不直接把编译好放在APK中呢,而是等到手机上去延迟绑定呢?

这是因为在Android版本迭代的问题导致的:

老板使用的是华为手机(Adnroid 10),在此系统上工程师定义的View大小是10

我使用的是小米手机(Adnroid 11),在此系统上工程师觉得可以优化一些东西,我们View的大小就变成了8

这时候大小就会不一致了,我们在编写代码的时候是无法预估我们产品将会运行到什么系统上的,

所以就需要用到我们的延迟绑定,将这个流程放到手机上执行。

开始和结束一栈管理

了解了他是怎么执行的,那么来看看他内部是怎么运作的吧:

ART对于解释执行编译后指令采用两种不同的策略

  1. 对于解释执行,栈托管到虚拟机完成。
  2. 对于编译后的,压栈处理和native代码是一样的,遵从对应指令集的约定

可以理解成,解释执行压栈的每一步,都是托管到虚拟机的 而oat(aot编辑的产物)或者jit的处理,实际上已经和native一样,作为指令被固化在函数头自己根据arm调用约定来执行

思考:在有的函数是解释执行,有的是编译执行,那么系统是怎么在不同执行方式之间的切换的呢?

对于AOT、JIT到解释执行,或者反之的调用,ART采用trampoline-bridge机制来进行切换

我们可以理解成对两种语言之间设置好映射,做了两个世界的翻译

image-20220830211540080

就引申到栈管理了,我们接下来就对比下这两种执行方式调用时候压栈的处理有什么不同?

通过上面的例子,我们可以得出如下两个不同

  1. 压栈-出栈的速度不同,解释执行的速度慢

  2. 解释执行的栈结构托管的,编译执行栈结构是遵从虚拟机规则

  3. 解释执行传递参数额外的空间成本编译执行没有

  4. 不同执行方式之间调用切换采用trampoline/bridge进行

扩展:回栈除了函数返回,还有什么可能会导致回栈?

这时候就要提到我们的异常处理—人生哪有一帆风顺呢

image-20220830212658569

那么异常处理是怎么执行的呢?

当拿到一个异常的时候,会逐级的回栈,做两个事情:

  1. 回一级栈看看要不要

  2. 如果不要就跳出

还有一点值得关注的是,我们在做着两件事情的时候,会存在一种特殊情况,就是解释执行和编译执行之间的栈穿透

image-20220830212914089

高效的执行—多线程

了解了它是如何执行的,这时候资本主义不满意了

一个小人干活肯定不过瘾嘛,要多找几个小弟,于是就有了多线程的需要 这个就是一个典型的原子性问题

课堂上先引用了一个小例子:

thread1 和thread2是两个线程的执行函数,有一个静态变量doubleCheck,右图的代码的执行结果有哪些可能?

image-20220830213734103

有没有可能会打出一堆fail但是一个success也打不出来?

答案是可能的,因为CPU是有Catch的,有可能访问的doubLeCheck都是自己内部的变量,它们之间的内存可能是没有同步的。

前面讲了执行,讲了异常,感觉好像很慢呀好多额外的事情要处理,那怎么能更好的利用CPU呢?

多线程必然带来原子性问题,那ART怎么解决的呢?

image-20220830214155373

synchronize

image-20220830214444717

sync本质是封装了加锁和解锁。

那么图中的monitor又是什么呢?

这时候就可以回到我们的对象篇中提到的Object基类的结构中

image-20220830214805520

此时我们就知道,shadow monitor是干嘛用的了,如果我们对一个对象使用了sync,

这个对象就会生成一个lock,保留在这个shadow monitor指向的内存地址中。

扩展:八股文里面不是还有什么胖瘦锁,没见到呀?

  1. 首先什么是胖瘦锁呢?

    瘦锁:就像你打游戏要开团时,队友的大招没好,你就会急促的一直在询问"好了吗好了吗,好了一起放技能”

    特点:费劲,但是响应快,

    实现上采用spinlock

image-20220830215047678

​ 胖锁就是:当你在写作业时,父母在旁边监督,如果父母在旁边一直说 "好了吗好了吗,赶紧写!”--那你肯定烦死了

​ 这时候你就让你父母去干点别的事情(父母刷抖音),等你写完了再去通知父母

​ 特点:省力,但是反应慢

​ 实现上采用mutexlock

image-20220830215223224

  1. 胖瘦锁之间是怎么切换的呢?

image-20220830214924327

这个shadow$ monitor 指向了一个瘦锁,是一个原子变量,满足一定条件之后,会升级成胖

比如说当瘦锁唤醒的次数达到一个阈值,就去会切换(由虚拟机控制)

image-20220830215533641

monitor保存的所有两个形态

因为ART虚拟机搞不清楚具体你是在等技能还是在写作业,因此采用持有状态+检测条件的方式

如果你一直在那开团(小黑子漏出了鸡脚)、放技能,那他也只会识别成需要胖锁,如果我们需要这种情况,

则需要使用volatile去自己实现

volatile是Java提供的一种轻量级的同步机制。

定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量

被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

所以它不会引起线程上下文的切换和调度。但是volatile它能保证读的安全,但是没保证写的安全,所以在使用上会出错

虚拟机为了通用性,没有办法做的十全十美

总结

本章主要介绍了三个问题

  1. 函数有哪几种执行方式?
  2. 出现问题了,异常被抛出的
  3. 提高效率引入并发,并发怎么控制的?

通过老师的讲解,我们不仅了解到了ART的发展还有基础概念,更是对JAVA部分的一些代码有了一定的认识,

但是这才是刚打开了ART的大门,里面蕴含的各种知识还有待我们进一步深入研究!

参考文章

说一说Android的Dalvik,ART与JIT,AOT

Java中volatile关键字的最全总结

Java并发编程:volatile关键字解析

萌新初学,本文为笔记,大佬若有更好的见解欢迎评论区留言