Anndroid GC 那些事


内存回收机制对于app性能优化中比较重要部分,我们要做好优化工作,Android GC工作情况我们需要熟知,

因此整理了一下关于GC知识点,主要分为Dalvik与ART两部分

首先介绍垃圾回收策略,参考深入jvm

基本回收策略:

引用计数(Reference Counting):
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收
时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

标记-清除(Mark-Sweep):
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的
对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。

复制(Copying):
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在
使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制
过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两
倍内存空间。

标记-整理(Mark-Compact):
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引
用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此
算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。


Dalvik回收机制:

GC的类型
GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,系统会自动触发GC操作来释放内存。
GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口触发的GC。
GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。

实际上,GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三种类型的GC都是在分配对象的过程触发的。而并发和非并发GC的区别主要在于前者在GC过程中,有条件地挂起和唤醒非GC线程,而后者在执行GC的过程中,一直都是挂起非GC线程的。并行GC通过有条件地挂起和唤醒非GC线程,就可以使得应用程序获得更好的响应性。但是同时并行GC需要多执行一次标记根集对象以及递归标记那些在GC过程被访问了的对象的操作,所以也需要花费更多的CPU资源。


有关GC的指标
这里需要了解以下几个概念:分别是Java堆的起始大小(Starting Size)、最大值(Maximum Size)和增长上限值(Growth Limit)。
在启动Dalvik虚拟机的时候,我们可以分别通过-Xms、-Xmx和-XX:HeapGrowthLimit三个选项来指定上述三个值,以上三个值分别表示表示:
Starting Size: Dalvik虚拟机启动的时候,会先分配一块初始的堆内存给虚拟机使用。
Growth Limit: 是系统给每一个程序的最大堆上限,超过这个上限,程序就会OOM。
Maximum Size: 不受控情况下的最大堆内存大小,起始就是我们在用largeheap属性的时候,可以从系统获取的最大堆大小。

同时除了上面的这个三个指标外,还有几个指标也是值得我们关注的,那就是堆最小空闲值(Min Free)、堆最大空闲值(Max Free)和堆目标利用率(Target Utilization)。假设在某一次GC之后,存活对象占用内存的大小为LiveSize,那么这时候堆的理想大小应该为(LiveSize / U),但是(LiveSize / U)必须大于等于(LiveSize + MinFree)并且小于等于(LiveSize + MaxFree),每次GC后垃圾回收器都会尽量让堆的利用率往目标利用率靠拢。所以当我们尝试生成一些几百个的对象,试图去扩大可用堆大小的时候,反而会导致频繁的GC,因为这些对象的分配会导致GC,而GC后会让堆内存回到合适的比例,而我们使用的局部变量很快会被回收理论上存活对象还是那么多,我们的堆大小也会缩减回来无法达到扩充的目的,同时这也是产生CONCURRENT GC的一个因素。


对象的分配和GC触发时机
         1. 调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存。如果分配成功,那么就将分配得到的地址直接返回给调用者了。函数dvmHeapSourceAlloc在不改变Java堆当前大小的前提下进行内存分配,这是属于轻量级的内存分配动作。
         2. 如果上一步内存分配失败,这时候就需要执行一次GC了。不过如果GC线程已经在运行中,即gDvm.gcHeap->gcRunning的值等于true,那么就直接调用函数dvmWaitForConcurrentGcToComplete等到GC执行完成就是了。否则的话,就需要调用函数gcForMalloc来执行一次GC了,参数false表示不要回收软引用对象引用的对象。
        3. GC执行完毕后,再次调用函数dvmHeapSourceAlloc尝试轻量级的内存分配操作。如果分配成功,那么就将分配得到的地址直接返回给调用者了。
        4. 如果上一步内存分配失败,这时候就得考虑先将Java堆的当前大小设置为Dalvik虚拟机启动时指定的Java堆最大值,再进行内存分配了。这是通过调用函数dvmHeapSourceAllocAndGrow来实现的。
        5. 如果调用函数dvmHeapSourceAllocAndGrow分配内存成功,则直接将分配得到的地址直接返回给调用者了。
        6. 如果上一步内存分配还是失败,这时候就得出狠招了。再次调用函数gcForMalloc来执行GC。参数true表示要回收软引用对象引用的对象。
        7. GC执行完毕,再次调用函数dvmHeapSourceAllocAndGrow进行内存分配。这是最后一次努力了,成功与否都到此为止。



通过这个流程可以看到,在对象的分配中会导致GC,第一次分配对象失败我们会触发GC但是不回收Soft的引用,如果再次分配还是失败我们就会将Soft的内存也给回收,前者触发的GC是GC_FOR_MALLOC类型的GC,后者是GC_BEFORE_OOM类型的GC。而当内存分配成功后,我们会判断当前的内存占用是否是达到了GC_CONCURRENT的阀值,如果达到了那么又会触发GC_CONCURRENT。
         那么这个阀值又是如何来的呢,上面我们说到的一个目标利用率,GC后我们会记录一个目标值,这个值理论上需要再上述的范围之内,如果不在我们会选取边界值做为目标值。虚拟机会记录这个目标值,当做当前允许总的可以分配到的内存。同时根据目标值减去固定值(200~500K), 当做触发GC_CONCURRENT事件的阈值。


回收算法和内存碎片
        主流的大部分Davik采取的都是标注与清理(Mark and Sweep)回收算法,也有实现了拷贝GC的,这一点和HotSpot是不一样的,具体使用什么算法是在编译期决定的,无法在运行的时候动态更换。如果在编译dalvik虚拟机的命令中指明了"WITH_COPYING_GC"选项,则编译"/dalvik/vm/alloc/Copying.cpp"源码 – 此是Android中拷贝GC算法的实现,否则编译"/dalvik/vm/alloc/HeapSource.cpp" – 其实现了标注与清理GC算法。
        由于Mark and Sweep算法的缺点,容易导致内存碎片,所以在这个算法下,当我们有大量不连续小内存的时候,再分配一个较大对象时,还是会非常容易导致GC所以对于Dalvik虚拟机的手机来说,我们首先要尽量避免掉频繁生成很多临时小变量(比如说:getView, onDraw等函数中new对象),另一个又要尽量去避免产生很多长生命周期的大对象。


ART回收机制:


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

2,ART运行时的每一个Space都有不同的回收策略,ART运行时根据这个特性提供了Mark Sweep、Partial Mark Sweep和Sticky Mark Sweep等三种回收力度不同的垃圾收集器。
其中,Mark Sweep的垃圾回收力度最大,它会同时回收Zygote Space、Allocation Space和Large Object Space的垃圾,Partial Mark Sweep的垃圾回收力度居中,它只会同时回收Allocation Space和Large Object Space的垃圾,而Sticky Mark Sweep的垃圾回收力度最小,它只会回收Allocation Stack的垃圾,即上次GC以后分配出来的又不再使用了的对象。力度越大的垃圾收集器,回收垃圾时需要的时候也就越长。这样我们就可以在应用程序运行的过程中根据不同的情景使用不同的垃圾收集器,那就可以更有效地执行垃圾回收过程。

3,几种回收方式的执行有前提条件的
类型为kGcTypeSticky的GC的执行代码虽然是最小的,但是它能够回收的垃圾也是最小的。如果回收的垃圾不足于满足请求分配的内存,那就相当于做了一次无用功了。
因此,执行类型为kGcTypeSticky的GC需要满足两个条件。第一个条件是上次GC后在Allocation Space上分配的内存要达到一定的阀值,这样才有比较大的概率回收到较多的内存。第二个条件Allocation Space剩余的未分配内存要达到一定的阀值,这样可以保证在回收得到较少内存时,也有比较大的概率满足请求分配的内存。

4,每一种类型的GC都是通过调用Heap类的成员函数CollectGarbageInternal来执行。注意这时候调用Heap类的成员函数CollectGarbageInternal传递的第三个参数为false,
表示不对那些只被软引用对象引用的对象进行回收。如果上述的三种类型的GC执行完毕,还是不能满足分配请求的内存,则继续往下执行。

5,经过前面三种类型的GC后还是不能成功分配到内存,那就说明能够回收的内存还是太小了,因此,这时候只能通过在允许范围内增长堆的大小来满足内存分配请求了。
前面分析Heap类的成员函数TryToAllocate时,将第四个参数设置为true,即可在允许范围内增长堆大小的前提下进行内存分配。
如果在允许范围内增长了堆的大小还是不能成功分配到请求的内存,那就只能出最后的一个大招了。

6,最后的大招是首先执行一个类型为kGcTypeFull的、要求回收那些只被软引用对象引用的对象的GC,接着再在允许范围内增长堆大小的前提下尝试分配内存。
这一次如果还是失败,那就真的是内存不足了。


GC的类型
kGcCauseForAlloc: 当要分配内存的时候发现内存不够的情况下引起的GC,这种情况下的GC会Stop World.
kGcCauseBackground: 当内存达到一定的阀值的时候会去出发GC,这个时候是一个后台GC,不会引起Stop World.
kGcCauseExplicit,显示调用的时候进行的gc,如果ART打开了这个选项的情况下,在system.gc显示调用时候会进行GC.


对象的分配和GC触发时机
由于ART下内存分配和Dalvik下基本没有任何区别,这里略过


前台与后台GC   
          前台Foreground指的就是应用程序在前台运行时,而后台Background就是应用程序在后台运行时。因此,Foreground GC就是应用程序在前台运行时执行的GC,而Background就是应用程序在后台运行时执行的GC。
          应用程序在前台运行时,响应性是最重要的,因此也要求执行的GC是高效的。相反,应用程序在后台运行时,响应性不是最重要的,这时候就适合用来解决堆的内存碎片问题。因此,Mark-Sweep GC适合作为Foreground GC,而Mark-Compact GC适合作为Background GC。  由于有Compact的能力存在,碎片化在ART上可以很好的被避免,这个是ART一个很好的处理策略。



GC Log:
 当我们想要根据GC日志来追查一些性能问题时,需要对日志格式信息进行分析
Dalvik GC Log
        Dalvik的日志格式基本如下:
        D/dalvikvm:<GC_Reason><Amount_freed>,<Heap_stats>,<Pause_time>,<Total_time>
        GC_Reason: 就是我们上文提到的,是gc_alloc还是gc_concurrent,了解到不同的原因方便我们做不同的处理。
        Amount_freed: 表示系统通过这次GC操作释放了多少内存。
        Heap_stats: 中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)。
        Pause_time: 表示这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间,所以还有后续的一个total_time。
        Total_time: 表示本次GC所花费的总时间和上面的Pause_time,也就是stop all是不一样的,卡顿时间主要看上面的pause_time。

ART GC Log
        I/art:<GC_Reason><Amount_freed>,<LOS_Space_Status>,<Heap_stats>,<Pause_time>,<Total_time>
        基本情况和Dalvik没有什么差别,GC的Reason更多了,还多了一个OS_Space_Status.
        LOS_Space_Status:Large Object Space,大对象占用的空间,这部分内存并不是分配在堆上的,但仍属于应用程序内存空间,主要用来管理 bitmap 等占内存大的对象,避免因分配大内存导致堆频繁 GC


参考资料:

http://blog.csdn.net/luoshengyang/article/details/44513977

http://blog.csdn.net/luoshengyang/article/details/42072975

https://developer.android.com/studio/command-line/logcat.html


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值