腾讯云技术社区-掘金主页持续为你们呈现云计算技术文章,欢迎你们关注!javascript
做者:陈昱全java
想写一篇关于Android GC的想法来源于追查一个魅族手机图片滑动卡顿问题,因为不断的GC致使的丢帧卡顿的问题让咱们想了不少方案去解决,因此就打算详细的看看内存分配和GC的原理,为何会不断的GC, GC ALLOC和GC COCURRENT有什么区别,能不能想办法扩大堆内存减小GC的频次等等。算法
乍一看这两个算法彷佛并无多大的区别,都是标记了而后挪到另外的内存地址进行回收,那为何不一样的分代要使用不一样的回收算法呢?网络
其实2者最大的区别在于前者是用空间换时间后者则是用时间换空间。并发
前者的在工做的时候是不没有独立的“Mark”与“Copy”阶段的,而是合在一块儿作一个动做,就叫Scavenge(或Evacuate,或者就叫Copy)。也就是说,每发现一个此次收集中还没有访问过的活对象就直接Copy到新地方,同时设置Forwarding Pointer,这样的工做方式就须要多一份空间。jvm
后者在工做的时候则须要分别的Mark与Compact阶段,Mark阶段用来发现并标记全部活的对象,而后compact阶段才移动对象来达到Compact的目的。若是Compact方式是Sliding Compaction,则在Mark以后就能够按顺序一个个对象“滑动”到空间的某一侧。由于已经先遍历了整个空间里的对象图,知道全部的活对象了,因此移动的时候就能够在同一个空间内而不须要多一份空间。函数
因此新生代的回收会更快一点,老年代的回收则会须要更长时间,同时压缩阶段是会暂停应用的,因此给咱们应该尽可能避免对象出如今老年代。组件化
Java堆其实是由一个Active堆和一个Zygote堆组成的,其中,Zygote堆用来管理Zygote进程在启动过程当中预加载和建立的各类对象,而Active堆是在Zygote进程Fork第一个子进程以前建立的。之后启动的全部应用程序进程是被Zygote进程Fork出来的,并都持有一个本身的Dalvik虚拟机。在建立应用程序的过程当中,Dalvik虚拟机采用Cow策略复制Zygote进程的地址空间。性能
Cow策略:一开始的时候(未复制Zygote进程的地址空间的时候),应用程序进程和Zygote进程共享了同一个用来分配对象的堆。当Zygote进程或者应用程序进程对该堆进行写操做时,内核就会执
行真正的拷贝操做,使得Zygote进程和应用程序进程分别拥有本身的一份拷贝,这就是所谓的Cow。由于Copy是十分耗时的,因此必须尽可能避免Copy或者尽可能少的Copy。优化
为了实现这个目的,当建立第一个应用程序进程时,会将已经使用了的那部分堆内存划分为一部分,尚未使用的堆内存划分为另一部分。前者就称为Zygote堆,后者就称为Active堆。这样只需把zygote堆中的内容复制给应用程序进程就能够了。之后不管是Zygote进程,仍是应用程序进程,当它们须要分配对象的时候,都在Active堆上进行。这样就可使得Zygote堆尽量少地被执行写操做,于是就能够减小执行写时拷贝的操做。在Zygote堆里面分配的对象其实主要就是Zygote进程在启动过程当中预加载的类、资源和对象了。这意味着这些预加载的类、资源和对象能够在Zygote进程和应用程序进程中作到长期共享。这样既能减小拷贝操做,还能减小对内存的需求。
记得咱们以前在优化魅族某手机的gc卡顿问题时,发现他很容易触发GC_FOR_MALLOC,这个GC类别后续会说到,是分配对象内存不足时致使的。但是咱们又设置了很大的堆Size为何还会内存不够呢,这里须要了解如下几个概念:分别是Java堆的起始大小(Starting Size)、最大值(Maximum Size)和增加上限值(Growth Limit)。
在启动Dalvik虚拟机的时候,咱们能够分别经过-Xms、-Xmx和-XX:HeapGrowthLimit三个选项来指定上述三个值,以上三个值分别表示表示:
同时除了上面的这个三个指标外,还有几个指标也是值得咱们关注的,那就是堆最小空闲值(Min Free)、堆最大空闲值(Max Free)和堆目标利用率(Target Utilization)。假设在某一次GC以后,存活对象占用内存的大小为LiveSize,那么这时候堆的理想大小应该为(LiveSize / U)。可是(LiveSize / U)必须大于等于(LiveSize + MinFree)而且小于等于(LiveSize + MaxFree),每次GC后垃圾回收器都会尽可能让堆的利用率往目标利用率靠拢。因此当咱们尝试手动去生成一些几百K的对象,试图去扩大可用堆大小的时候,反而会致使频繁的GC,由于这些对象的分配会致使GC,而GC后会让堆内存回到合适的比例,而咱们使用的局部变量很快会被回收理论上存活对象仍是那么多,咱们的堆大小也会缩减回来没法达到扩充的目的。 与此同时这也是产生CONCURRENT GC的一个因素,后文咱们会详细讲到。
实际上,GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三种类型的GC都是在分配对象的过程触发的。而并发和非并发GC的区别主要在于前者在GC过程当中,有条件地挂起和唤醒非GC线程,然后者在执行GC的过程当中,一直都是挂起非GC线程的。并行GC经过有条件地挂起和唤醒非GC线程,就可使得应用程序得到更好的响应性。可是同时并行GC须要多执行一次标记根集对象以及递归标记那些在GC过程被访问了的对象的操做,因此也须要花费更多的CPU资源。后文在ART的并发和非并发GC中咱们也会着重说明下这二者的区别。
经过这个流程能够看到,在对象的分配中会致使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,好比咱们在该手机上decode图片,具体状况以下:
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的管理效率和总体性能,相似以下图:
kGcCauseForAlloc: 当要分配内存的时候发现内存不够的状况下引发的GC,这种状况下的GC会Stop World.
kGcCauseBackground: 当内存达到必定的阀值的时候会去出发GC,这个时候是一个后台GC,不会引发Stop World.
kGcCauseExplicit,显示调用的时候进行的gc,若是ART打开了这个选项的状况下,在system.gc的时候会进行GC.
其余更多。
因为ART下内存分配和Dalvik下基本没有任何区别,我直接贴图带过了。
ART在GC上不像Dalvik仅有一种回收算法,ART在不一样的状况下会选择不一样的回收算法,好比Alloc内存不够的时候会采用非并发GC,而在Alloc后发现内存达到必定阀值的时候又会触发并发GC。同时在先后台的状况下GC策略也不尽相同,后面咱们会一一给你们说明。
步骤1. 调用子类实现的成员函数InitializePhase执行GC初始化阶段。
步骤2. 挂起全部的ART运行时线程。
步骤3. 调用子类实现的成员函数MarkingPhase执行GC标记阶段。
步骤4. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
步骤5. 恢复第2步挂起的ART运行时线程。
步骤6. 调用子类实现的成员函数FinishPhase执行GC结束阶段。复制代码
步骤3. 调用子类实现的成员函数MarkingPhase执行GC并行标记阶段。
步骤4. 释放用于访问Java堆的锁。
步骤5. 挂起全部的ART运行时线程。复制代码
步骤6. 调用子类实现的成员函数HandleDirtyObjectsPhase处理在GC并行标记阶段被修改的对象。步骤12. 调用子类实现的成员函数FinishPhase执行GC结束阶段。复制代码
因此不管是并发仍是非并发,都会引发Stop World的状况出现,并发的状况下单次Stop World的时间会更短,基本区别和Dalvik相似。ART的并发GC和Dalvik的并发GC有什么区别呢,初看好像2者差很少,虽然没有一直挂起线程,可是也会有暂停线程去执行标记对象的流程。经过阅读相关文档能够了解到ART并发GC对于Dalvik来讲主要有三个优点点:
前台Foreground指的就是应用程序在前台运行时,然后台Background就是应用程序在后台运行时。所以,Foreground GC就是应用程序在前台运行时执行的GC,而Background就是应用程序在后台运行时执行的GC。
应用程序在前台运行时,响应性是最重要的,所以也要求执行的GC是高效的。相反,应用程序在后台运行时,响应性不是最重要的,这时候就适合用来解决堆的内存碎片问题。所以,Mark-Sweep GC适合做为Foreground GC,而Mark-Compact GC适合做为Background GC。
因为有Compact的能力存在,碎片化在ART上能够很好的被避免,这个也是ART一个很好的能力。
总的来看,ART在GC上作的比Dalvik好太多了,不光是GC的效率,减小Pause时间,并且还在内存分配上对大内存的有单独的分配区域,同时还能有算法在后台作内存整理,减小内存碎片。对于开发者来讲ART下咱们基本能够避免不少相似GC致使的卡顿问题了。另外根据谷歌本身的数据来看,ART相对Dalvik内存分配的效率提升了10倍,GC的效率提升了2-3倍。
当咱们想要根据GC日志来追查一些GC可能形成的卡顿时,咱们须要了解GC日志的组成,不一样信息表明了什么含义。
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。
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。
写在最后:图片来源自网络,特别鸣谢罗升阳。
文章来源于公众号:QQ空间终端开发团队(qzonemobiledev)
相关推荐
Android开发入门的正确姿式
Android 新一代多渠道打包神器
React + Redux 组件化方案
此文已由做者受权腾讯云技术社区发布,转载请注明文章出处
原文连接:www.qcloud.com/community/a…
获取更多腾讯海量技术实践干货,欢迎你们前往腾讯云技术社区