Anndroid GC 那些事
内存回收机制对于app性能优化中比较重要部分,我们要做好优化工作,Android GC工作情况我们需要熟知,
因此整理了一下关于GC知识点,主要分为Dalvik与ART两部分
Dalvik堆内存结构:
Dalvik虚拟机的堆划分为Active堆和Zygote堆,Dalvik虚拟机的堆最初是只有一个,Zygote进程在fork第一个应用程序进程之前,会将已经使用了的那部分堆内存划分为一部分,
还没有使用的堆内存划分为另外一部分。前者就称为Zygote堆,后者就称为Active堆。以后无论是Zygote进程,还是应用程序进程,当它们需要分配对象的时候,都在Active堆上进行,在Zygote堆里面分配的对象其实主要就是Zygote进程在启动过程中预加载的类、资源和对象了。这意味着这些预加载的类、资源和对象可以在Zygote进程和应用程序进程中做到长期共享。这样既能减少拷贝操作,还能减少对内存的需求。Davlk虚拟机使用标记-清除(Mark-Sweep)算法进行GC。在标记阶段,通过一个Mark Stack来实现递归检查被引用的对象,即在当前GC中存活的对象
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后垃圾回收器都会尽量让堆的利用率往目标利用率靠拢。所以当我们尝试生成一些几百K的对象,试图去扩大可用堆大小的时候,反而会导致频繁的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事件的阈值。
GC执行流程:
串行GC执行过程:
1. 调用子类实现的成员函数InitializePhase执行GC初始化阶段。
2. 挂起所有的ART运行时线程。
3. 调用子类实现的成员函数MarkingPhase执行GC标记阶段。
4. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
5. 恢复第2步挂起的ART运行时线程。
6. 调用子类实现的成员函数FinishPhase执行GC结束阶段。
并发GC执行流程:
1. 调用子类实现的成员函数InitializePhase执行GC初始化阶段。
2. 获取用于访问Java堆的锁。
3. 调用子类实现的成员函数MarkingPhase执行GC并行标记阶段。
4. 释放用于访问Java堆的锁。
5. 挂起所有的ART运行时线程。
6. 调用子类实现的成员函数HandleDirtyObjectsPhase处理在GC并行标记阶段被修改的对象。。
7. 恢复第4步挂起的ART运行时线程。
8. 重复第5到第7步,直到所有在GC并行阶段被修改的对象都处理完成。
9. 获取用于访问Java堆的锁。
10. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
11. 释放用于访问Java堆的锁。
12. 调用子类实现的成员函数FinishPhase执行GC结束阶段。
从上面的分析就可以看出,并行GC和非并行GC的区别在于:
1. 非并行GC的标记阶段和回收阶段是在挂住所有的ART运行时线程的前提下进行的,因此,只需要执行一次标记即可。
2. 并行GC的标记阶段只锁住了Java堆,因此它不能阻止那些不是正在分配对象的ART运行时线程同时运行,而这些同进运行的ART运行时线程可能会引用了一些在之前的标记阶段没有被标记的对象。如果不对这些对象进行重新标记的话,那么就会导致它们被GC回收,造成错误。因此,与非并行GC相比,并行GC多了一个处理脏对象的阶段。所谓的脏对象就是我们前面说的在GC标记阶段同时运行的ART运行时线程访问或者修改过的对象。
3. 并行GC并不是自始至终都是并行的,例如,处理脏对象的阶段就是需要挂起除GC线程以外的其它ART运行时线程,这样才可以保证标记阶段可以结束。
回收算法和内存碎片
主流的大部分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堆内存结构:
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的管理效率和整体性能,
ART回收机制:
GC的类型
kGcCauseForAlloc: 当要分配内存的时候发现内存不够的情况下引起的GC,这种情况下的GC会Stop World.
kGcCauseBackground: 当内存达到一定的阀值的时候会去出发GC,这个时候是一个后台GC,不会引起Stop World.
kGcCauseExplicit,显示调用的时候进行的gc,如果ART打开了这个选项的情况下,在system.gc的时候会进行GC.
对象的分配和GC触发时机
两种可能会触发GC的情况。第一种情况是没有足够内存分配请求的分存时,会调用Heap类的成员函数CollectGarbageInternal触发一个原因为kGcCauseForAlloc的GC。第二种情况下分配出请求的内存之后,堆剩下的内存超过一定的阀值,就会调用Heap类的成员函数RequestConcurrentGC请求执行一个并行GC。
前台与后台GC
前台Foreground指的就是应用程序在前台运行时,而后台Background就是应用程序在后台运行时。因此,Foreground GC就是应用程序在前台运行时执行的GC,而Background就是应用程序在后台运行时执行的GC。
应用程序在前台运行时,响应性是最重要的,因此也要求执行的GC是高效的。相反,应用程序在后台运行时,响应性不是最重要的,这时候就适合用来解决堆的内存碎片问题。因此,Mark-Sweep GC适合作为Foreground GC,而Mark-Compact GC适合作为Background GC。 由于有Compact的能力存在,碎片化在ART上可以很好的被避免,这个也是ART一个很好的地方。
与Dalvik虚拟机GC相比,ART运行时GC的优势在于:
1. ART运行时堆的划分和管理更细致,它分为Image Space、Zygote Space、Allocation Space和Large Object Space四个Space,再加上一个Allocation Stack。其中,Allocation Space和Large Object Space和Dalvik虚拟机的Zygote堆和Active堆作用是一样的,而其余的Space则有特别的作用,例如,Image Space的对象是永远不需要回收的。
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. ART运行时充分地利用了设备的CPU多核特性,在并行GC的执行过程中,将每一个并发阶段的工作划分成多个子任务,然后提交给一个线程池执行,这样就可以更高效率地完成整个GC过程,避免长时间对应用程序造成停顿。
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
参考资料:
dalvik
http://blog.csdn.net/luoshengyang/article/details/41581063
http://blog.csdn.net/luoshengyang/article/details/41688319
art
http://blog.csdn.net/luoshengyang/article/details/42379729
http://blog.csdn.net/luoshengyang/article/details/42555483
https://developer.android.com/studio/command-line/logcat.html
https://developer.android.google.cn/topic/performance/memory-management (memory train)