Android 虚拟机 Vs Java 虚拟机

2,964 阅读15分钟

读书笔记,如需转载,请注明作者:Yuloran (t.cn/EGU6c76)

前言

本文分为两部分,第一部分为 《Garbage Collection in Android》 的翻译,第二部分简介 Android 虚拟机与 Java 虚拟机的差别。

Garbage Collection in Android

演讲人介绍

Colt McAnlis,Google 开发工程师。为便于写作,笔者将以第一人称视角对视频内容进行概述。

自动内存管理的陷进

很多高性能语言,如 C 和 C++ 都需要开发人员手动管理内存的分配与释放,但在代码量很大、业务逻辑很复杂时,很容易忘记释放已分配的内存,进而导致内存泄露。位于 Android Runtime 的 Dalvik(< Android 5.0) 或 ART(≥ Android 5.0)虚拟机的自动垃圾回收机制,将开放人员从手动管理内存的工作方式中解放了出来,从而提高了工作效率。但同时也隐藏了性能陷进,其中最需要关注的就是如何分配以及使用内存。

GC 概念由来

自动内存管理机制称为 Garbage Collection,是 John McCarthy 于1959 年提出的概念,用于解决 LISP 语言中的问题。它主要涉及两个原则:

  1. 找出不再访问的对象;
  2. 回收这些对象占用的资源。

GC 实现的难点

想象一下,你分配了 20,000 个对象,那么怎么识别哪些对象是不再访问的,或者什么时候应该触发 GC 事件呢?

这真是太难了!好在,自 GC 概念提出之后的 50 年里,我们一直致力于提升垃圾收集器的性能,这也是为什么 Android 的垃圾回收器比 John McCarthy 提出的复杂的多的原因。

Android GC 简介

事实上,Android 系统根据所分配对象的类型以及 GC 时系统如何管理这些对象,将进程所使用的内存分成了多个空间。新分配的对象位于什么空间,取决于你的 Android Runtime 是什么版本,5.0 以上是 ART(Android Runtime)虚拟机,5.0以下是 Dalvik 虚拟机。

笔者注:上图具体含义见下文差异分析。

每个空间都有大小限制(Set Size),系统会跟踪整个程序所占用的内存大小。当程序占用内存达到一定程度时,系统就会触发 GC 事件回收内存,以便将来分配给其它对象:

GC 事件在 Dalvik 虚拟机和 ART 虚拟机上的表现也不尽相同。在 Dalvik 虚拟机中,很多 GC 事件都是 "Stop the World Event":

而 ART 虚拟机扩展了并行 GC 算法,消除了大的 GC 停顿时间,但是在重要步骤上,还是会有短暂的停顿:

GC 导致的掉帧

尽管系统工程师做了大量优化来提升 GC 速度来减少卡顿,但是你的 App 仍然可能存在性能问题。我们知道,Android 系统每 16 ms 就要渲染一帧,所以在一帧时间内,GC 时间越长,留给业务逻辑的时间就越短:

如果你的 GC 过于频繁(比如在循环中创建了大量临时对象)或 GC 时间过长,就会导致 1 帧的处理时间超过 16 ms 上限,这就会给用户造成卡顿、掉帧的视觉效果:

内存分析工具

好在,Android Studio 的 Profiler 工具,可以用来查看内存使用情况以及内存分配情况。

总结

不过,内存优化就是说起来简单,做起来难,所以你还需要观看以下视频来掌握更多的性能优化知识:

链接

Android 虚拟机与 Java 虚拟机的差异

其实笔者主要想关注的这几个虚拟机在内存布局及 GC 算法方面的差异,至于 JVM、Dalvik、ART 各自对应的可执行文件格式(.jar、.dex、.elf)、字节码结构(class、dex、elf)这显然是不同的。奈何关于 Android 虚拟机内存布局网上资料甚少,大部分只围绕运行时堆内存的分配及其 GC 算法来讲,没有涉及虚拟机栈(方法执行模型)、常量池、方法区,所以这部分没法跟 Java 虚拟机进行对比。不过,万物之间是存在普遍联系的,没有东西可以凭空产生。既然 Java 虚拟机的方法执行模型都能跟 C 语言在概念上很相似,Dalvik 和 ART 自然也可以照此理解,细节上肯定是不一样的,毕竟指令集都不一样。真想深究,只能看 虚拟机源码 了。

不钻牛角尖了,头疼。再来看下这个图:

这图描述的就是 Dalvik 和 ART 虚拟机对运行时堆的空间划分,这个在源码中都有对应的实现。 上图具体含义可参考:

或者使用 看云 阅读更方便。

总的来说,就是 Android 虚拟机没有使用 HotSpot 虚拟机所采用的分代收集算法,而是采用了标记-清除或者标记-复制算法,这个可以在编译系统时指定,可参考《Android Garbage Collection/dalvik GC》,不过一般都是标记清除算法。

以下摘自 《Android虚拟机之Dalvik虚拟机》

  • 内存管理

    ◇ Java Object Heap 大小受限,如:16M/24M/32M/48M
    ◇ Bitmap Memory(External Memroy):大小计入 Java Object Heap
    ◇ Native Heap:大小不受限

  • 垃圾收集(GC)

    ◇ Mark:使用RootSet标记对象引用
    ◇ Sweep:回收没有被引用的对象

  • GingerBread(Android 2.3)之前

    ◇ Stop-the-word:也就是垃圾收集线程在执行的时候,其它的线程都停止
    ◇ Full heap collection:也就是一次收集完全部的垃圾,一次垃圾收集造成的程序中止时间通常都大于 100ms

  • GingerBread(Android 2.3)之后

    ◇ Cocurrent:也就是大多数情况下,垃圾收集线程与其它线程是并发执行的
    ◇ Partial collection:也就是一次可能只收集一部分垃圾,一次垃圾收集造成的程序中止时间通常都小于 5ms

ART 虚拟机对并行 GC 进行了扩展,将堆内存划分成更多不同类型的具体空间,使用不同的 GC 算法以获得更短的 GC 停顿时间。

Dalvik 虚拟机

wiki

Dalvik 虚拟机,是 Google 等厂商合作开发的 Android 移动设备平台的核心组成部分之一。它可以支持已转换为 .dex(即“Dalvik Executable”)格式的 Java 应用程序的运行。.dex 格式是专为 Dalvik 设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik 由 Dan Bornstein 编写的,名字来源于他的祖先曾经居住过的小渔村达尔维克(Dalvík),位于冰岛 Eyjafjörður。

大多数虚拟机包括 JVM 都是一种堆栈机器,而 Dalvik 虚拟机则是寄存器机。两种架构各有优劣,一般而言,基于堆栈的机器需要更多指令,而基于寄存器的机器指令更长。

从 Android 5.0 版起,Android Runtime(ART)取代 Dalvik 成为系统内默认虚拟机。

差异:

  • Dalvik 虚拟机早期并没有使用即时编译(JIT)技术。从 Android 2.2 开始, Dalvik 虚拟机也支持 JIT.
  • Dalvik 虚拟机有自己的字节码,并非使用 Java 字节码。
  • Dalvik 基于寄存器,而 JVM 基于堆栈。
  • Dalvik VM 透过 Zygote 进行类别的预加载,Zygote 会完成虚拟机的初始化,也是与 JVM 不同之处。

标记-清除算法(Mark-Sweep Algorithm)

Dalvik 虚拟机采用 Mark-Sweep 算法,不带压缩整理(Compact),所以比 Java 虚拟机更简单一些。

(a)GC 前的状态。示例中有一个 GC Root,所有对象都未被标记。

(b)GC 标记后的状态。在标记阶段,所有活动对象(Active Objects)都会被标记。

(c)GC 清除后的状态。所有垃圾已被回收,并且所有活动对象的标记状态都被重置为 false。

GC 触发时机

  • 即将产生 OOM 时
  • 堆内存使用达到上限时(系统可配)
  • 显示调用 gc()

GC 日志

在 Dalvik(而不是 ART)中,每次垃圾回收都会将以下信息打印到 logcat 中:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>

示例:

D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms

垃圾回收原因

什么触发了垃圾回收以及是哪种回收。可能出现的原因包括:

  • GC_CONCURRENT:在您的堆开始占用内存时可以释放内存的并发垃圾回收。
  • GC_FOR_MALLOC:堆已满而系统不得不停止您的应用并回收内存时,您的应用尝试分配内存而引起的垃圾回收。
  • GC_HPROF_DUMP_HEAP:当您请求创建 HPROF 文件来分析堆时出现的垃圾回收。
  • GC_EXPLICIT:显式垃圾回收,例如当您调用 gc() 时(您应避免调用,而应信任垃圾回收会根据需要运行)。
  • GC_EXTERNAL_ALLOC:这仅适用于 API 级别 10 及更低级别(更新版本会在 Dalvik 堆中分配任何内存)。外部分配内存的垃圾回收(例如存储在原生内存或 NIO 字节缓冲区中的像素数据)。

释放量

从此次垃圾回收中回收的内存量。

堆统计数据

堆的可用空间百分比与(活动对象数量)/(堆总大小)。

外部内存统计数据

API 级别 10 及更低级别的外部分配内存(已分配内存量)/(发生回收的限值)。

暂停时间

堆越大,暂停时间越长。并发暂停时间显示了两个暂停:一个出现在回收开始时,另一个出现在回收快要完成时。 在这些日志消息积聚时,请注意堆统计数据的增大(上面示例中的 3571K/9991K 值)。如果此值继续增大,可能会出现内存泄漏。

ART 虚拟机

wiki

Android Runtime(缩写为ART),是一种在 Android 操作系统上的运行环境,由 Google 公司研发,并在 2013 年作为 Android 4.4 系统中的一项测试功能正式对外发布,在 Android 5.0 及后续 Android 版本中作为正式的运行时库取代了以往的 Dalvik 虚拟机。ART 能够把应用程序的字节码转换为机器码,是 Android 所使用的一种新的虚拟机。它与 Dalvik 的主要不同在于:Dalvik 采用的是 JIT 技术,而 ART 采用 Ahead-of-time(AOT)技术。ART 同时也改善了性能、垃圾回收(Garbage Collection)、应用程序出错以及性能分析。

JIT 最早在 Android 2.2 系统中引进到 Dalvik 虚拟机中,在应用程序启动时,JIT 通过进行连续的性能分析来优化程序代码的执行,在程序运行的过程中,Dalvik 虚拟机在不断的进行将字节码编译成机器码的工作。与 Dalvik 虚拟机不同的是,ART 引入了 AOT 这种预编译技术,在应用程序安装的过程中,ART 就已经将所有的字节码重新编译成了机器码。应用程序运行过程中无需进行实时的编译工作,只需要进行直接调用。因此,ART 极大的提高了应用程序的运行效率,同时也减少了手机的电量消耗,提高了移动设备的续航能力,在垃圾回收等机制上也有了较大的提升。为了保证向下兼容,ART 使用了相同的 Dalvik 字节码文件(dex),即在应用程序目录下保留了 dex 文件供旧程序调用然而 .odex 文件则替换成了可执行与可链接格式(ELF)可执行文件。一旦一个程序被 ART 的 dex2oat 命令编译,那么这个程序将会指通过 ELF 可执行文件来运行。因此,相对于 Dalvik 虚拟机模式,ART 模式下 Android 应用程序的安装需要消耗更多的时间,同时也会占用更大的储存空间(指内部储存,用于储存编译后的代码),但节省了很多 Dalvik 虚拟机用于实时编译的时间。

Google 公司在 Android 4.4 中带来的 ART 模式仅仅是 ART 的一个预览版,系统默认仍然使用的是 Dalvik 虚拟机,4.4 上面提供的预览版 ART 相对于 Android 5.0 以后的 ART 运行时库有较大的不同,尤其体现在兼容性上。

GC 日志

与 Dalvik 不同,ART 不会为未明确请求的垃圾回收记录消息。只有在认为垃圾回收速度较慢时才会打印垃圾回收。更确切地说,仅在垃圾回收暂停时间超过 5ms 或垃圾回收持续时间超过 100ms 时。如果应用未处于可察觉的暂停进程状态,那么其垃圾回收不会被视为较慢。始终会记录显式垃圾回收。

ART 会在其垃圾回收日志消息中包含以下信息:

I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>

示例:

I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

垃圾回收原因

什么触发了垃圾回收以及是哪种回收。可能出现的原因包括:

  • Concurrent:不会暂停应用线程的并发垃圾回收。此垃圾回收在后台线程中运行,而且不会阻止分配。
  • Alloc:您的应用在堆已满时尝试分配内存引起的垃圾回收。在这种情况下,分配线程中发生了垃圾回收。
  • Explicit:由应用明确请求的垃圾回收,例如,通过调用 gc() 或 gc()。与 Dalvik 相同,在 ART 中,最佳做法是您应信任垃圾回收并避免请求显式垃圾回收(如果可能)。不建议使用显式垃圾回收,因为它们会阻止分配线程并不必要地浪费 CPU 周期。如果显式垃圾回收导致其他线程被抢占,那么它们也可能会导致卡顿(应用中出现间断、抖动或暂停)。
  • NativeAlloc:原生分配(如位图或 RenderScript 分配对象)导致出现原生内存压力,进而引起的回收。
  • CollectorTransition:由堆转换引起的回收;此回收由运行时切换垃圾回收引起。回收器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,回收器转换仅在以下情况下出现:在 RAM 较小的设备上,应用将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
  • HomogeneousSpaceCompact[/ˌhɒmə(ʊ)'dʒiːnɪəs/]:齐性空间压缩是空闲列表空间到空闲列表空间压缩,通常在应用进入到可察觉的暂停进程状态时发生。这样做的主要原因是减少 RAM 使用量并对堆进行碎片整理。
  • DisableMovingGc:这不是真正的垃圾回收原因,但请注意,发生并发堆压缩时,由于使用了 GetPrimitiveArrayCritical,回收遭到阻止。一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动回收器方面具有限制。
  • HeapTrim:这不是垃圾回收原因,但请注意,堆修剪完成之前回收会一直受到阻止。

垃圾回收名称

ART 具有可以运行的多种不同的垃圾回收。

  • Concurrent mark sweep (CMS):整个堆回收器,会释放和回收映像空间(Image Space)以外的所有其他空间。
  • Concurrent partial mark sweep:几乎整个堆回收器,会回收除了映像空间(Image Space)和 zygote 空间以外的所有其他空间。
  • Concurrent sticky mark sweep:生成回收器,只能释放自上次垃圾回收以来分配的对象。此垃圾回收比完整或部分标记清除运行得更频繁,因为它更快速且暂停时间更短。
  • Marksweep + semispace:非并发、复制垃圾回收,用于堆转换以及齐性空间压缩(对堆进行碎片整理)。

释放的对象

此次垃圾回收从非大型对象空间回收的对象数量。

释放的大小

此次垃圾回收从非大型对象空间回收的字节数量。

释放的大型对象

此次垃圾回收从大型对象空间回收的对象数量。

释放的大型对象大小

此次垃圾回收从大型对象空间回收的字节数量。

堆统计数据

空闲百分比与(活动对象数量)/(堆总大小)。

暂停时间

通常情况下,暂停时间与垃圾回收运行时修改的对象引用数量成正比。当前,ART CMS 垃圾回收仅在垃圾回收即将完成时暂停一次。移动的垃圾回收暂停时间较长,会在大部分垃圾回收期间持续出现。

如果您在 logcat 中看到大量的垃圾回收,请注意堆统计数据的增大(上面示例中的 25MB/38MB 值)。如果此值继续增大,且始终没有变小的趋势,则可能会出现内存泄漏。或者,如果您看到原因为“Alloc”的垃圾回收,那么您的操作已经快要达到堆容量,并且将很快出现 OOM 异常。

官网 GC 日志原文

这个网页好像没有英文版,但是有的中文解释又很别扭,比如“映像空间”,这是什么鬼?其实就是 Image Space...