java虚拟机学习笔记(三)——不是虚拟机的安卓运行时

一、发展简述

1.1 初期——Dalvik

  在 Android 系统初期,不同于 Java 平台使用 JVM 加载字节码文件(.class),Android 系统由 Dalvik 担任虚拟机的角色,每次运行程序的时候,Dalvik 负责加载 dex/odex 文件并解析成机器码交由系统调用。Dalvik虚拟机是基于apache的java虚拟机,并被改进以适应低内存,低处理器速度的移动设备环境。

1.2 引入JIT

  为了适应硬件速度的提升,Android 系统系统也在不断更新,单一的 Dalvik 虚拟机已经渐渐地满足系统的要求了,2010 年 5 月 20 日,Google 发布 Android 2.2(Froyo冻酸奶),在这个版本中,Google 在 Android 虚拟中加入了 JIT 编译器(Just-In-Time Compiler)。
  Dalvik 使用 JIT 进行即时编译,借助 Java HotSpot VM,JIT 编译器可以对执行次数频繁的 dex/odex 代码进行编译与优化,将 dex/odex 中的 Dalvik Code(Smali 指令集)翻译成相当精简的 Native Code 去执行,JIT 的引入使得 Dalvik 的性能提升了 3~6 倍。

1.3 ART 诞生

  2013 年 10 月 31 日,Google 发布 Android 4.4(奇巧Kitkat),带来了全新的虚拟机运行环境 ART(Android RunTime)的预览版和全新的编译策略 AOT(Ahead-of-time),这个时候 ART 是和 Dalvik 共存的,用户可以在两者之间进行选择。
  2014 年 10 月 16 日,Google 发布 Android 5.0(棒棒糖Lollipop),ART 全面取代 Dalvik 成为 Android 虚拟机运行环境,至此,Dalvik 退出历史舞台,AOT 也成为唯一的编译模式。
  AOT 和 JIT 的不同之处在于:JIT 是在运行时进行编译,是动态编译,并且每次运行程序的时候都需要对 odex 重新进行编译;而 AOT 是静态编译,应用在安装的时候会启动 dex2oat 过程把 dex 预编译成 ELF 文件,每次运行程序的时候不用重新编译,是真正意义上的本地应用。

1.4 JIT回归

  在 Android 5.x 和 6.x 的机器上,系统每次 OTA 升级完成重启的时候都会有个应用优化的过程,这个过程就是刚才所说的 dex2oat 过程,这个过程比较耗时并且会占用额外的存储空间。
  2016 年 8 月 22 日,Google 发布 Android 7.0(牛轧糖Nougat),JIT 编译器回归,形成 AOT/JIT 混合编译模式,这种混合编译模式的特点是:
  应用在安装的时候 dex 不会被编译
  应用在运行时 dex 文件先通过解析器(Interpreter)后会被直接执行(这一步骤跟 Android 2.2 - Android 4.4之前的行为一致),与此同时,热点函数(Hot Code)会被识别并被 JIT 编译后存储在 jit code cache 中并生成 profile 文件以记录热点函数的信息。
手机进入 IDLE(空闲) 或者 Charging(充电) 状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。
  可以看出,混合编译模式综合了 AOT 和 JIT 的各种优点,使得应用在安装速度加快的同时,运行速度、存储空间和耗电量等指标都得到了优化。

二、Dalvik和传统JVM的区别

  严格来说,Dalvik并不是java虚拟机,它并没有遵循JVM的实现规范。Dalvik与JVM在以下几个方面有明显不同:

  1. 架构
      java虚拟机基于栈,基于栈的机器必须使用指令来载入和操作栈上数据;Dalvik虚拟机基于寄存器。
  2. 字节码
      java虚拟机运行的是java字节码。(java类会被编译成一个或多个字节码.class文件,打包到.jar文件中,java虚拟机从相应的.class文件和.jar获取相应的字节码)。Dalvik运行的是自己专属的.dex字节码格式。(java类被编译成.class文件后,会通过一个dx工具将所有的.class文件转换成一个.dex文件,然后Dalvik虚拟机会从其中读取指令和数据)
  3. 进程模型
      JVM设计上是一个JVM运行多个java程序实例,然而DVM允许每个应用程序允许在自己独立的DVM中,每个DVM运行在独立的进程空间,放在了某个应用崩溃的时候影响到其他应用进程。
  4. 共享机制
      DVM有预加载-共享的机制,不用应用之间在运行时可以共享相同的类,拥有更高的效率。JVM不存在这种共享机制,不同的程序打包后是独立的。
  5. DVM早期没有使用JIT编译
      JVM使用了JIT编译器,而DVM早期没有使用JIT编译器。早期的DVM每次执行代码,都需要通过解释器将dex代码编译成机器码,然后交给系统处理,效率不是很高。从Android 2.2版本开始,DVM使用了JIT编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器码(Native Code),这样在下次执行到相同逻辑的时候,直接使用编译之后的本地机器码,而不是每次都需要编译。但是应用程序每一次重新运行的时候,都要重做这个编译工作,因此每次重新打开应用程序,都需要JIT编译。

三、DVM和ART的区别

  ART是一种在Android操作系统上的运行环境,ART能够在第一次安装的时候,把应用程序的字节码转换为机器码。采用了预编译(AOT,Ahead-Of-Time)技术。DVM和ART的区别主要有4点:

  1. JIT与AOT
      DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这会使得应用程序的运行效率降低。而在ART中,系统在安装应用程序时会进行一次AOT(ahead of time compilation,预编译),将字节码预先编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提升,设备的耗电量也会降低。
      采用AOT也会有缺点,主要有两个:第一个是AOT会使得应用程序的安装时间变长,尤其是一些复杂的应用,第二个是字节码预先编译成机器码,机器码需要的存储空间会多一些。为了解决上面的缺点,Android 7.0版本中的ART加入了即时编译器JIT,作为AOT的一个补充,在应用程序安装时并不会将字节码全部编译成机器码,而是在运行中将热点代码编译成机器码,从而缩短应用程序的安装时间并节省了存储空间。
  2. 处理位数
      DVM是为32位CPU设计的,而ART支持64位并兼容32位CPU,这也是DVM被淘汰的主要原因之一。
  3. 垃圾回收
      ART对垃圾回收机制进行了改进,比如更频繁地执行并行垃圾收集,将GC暂停由2次减少为1次等。
  4. 内存划分
      ART的运行时堆空间划分和DVM不同。

由此可以总结出ART的优缺点:

ART优缺点

四、垃圾回收

4.1 Dalvik垃圾回收

  Dalvik虚拟机比其他Java虚拟机中的垃圾收集要简单一些, 因为没有进行内存整理(no compacting)。也就是说堆内存中的对象在创建之后其地址永远都不会发生改变, 使得虚拟机其余部分的实现变得相对简单。

4.1.1 DVM运行时堆

  ​Android 4.x中DVM运行时堆使用的是标记-清除算法进行GC。DVM的运行时堆包括两个Space和一些辅助数据结构:

DVM运行时堆

4.1.2 引起DVM发生GC的原因

  有以下几种:

  1. GC_CONCURRENT:当堆开始填充时,并发GC可以释放内存。

2 .GC_POR_MALLOC:当堆内存已满时,App尝试分配内存而引起的GC,系统必须停止App并回收内存。

  1. GC_HPROF_DUMP_HEAP:当请求创建HPROF文件来分析堆内存时出现的GC。

  2. GC_EXPLICIT:显式的GC,例如调用System.gc()(应该避免调用显式的GC,信任GC会在需要时运行)。

  3. GC_EXTERNAL_ALLOC:仅适用于API级别小于等于10,且用于外部分配内存的GC。

4.2 ART垃圾回收

  ​与DVM的GC不同的是,ART 采用了多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认是采用了CMS(Concurrent Mark-Sweep)方案,该方案主要使用了sticky-CMS和partial-CMS。根据不同的CMS方案,ART的运行时堆的空间也会有不同的划分。

4.2.1 ART运行时堆

  默认的ART运行时堆由4个Space和多个辅助结构组成:

ART运行时堆

  采用标记-清除算法时。DVM和ART的空间划分如下:

空间划分

4.2.2 引起ART发生GC的原因

  有以下几种:

  1. Concurrent:并发GC,不会使App的线程暂停,该GC是在后台线程运行的,并不会阻止内存分配。
  2. Alloc:当堆内存已满时,App尝试分配内存而引起的GC,这个GC会发生在正在分配内存的线程中。
  3. Explicit:App显示的请求垃圾收集,例如调用System.gc()。与DVM一样,最佳做法是应该信任GC并避免显式地请求GC,显式地请求GC会阻止分配线程并不必要地浪费CPU周期。如果显式地请求GC导致其他线程被抢占,那么有可能会导致jank(App同一帧画了多次)。
  4. NativeAlloc:Native内存分配时,比如为Bitmaps或者RenderScript分配对象,这会导致Native内存压力,从而触发GC。
  5. CollectorTransition:由堆转换引起的回收,这是运行时切换GC而引起的。收集器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集器转换仅在以下情况下出现:在内存较小的设备上,App 将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
  6. HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间,通常发生在当App已经移动到可察觉的暂停进程状态时。这样做的主要原因是减少了内存使用并对堆内存进行碎片整理。
  7. DisableMovingGc:不是真正触发GC的原因,发生并发堆压缩时,由于使用了GetPrimitiveArrayCritical,收集会被阻塞。在一般情况下,建议不要使用GetPrimitiveArrayCritical,因为它在移动收集器方面具有限制。
  8. HcapTrim:不是触发GC的原因,但是请注意,收集会一直被阻塞,直到堆内存整理完毕。

五、小结

  本文简要描述了安卓中虚拟机的发展,其实目前来看,ART的名字是非常贴切的,DVM开始具备了非虚拟机的特点,而ART则完全无法仅仅用虚拟机去描述,ART为安卓应用提供了一整套的运行时环境,而不是作为虚拟机去执行指令。

你可能感兴趣的:(java虚拟机学习笔记(三)——不是虚拟机的安卓运行时)