AndroidGC研究二

Dalvik和ART区别

什么是Dalvik?

Dalvik是Google公司自己设计用于Android平台的虚拟机。 Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。 它可以支持已转换为 .dex格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。 Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。 很长时间以来,Dalvik虚拟机一直被用户指责为拖慢安卓系统运行速度不如IOS的根源。 2014年6月25日,Android L 正式亮相于召开的谷歌I/O大会,Android L 改动幅度较大,谷歌将直接删除Dalvik,代替它的是传闻已久的ART。

Dalvik和JVM有啥关系?

主要区别: Dalvik是基于寄存器的,而JVM是基于栈的。 Dalvik运行dex文件,而JVM运行java字节码 自Android 2.2开始,Dalvik支持JIT(just-in-time,即时编译技术)。 优化后的Dalvik较其他标准虚拟机存在一些不同特性: 1.占用更少空间 2.为简化翻译,常量池只使用32位索引 3.标准Java字节码实行8位堆栈指令,Dalvik使用16位指令集直接作用于局部变量。局部变量通常来自4位的“虚拟寄存器”区。这样减少了Dalvik的指令计数,提高了翻译速度。

当Android启动时,Dalvik VM 监视所有的程序(APK),并且创建依存关系树,为每个程序优化代码并存储在Dalvik缓存中。Dalvik第一次加载后会生成Cache文件,以提供下次快速加载,所以第一次会很慢。 Dalvik解释器采用预先算好的Goto地址,每个指令对内存的访问都在64字节边界上对齐。这样可以节省一个指令后进行查表的时间。为了强化功能, Dalvik还提供了快速翻译器(Fast Interpreter)。

一般来说,基于堆栈的机器必须使用指令才能从堆栈上的加载和操作数据,因此,相对基于寄存器的机器,它们需要更多的指令才能实现相同的性能。但是基于寄存器机器上的指令必须经过编码,因此,它们的指令往往更大。 Dalvik虚拟机既不支持Java SE 也不支持Java ME类库(如:Java类,AWT和Swing都不支持)。 相反,它使用自己建立的类库(Apache Harmony Java的一个子集)。

1.Dalvik到ART,开发者无需修改代码即可兼容; 2.Dalvik是JIT,ART是AOT,仅需一次编译,运行时的负担大幅减小,唯一缺点是第一次运行时需要更多时间; 3.ART大幅减少了垃圾回收时间,因此造成的卡顿现象也大幅缓解; 4.内存分配系统与垃圾回收算法也有提升; 5.85%的Play应用完全为虚拟机代码,可以无需修改立即兼容64bit; 6.64bit的纯性能提升,部分应用是13-19%,ARM64指令集有数倍的加密性能提升,但是不应算到64bit头上。另外因为现在还没有大于4G内存的手机,所以64bit可以读取大容量内存减少I/O带来的数十倍性能提升现在并未显现出来。

什么是ART?

即Android Runtime ART 的机制与 Dalvik 不同。在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率。

ART真正的优化内容

我们知道在Android N 中对其 ART做了比较大的变化。主要是同一程序的代码可能同时运行在本地机器码(编译)、解释和JIT(Just In Time)的混合运行模式,并且不同的用户,同一应用程序的代码,可能运行不同的编译代码。因为有了Profile-guided JIT/AOT Compilation,那么不同的用户行为对同一app可能会有不同的编译结果。N 上做此变化的其目的是为了在安装时间、内存占用、电池消耗和性能之间获得最好的折衷。

ART是在Android KitKat引入并在Lollipop中设为默认的运行方式。ART的主要特征之一就是安装时对应用的AOT编译。这种方式的主要优点就是优化产生的本地代码性能更好,执行起来需要更少的电量。劣势在于安装文件所需的空间和时间。在Lollipop和Marshmallow(译者注:Android 6.0)中,大的应用需要数分钟才能安装完。为了改变这种状态,Android N实现了一个混合模式的运行环境。应用在安装时不做编译,而是解释字节码,所以可以快速启动。ART中有一种新的、更快的解释器,通过一种新的JIT完成,但是这种JIT的信息不是持久化的。取而代之的是,代码在执行期间被分析,分析结果保存起来。然后,当设备idle和充电的时候,ART会执行针对“热代码”进行AOT编译,其他代码不做编译。为了得到更优的代码,ART采用了几种技巧包括深度内联。 对同一个应用可以编译数次,或者找到变“热”的代码路径或者对已经编译的代码进行新的优化,这取决于分析器在随后的执行中的分析数据 这种混合使用AOT、解释、JIT的策略有如下优点:

  • 即使是大应用,安装时间也能缩短到几秒

  • 系统升级能更快地安装,因为不再需要优化这一步

  • 应用的内存占用更小,有些情况下可以降低50%

  • 改善了性能

  • 更低的电池消耗

不过,它机器码占用的存储空间更大,字节码变为机器码之后,可能会增加10%-20%。

Foreground GC与Background GC

我们知道了ART运行时既支持Mark-Sweep GC,又支持Compacting GC。其中,Mark-Sweep GC执行效率更高,但是存在内存碎片问题;而Compacting GC执行效率较低,但是不存在内存碎片问题。ART运行时通过引入Foreground GC和Background GC的概念来对这两种GC进行扬长避短。本文就详细分析它们的执行过程以及切换过程。

ART运行时Foreground GC和Background GC切换时机

应用程序在前台运行时,响应性是最重要的,因此也要求执行的GC是高效的。相反,应用程序在后台运行时,响应性不是最重要的,这时候就适合用来解决堆的内存碎片问题。因此,Mark-Sweep GC适合作为Foreground GC,而Compacting GC适合作为Background GC。

但是,ART运行时又是怎么知道应用程序目前是运行在前台还是后台呢?这就需要负责管理应用程序组件的系统服务ActivityManagerService闪亮登场了。因为ActivityManagerService清楚地知道应用程序的每一个组件的运行状态,也就是它们当前是在前台运行还是后台运行,从而得到应用程序是前台运行还是后台运行的结论。

ART与Dalvik GC上的区别

内存分配器

  • Dalvik 在Android 系统C 语言库bionic 中直接使用了dlmalloc这一个十分流行的开源内存分配器。

  • ART 参考http://blog.tek-life.com/understanding-ros-memory-allocator-in-art-virtual-machine/ 创建了一种名叫Runs-of-Slots-Allocator(RosAlloc)的分配器。这种分配器的特点是分配内存时,会采用更细粒度的锁控制。例如有不同的锁来保护不同的对象分配,或者当线程分配一些小尺寸对象时使用线程自己的堆栈,从而可完全不使用锁保护。同时这种分配器也更适合多线程的实现。 据Google自己的数据,RosAlloc能达到最多10倍的速度提升.

垃圾回收算法

  1. Dalvik Dalvik的垃圾回收分为两个阶段。 第一个阶段,Dalvik暂停所有的线程来分析堆的使用情况。 第二个阶段,Dalvik暂停所有线程来清理堆。这就会导致应用在性能上的“卡顿”。

  2. ART ART改进后的垃圾回收算法只暂停线程一次。ART 能够做到这一点,是因为应用本身做了垃圾回收的一些工作。垃圾回收启动后,不再是两次暂停,而是一次暂停。在遍历阶段,应用不需要暂停,同时垃圾回收停时间也大大缩短,因为 Google使用了一种新技术(packard pre-cleaning),在暂停前就做了许多事情,减轻了暂停时的工作量。

超大对象存储空间的支持

ART还引入了一个特殊的超大对象存储空间(large object space,LOS),这个空间与堆空间是分开的,不过仍然驻留在应用程序内存空间中。这一特殊的设计是为了让ART可以更好的管理较大的对象,比如位图对象(bitmaps)。 堆空间碎片化严重时,较大的对象会带来一些问题。比如,在分配一个此类对象时,相比其他普通对象,会导致垃圾收集器启动的次数增加很多。有了这个超大对象存储空间的支持,垃圾收集器因堆空间分段而引发调用次数将会大大降低,这样垃圾收集器就能做更加合理的内存分配,从而降低运行时开销。

Moving GC策略

ART为了解决堆空间内存碎片化的问题,近期提出了“Moving GC”的方法。其目的是清理堆栈以减少内存碎片。由于这个工作会导致应用程序长时间中断,所以它必须等程序退到后台时才能开展。核心思想是,当应用程序运行在后台时,将程序的堆空间做段合并操作。其实就是ART运行时Foreground GC和Background GC在不同时机回收不同的垃圾。

GC调度策略的多样性

经过比较Dalvik和ART的源码后,发现ART中GC调度策略发生了很大的变动。

具体来说分为以下几个方面

(a)GC触发方式  
(b)GC的种类  
(c)垃圾回收算法的多样性

GC触发方式

  1. Dalvik

    GC触发方式主要有

  2. GCFORMALLOC;

  3. GCCONCURRENT;

  4. GCEXPLICIT;

  5. GCBEFOREOOM

  6. ART

enum GcCause {  
  // GC triggered by a failed allocation. Thread doing allocation is blocked waiting for GC before  
  // retrying allocation.  
  kGcCauseForAlloc,  
  // A background GC trying to ensure there is free memory ahead of allocations.  
  kGcCauseBackground,  
  // An explicit System.gc() call.  
  kGcCauseExplicit,  
  // GC triggered for a native allocation.  
  kGcCauseForNativeAlloc,  
  // GC triggered for a collector transition.  
  kGcCauseCollectorTransition,  
  // Not a real GC cause, used when we disable moving GC (currently for     GetPrimitiveArrayCritical).  
  kGcCauseDisableMovingGc,  
  // Not a real GC cause, used when we trim the heap.  
  kGcCauseTrim,  
  // GC triggered for background transition when both foreground and background     collector are CMS.  
  kGcCauseHomogeneousSpaceCompact, 
};

GC的种类

  1. Dalvik 就一种GC(并发、非并发)

  2. ART 三种GC(并发、非并发):

  3. 快速GC策略Sticky GC:Sticky Mark sweep只扫描自从上次GC后被修改过的堆空间,并且也只回收自从上次GC后分配的空间。Sticky是只回收kGcRetentionPolicyAlwaysCollect的space。不回收其他两个,因此sticky的回收的力度是最小的。作为最全面的full mark sweep, 上面的三个策略都是会回收的。

  4. 局部GC策略Partial GC:这是mark sweep收集器里使用的最少的GC收集策略。按照官方文档,一般是使用sticky mark sweep较多。这里有一个概念就是吞吐率,即一次GC释放的字节数和GC持续的时间(秒)的比值。由于一般是sticky mark sweep进行GC,所以当上次GC的吞吐率小于同时的partial mark sweep的吞吐率时,就会把下次GC收集器从sticky变成partial。但是在partial执行一次GC后,就仍然会恢复到stick mark sweep收集器。阅读源码发现,partial重写了父类的成员函数。

  5. 全局GC策略Full GC:对整个堆进行垃圾回收,除了image空间。

其实分析这些可以发现,从full mark sweep到partial mark sweep到stick mark sweep,GC的力度是越来越小的,因为可以回收的越来越少。之所以说回收力度大,就是指可以回收的space多,比如partial, 是不回收kGcRetentionPolicyFullCollect,但是是会回收kGcRetentionPolicyAlwaysCollect的space的。因此partial mark sweep每次执行一次GC后,就会自动切换到sticky策略,这样才能使系统更流畅得进行GC,并减少了GC带来的消耗。。

  • Concurrent mark sweep(CMS) 对整个堆进行垃圾回收,除了image空间。

  • Concurrent partial mark sweep 对几乎整个堆进行回收,除了image空间和zynote空间。

  • Concurrent sticky mark sweep 一次普通的垃圾回收,它只负责回收上次垃圾回收之后的分配的对象。它要比Concurrent partial mark sweep执行的次数频繁的多,因为它的执行速度快,暂停时间少。

  • Marksweep + semispace 一种非同时进行的,包含复制过程的GC。可以用来移动堆,也可以用来压缩堆(减少堆的碎片化)。

enum GcType {  
  // Placeholder for when no GC has been performed.  
  kGcTypeNone,  
  // Sticky mark bits GC that attempts to only free objects allocated since the last GC.  
  kGcTypeSticky,  
  // Partial GC that marks the application heap but not the Zygote.
  kGcTypePartial,  
  // Full GC that marks and frees in both the application and Zygote heap. 
  kGcTypeFull,  
  // Number of different GC types.  
  kGcTypeMax, 
};

垃圾回收算法的多样性

(1)Dalvik

两种:串行Mark-Sweep算法、并行Mark-Sweep算法

(2)ART

enum CollectorType {  
// No collector selected.  
kCollectorTypeNone,  
// Non concurrent mark-sweep.  
kCollectorTypeMS,  
// Concurrent mark-sweep.  
kCollectorTypeCMS,  
// Semi-space / mark-sweep hybrid, enables compaction.  
kCollectorTypeSS,  
// A generational variant of kCollectorTypeSS.  
kCollectorTypeGSS,  
// Mark compact colector.  
kCollectorTypeMC,  
// Heap trimming collector, doesn't do any actual collecting.  
kCollectorTypeHeapTrim,  
// A (mostly) concurrent copying collector.  
kCollectorTypeCC,  
// A homogeneous space compaction collector used in background transition  
// when both foreground and background collector are CMS.  
kCollectorTypeHomogeneousSpaceCompact,  
};

可以看到,除了标记清除算法,基于半空间(semi-space)的拷贝算法也实现了,其中GSS(分代半空间拷贝算法)的实现具有很大的研究性。

参考: http://www.jianshu.com/p/d90283ab9a3b http://cruise1008.github.io/2016/03/30/Android-GC-从dalvik到ART的改进分析/ https://blog.dreamtobe.cn/2015/11/30/gc/

总结

首先通过对dalvik的GC的过程的分析,我们可以发现dalvik的在GC时出现的几个主要问题, 首先即在GC时会有三次暂停其他进程运行,三次停顿导致的总的时间太长会导致丢帧卡顿现象严重。其次,就是在堆空间中给较大的对象分配空间后会导致碎片化比较严重,并且可能会导致GC调用次数变多增加开销。(各个文章对暂停次数说的不一致,我们暂且不管,其实是在DVM在非并发时是三次,在并发时是暂停两次

针对dalvik的以上两个问题,ART都有做了对应的优化来解决这些问题。针对第一个问题,ART在标记阶段做了非常大的优化,消除了第一次遍历堆地址空间的停顿,和第二次标记根集对象的停顿,并缩短了第三次处理card table的停顿,因此大大的缩短了应用程序在执行时的卡顿时间。针对第二个问题,提出了LOS的管理方法。

除此以外,还提供了丰富的GC收集器,例如继承自mark sweep的sticky mark sweep和partial mark sweep,二者的回收力度都要比full mark sweep小,因此性能上也得到了一些提升。一般情况下的收集器的主力就是sticky mark sweep, 这是对应用程序的性能影响最小的一种方式,因此大多数情况的GC表现,都要比dalvik的GC表现更好。

GC日志研究

因为Android是使用在移动设备上,因此必须时刻考虑应用使用了多少RAM。即使Dalvik与ART提供了垃圾回收机制,也并不意味着你能忽视应用何时分配、释放内存。为了在系统切换应用时提供稳定的用户体验,重要的是当用户没有与你的应用交互时,需要尽可能地减少应用的内存消耗。

即使你在开发中按照Managing Your App Memory中的最佳示例做了,可能还会造成内存泄漏或引入其它的内存问题。唯一能够确保应用使用尽可能少的内存的方法是使用工具分析应用内存的使用情况。

Dalvik 日志信息

在DVM中,每次GC都会被打印出来

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 触发机制

在DVM下,出发GC的原因可能有以下几种:

  • GC_CONCURRENT 当堆将要被填满的时候触发的垃圾回收

  • GC_FOR_MALLOC 当应用的堆已经被填满的时候,如果应用继续申请内存就会触发此类垃圾回收。系统会杀死应用的进程并且回收所有内存。它是在创建对象时被触发的。

  • GC_HPROF_DUMP_HEAP 当请求生成HPROF文件来分析内存的时候会触发此类垃圾回收

  • GC_EXPLICIT 一次指定的垃圾回收,例如主动调用System.gc()的时候。主动调用System.gc()时被调用。

  • GC_EXTERNAL_ALLOC 在API版本10(Android3.0)以下的时候的垃圾回收机制。3.0以上版本所有的内存都在Dalvik堆中分配。它是用来回收dalvik虚拟机以外的内存(例如Bitmap中的内存或者NIO buffer中的内存)。

本次垃圾回收中释放的内存总量

堆中可用空间所占的百分比 和 (堆中对象的数量)/(堆的大小)

垃圾回收过程中应用暂停挂起的时间。Concurrent类型的垃圾回收有两次暂停时间:一次发生在开始,另一次发生在结束。堆的内容越多,暂停的时间越长

系统API版本10以下的系统中, Dalvik虚拟机堆外 (分配的内存) / (限制的内存量)

观察这些Log信息,如果heap stats中的数值(堆中对象数量)/(堆的大小)越来越大,那么应用中很有可能存在内存泄漏。

ART的Log信息

不像Dalvik虚拟机,ART不会把所有的GC结果都输出到Logcat中。只有那些被认为执行缓慢的GC才会被输出到Logcat中。确切的说,只有GC停顿时间超过5ms或者整个GC耗时超过100ms才会被输出到Logcat中。(注意:3.0之后垃圾回收做了优化,整个GC过程中只有一小部分时间会导致应用停顿)。主动发起的GC一定会被输出到Logcat中。ART输出的Log信息的例子如下:

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

GC 触发机制

触发垃圾回收的原因以及触发了何种类型的垃圾回收,它包含以下几类:

  • Concurrent特点是不需要挂起应用线程。它在后台线程中运行,不会影响到内存的分配。

  • Alloc 它在应用申请内存但是堆已满的情况下触发。在这种情况下,垃圾回收在分配内存的线程中进行。(它会导致应用暂停一段时间)

  • 主动发起的垃圾回收,例如System.gc()。跟dalvik一样,建议不要主动发起垃圾回收。

  • NativeAlloc 它会在native层内存吃紧的时候发起。比如说分配Bitmap或者RenderScript内存空间不够的时候。

  • CollectorTransition 一般由堆转换引起,垃圾回收器会把free-list back空间的所有对象都复制到bump pointer空间中。目前,转换过程只在一些低内存的设备上应用所在进程从对暂停敏感切换到对暂停不敏感状态的时候发生。

  • HomogeneousSpaceCompact 它是在free-list 空间到free-list空间的复制。当app所在进程对暂停不敏感的时候发生。它可以减少内存的使用,减少内存分配的碎片化。

  • DisableMovingGc 它并不是引起内存回收的真正原因,它是垃圾回收被GetPrimitiveCritical中断时发生的。当concurrent 堆压缩正在执行的时候,因为对垃圾回收器的限制,所以非常不建议使用它。

  • HeapTrim 它不是触发垃圾回收的原因,但是在堆压缩的时候垃圾回收会被终止。

GC Name 垃圾回收的名称,一共有如下几类:

  • Concurrent mark sweep(CMS) 对整个堆进行垃圾回收,除了image空间。

  • Concurrent partial mark sweep 对几乎整个堆进行回收,除了image空间和zynote空间。每次执行它其实会增加内存的上限,当上次GC的吞吐率小于同时的partial mark sweep的吞吐率时,就会把下次GC收集器从sticky变成partial。

  • Concurrent sticky mark sweep 一次普通的垃圾回收,它只负责回收上次垃圾回收之后的分配的对象。它要比Concurrent partial mark sweep执行的次数频繁的多,因为它的执行速度快,暂停时间少。

  • Marksweep + semispace 一种非同时进行的,包含复制过程的GC。可以用来移动堆,也可以用来压缩堆(减少堆的碎片化)。

ART运行时内部使用的Java堆的主要组成包括Image Space、Zygote Space、Allocation Space和Large Object Space四个Space,Image Space用来存在一些预加载的类, Zygote Space和Allocation Space与Dalvik虚拟机垃圾收集机制中的Zygote堆和Active堆的作用是一样的。 Large Object Space就是一些离散地址的集合,用来分配一些大对象从而提高了GC的管理效率和整体性能。

Zygote Heap

  • 分布:第一个应用程序fork前,已经使用的部分

  • 内容:Zygote进程在启动过程中加载的类、资源、对象

    Activity Heap

  • 分布:第一个应用程序fork前,未使用的部分

  • 内容:第一个应用fork开始后,无论是Zygote进程,还是应用进程,分配的对象。

这两块就是放在系统启动过程中加载的类、资源、对象,以后之后App启动之后产生的对象等等,这一点,ART与DVM差不多。

  • Objects freed 释放了对象(非大对象)的数量

  • Size freed 释放了空间(非大对象)的大小

  • Large objects freed 释放了大对象的数量

  • Large object size freed 释放了大对象的空间的大小

  • Heap stats 堆中空闲空间的百分比 和 (对象的个数)/(堆的总空间)

  • Pause times 一般情况下,垃圾回收的暂停时间跟堆中引用的数量成正比。目前,ART CMS GC 只有一次在垃圾回收结束的时候。内存转移的GC在整个过程中有一个长时间的暂停。所以在ART的GC中只有一次暂停。

同样,在使用ART的情况下,如果Logcat中看到大量的GC的记录。并且Heap stats信息中的(对象数/堆的空间)的数值不断增长,没有变小的趋势。那么应用很有可能存在内存泄漏。 如果看到GC Reason对应的信息变成了 “Alloc”,那说明应用的堆几乎满了,接下来马上要内存溢出了。

参考:https://developer.android.com/studio/profile/investigate-ram.html

Last updated