当前位置:首页 » 《随便一记》 » 正文

Android R常见GC类型与问题案例_内核工匠

28 人参与  2022年03月04日 11:09  分类 : 《随便一记》  评论

点击全文阅读


前言

Android系统的APP运行需要依赖ART虚拟机(Android Runtime),ART虚拟机的主要作用是给APP的java代码提供运行环境,其中编译、执行、垃圾回收(GC)模块是ART虚拟机的重中之重。GC使得java开发人员能专注于业务实现,而不用担心内存泄漏。

此文章将简要的向大家介绍ART虚拟机中Heap布局、常见GC类型和对应的问题案例。为大家分析优化应用提供一些思路。

本文基于的代码和调试手机系统为Android R(11)版本。

一、GC的相关配置

1. 内存回收器(回收算法)、内存分配器

因为Android R支持读屏障(kUseReadBarrier),在虚拟机创建阶段,内存回收器设置为:

*前台回收器foreground_collector_type = kCollectorTypeCC

*后台回收器background_collector_type = kCollectorTypeCCBackground

##对于CC(ConcurrentCopying)并发复制回收器而言,前后台回收器只用于配置前后台GC时的一些回收器参数,实质回收器只有一种即kCollectorTypeCC。一般当应用退到后台,系统希望应用尽可能释放UI相关的垃圾对象并最大程度的进行内存整理。

4a74814525b06b0939bc308b7daeda63.png

##读屏障(kUseReadBarrier)有三种实现方式,涉及到内存回收算法的底层实现逻辑,读者可自行了解,本文不做阐述。

ART虚拟机创建时先确定内存回收器的类型,进而绑定对应的内存分配器,因回收器已设置为CC(ConcurrentCopying)即并发复制回收器,则Heap内存的主要分配区域定为RegionSpace,RegionSpace由一个个256KB的Region组成。对应的RegionSpace内存分配器定为kAllocatorTypeRegion。

从下图代码中可知,并发复制回收若启用分代GC,默认回收策略是Sticky即只回收上次GC后新生成的对象。若未启用分代,默认回收策略是Full即扫描所有space包括zygoteSpace的对象做回收。Android R版已启用分代,则默认策略模式是Sticky。

e93e92e2efde420bbb97308cb1c4773a.png

二、ART虚拟机Heap内存布局

1. 应用Heap布局示例

作者使用Android R版本手机,安装HelloWorld应用查看进程map文件,应用的Heap布局如下图。

此手机Heap参数和应用large_heap配置如下,应用可用堆上限为256MB。

[dalvik.vm.heapgrowthlimit]: [256m]

[dalvik.vm.heapsize]: [512m]

android:largeHeap="false"

8ee4313b35b8f1f0d81df8ef71e60eb4.png

2. Space类型

(1)RegionSpace  [512M]:

A) 虚拟地址起点为300MB(0x12c00000),可看到图中和ImageSpace之间地址不连续。

B) 虚拟地址范围:为支持堆完整复制,需要额外预留一倍空间。当进程启动后BindApplication阶段调用ClampGrowthLimit()将RegionSpace 虚拟地址空间size设置为两倍的堆上限。

C) 内存分配:绝大多数的对象分配的区域。RegionSpace由256KB大小的Region组成,Region的分配算法是最简单的指针碰撞(BumpPointer)。

当一次分配请求到来,通过For循环遍历可用的Region。当已分配的Region数量已超过总数的一半,不允许再占用新的Region,只能依赖内存回收和整理来释放内存用于分配。

D) 回收策略:当一个Region中存活对象占用大小超过75%时,此Region标记为kRegionTypeUnevacFromSpace,表示无需进行拷贝。否则将此Region中存活对象拷贝至标记成ToSpace的另一个Region中,拷贝完成后回收整个Region,立刻释放256KB内存。

具体的算法实现更加复杂,读者可进一步了解CC算法的实现。

(2) ImageSpace  [4M]:

A) Zygote进程创建时,根据boot.art文件ImageHeader结构体中指定的映射地址(0x70e44000),创建ImageSpace,将boot.art内存镜像文件映射到进程的虚拟地址空间。

B) ImageSpace不支持分配对象,也无需回收。

C) ImageSpace创建后同步映射boot.oat文件到ImageSpace地址后面。

D) ImageSpace中映射的.art文件可能有多个,比如boot-core-libart.art/boot-okhttp.art。boot.oat区域亦然。

(3)ZygoteSpace  [3M]:

A)包含Zygote进程从创建到第一次Fork前所有存活的对象。当Fork新进程时无需再单独映射或创建必需的对象,直接复用Zygote进程的数据,加快进程创建。

B)创建过程:

1 Zygote进程创建Heap时,申请一块DlmallocSpace类型的64MB地址空间,命名为“zygote/non moving space”。

2 Zygote进程此时可在“zygote/non moving space”和RegionSpace中分配对象。

3 当Fork SystemServer时,Zygote进程进行一次Full GC并Trim裁剪内存,将回收后的内存还给系统。再将RegionSpace中存活对象复制到“zygote/non moving space”,将两部分的存活对象进行一次压缩。整个64MB空间一拆为二,压缩后保留Zygote进程所有存活对象的Space命名为“zygote space”,剩余的空闲内存命名为“non moving space”。

C)一拆为二后,新的“zygote space”不再支持内存分配与释放。

(4)Non moving space  [61M]:

A) 分配算法:Dlmalloc。

B) 存放AllocNonMovableObject分配的obj,主要是类加载过程创建的类对象(Class)、类方法对象(ArtMethod)、类成员变量对象(ArtField)。

C) 创建过程见2.2.3。

(5) LOS-LargeObjectSpace  [512M]:

A) LOS有两种实现类型(free list\mmap),Android R默认实现为free list形式,以Heap size(capacity_)作为参数调用FreeListSpace::Create()接口创建。

B) 单次分配超过12KB的String和基础类型的数组如int[],在LOS中分配。

三、GC的常见类型

1. GC流程简介

GC相关博客文章已经浩如烟海,推荐读者阅读邓凡平老师的《深入理解Android:java虚拟机ART》、周志明老师的《深入理解java虚拟机》和网上的博客例如罗升阳、芦航等,本文不做铺陈。

2. GC类型与案例

为了反映不同场景下的GC,作者使用GC对应用状态的影响和gc_cause(触发原因)来做区分。能够更好的阐述GC不同类型之间的异同。

基于GC对应用状态的影响将GC初步分为两类,并发类和阻塞类。并发类GC指GC在GC回收线程(HeapTaskDaemon)执行,阻塞类GC在进程的工作线程执行。

再根据gc_cause具体分为Background GC\Native GC\CollectorTransition GC\Alloc GC\Explicit GC。

(1) 并发类GC

并发类GC因为运行在HeapTaskDaemon线程中,对应用的状态影响较小,一般情况下对于用户体验和应用逻辑是透明的。并发类GC对于内存回收具有至关重要的作用,减少系统处于低内存和大量内存碎片的情况。对于如今的手机SOC算力和配置来说,CPU能力比较富余,而内存不太富余,应当在合适的时机多执行并发类GC。

但是在一些特殊场景如某些特殊应用、低端手机中,因并发类GC带来的卡顿、高负载、功耗问题也很常见,主要的影响有以下几类。

(A) HeapTaskDaemon运行GC时,CC算法需要很强算力,容易抢占CPU大核\超大核资源,导致应用UI绘制相关线程task无法及时被CPU调度或UI绘制相关线程被挤在小核中无法在一个Vsync周期完成绘制与渲染。进而引起卡顿不流畅。

(B) 后台驻留应用多,且后台活动频繁时,多个后台HeapTaskDaemon占用多个CPU核,加重负载情况,引起整机高负载,在整机高负载下各进程的线程存在各种Runnable和Uninterruptable sleep(D)状态,各种SystemServer等锁、ANR、IOwait、BlockMsg情形接踵而至,卡顿也就逃不掉了。

(C) CC回收算法STW(stop the world)时间即pause time已经非常低,但是依然存在一些场景下,因CC算法某些阶段需要STW(比如ReclaimPhase阶段需要LockHeap锁堆,锁堆会STW),造成进程除HeapTaskDaemon外的其他线程都处于Sleeping状态,特殊应用可能出现STW时间达到几十ms级别以上,会导致卡顿问题。见3.2.1.1.1点。

(D) Blocking GC导致丢帧,例如主线程调用System.gc()进行一次显示GC,因ART虚拟机一次只能发起一次GC,若此时有其他GC在运行,需等待此次GC完成,主线程一直等待导致丢帧卡顿。

(E) 特殊应用在用户操作时临时对象分配内存过多,HeapTaskDaemon持续进行GC,GC频繁导致场景功耗电流很高,影响续航与发热。

① Background GC/Bg GC

此类为最常见的GC类型,大致90%以上的GC都是此类型。

(A) 触发逻辑:ART 中heap.cc维护一个Background GC水位值(concurrent_start_bytes_),此水位值由以下因素(multiplier\maxfree\minfree\utilization\gctype)共同影响。在每次GC完成后,调用GrowForUtilization()接口计算Background GC水位值。

     GC水位计算比较简单,读者可百度GC触发时机相关文章,本文不再铺陈。

在每次对象分配后,判断已分配java堆大小和水位的对比,若超水位立刻触发Background GC。

(B) 作用:时刻追踪java堆大小,避免垃圾对象过多,及时回收内存,减少不必要的内存消耗。

1) 案例1【前台应用Bg GC频繁,引起主观滑动卡顿,顿挫感明显】

应用:新浪新闻V7.63.1

场景:视频>>小视频>>短视频展示页,上下滑动,明显卡顿

systrace情形:HeapSize呈锯齿状,HeapTaskDaemon中GC频繁,Sticky/Full GC互相交替运行。

卡顿根因:滑动过程此应用临时对象占用内存过多,堆Heap size上升迅速,达到GC水位线,触发Bg GC。GC回收ReclaimPhase阶段lock heap锁堆各线程被pause,主线程Sleeping耗时过长丢帧卡顿。

优化方向:应用内存使用不合理,反馈应用优化。

cd057cbf0f65dda981f31a142bd71bc2.png

2) 案例2【后台应用Bg GC频繁,引起整机高负载,引起前台应用卡顿】

场景:后台驻留20-30个应用,压力测试应用启动和关闭时桌面动画流畅性

Systrace情形:后台应用活动频繁,多个进程HeapTaskDaemon进程占用卡顿时间段内30-50% CPU资源。

卡顿根因:后台应用活动频繁,引起整机高负载,引起前台应用的UI相关线程资源调度不及时。

优化方向:1 排查后台管控策略,加强管控能力 2 缓解方案:减少高负载时Bg GC,优化HeapTaskDaemon调度

6999f9c8b734788e1da7b551d0a247a2.png

b84e0d7424f4d01669d3515d48f98088.png

② Native GC/NativeAlloc GC

ART可以管理一些特定的Native内存。某些java类(如Bitmap)使用NativeAllocationRegistry类申请和释放Native内存时会同步告知ART,ART可监控此类由java对象引用的Native对象内存。

此类引用模式就像“提线木偶”,java对象引用Native对象,java对象内存占用很小,大头在Native对象。在ART回收java对象时,此java对象已设置死亡回调会主动释放引用的Native内存。

基于此原理ART虚拟机可间接管理一部分Native内存。

##注意,开发人员在C++\C代码中malloc\mmap分配的Native内存,无法被ART管理,此部分内存使用没有上限,只能依靠LMKD或虚拟地址空间耗尽OOM,进程死亡后回收。

(A) 触发逻辑:

1 单次超过300KB的Native内存分配,触发Native已分配内存和Native GC水位检查,超过则进行一次NativeGC。

2 每32次小于300KB的Native内存分配,触发一次检查,超过Native GC水位进行一次NativeGC。

(B) 与Background GC异同点:GC代码流程完全一样,主要为触发原因不同。Bg GC回收Bitmap,也会自动释放Bitmap关联的Native内存。

 案例1【Native GC频繁引起特定场景高负载】

场景:应用内退出到桌面、特定应用(如相机、相册)冷热启动

Systrace表现:应用进程HeapTaskDaemon线程运行gc_cause为NativeAlloc的GC,增加系统负载。

优化方向:simplePerf确认触发堆栈,推动优化,若为超过300KB的NativeGC,强烈建议优化掉,减少必现的GC。

1f33c5b2d74a941cef2bc6afaa125b4d.png

④ CollectorTransition GC

此类GC发生在应用processState在IMPORTANT_FOREGROUND(6)状态上下切换时,processState<=6代表应用可被感知,>6代表应用活动不被感知。ART中只关心是否能被感知,进而调整一些GC的参数。在ART设计中,应用processState>6时,代表应用退入后台用户无法感知(STW时间不关心),此时应进行内存回收和整理,减少内存碎片,提升内存利用率。CollectorTransition GC就是起此作用。

(A) 触发逻辑:应用退入后台即processState>6,代表用户无法感知此应用,请求一次delay 5s的并发GC,gc_cause为kGcCauseCollectorTransition。

(B) 与Background GC异同点:GC逻辑流程一致,但是CC回收器对Explicit GC和CollectorTransition GC会进行最大程度的内存整理,所有region会被拷贝至tospace,不受75%比例的限制,存活对象整理到新的region中。

⑤ 案例1【CollectorTransition GC频繁引起应用退出动画卡顿】

应用:腾讯视频

场景:后台驻留20-30个应用,压力测试应用开启与关闭时的流畅性

Systrace表现:桌面启动应用时动画不流畅,整机负载偏高,Heaptaskdaemon为负载TOP1,主要占据4个小核。

卡顿根因:GC负载主因是腾讯视频退后台,延迟5S后触发CollectorTransition类型的GC,且腾讯视频同个UID下5个进程同步触发。刚好遇上桌面启动应用过程。另外其他应用的GC也有一定负载影响。

优化方向:1 加强后台进程管控(退后台快速冻结,限制资源使用等)2 保证内存压缩效果情况下减少CollectorTransition GC。

18bf1cf162135ce1ec5233e95a30e635.png

fc2beccc57c7a59315f408e022e41fab.png

(2) 阻塞类GC

作者将运行在工作线程中的GC归类为阻塞类GC,主要有两类:Alloc GC和Explicit GC,阻塞类GC会Block工作线程,可能出现GC耗时过长,导致进程出现操作卡顿、ANR、卡死等情况发生。在应用设计中,应尽可能避免此类GC。

Alloc GC常见于进程heap size触顶即达到堆上限,无法再继续分配对象,将发起一次或多次GC回收内存再尝试分配,若依然分配失败,会触发OOM进程死亡。

Explicit GC常见于两种情况,1 应用代码中主动调用Runtime.gc()\System.gc(),主要见于APP覆写onTrimMemory()接口。2 SystemServer\系统应用\adb调试等进程给对应进程发送kill -10产生SIGUSER1的signal,此进程的SignalCatcher线程捕获signal后执行一次Explicit GC。

阻塞类GC主要影响有以下几点:

(A) 阻塞工作线程运行,GC耗时普遍在50ms-200ms间甚至更长,APP逻辑运行时间增加,可能引起卡顿问题。

(B) 应用主动频繁调用Explicit GC,可能出现回收效果甚微,但增加大量无效负载,引起功耗增加,续航减少,手机发热等问题。

(C) 在进程heap size快要触顶达到堆上限时,一般是内存泄漏场景,此时各工作线程和HeaptaskDaemon都在发起各种GC,各类GC互相Block等锁,导致应用卡死。头部TOP应用比较常见。

① Alloc GC

从项目经验看,因为最近几年的Android版本都使用CC回收器,RegionSpace的limit已经设置成堆上限值,出现Alloc GC即可代表heap size接近触顶快到达堆上限,几乎无法回收出内存。

绝大部分都是应用出现内存泄漏。此时多次Alloc GC也释放不出内存,出现各种Block等锁,应用卡死,挤牙膏挤不出,或者只能挤一点点,等待此进程的要不卡死要不就是OOM。

以下两个案例介绍这种情形。

1) 案例1【Alloc GC频繁引起应用操作卡顿】

应用:内部测试工具

场景:测试工具持续压力使用,应用长时间卡顿定屏

Systrace表现:如下图,heapsize已经达到242M,此时分配大内存对象失败,主线程触发多次Alloc GC。因内存泄漏,并未回收出内存,应用马上出现OOM闪退。

卡顿根因:应用内存泄漏。

优化方向:1 解决内存泄漏点 2 若应用必须占用大量内存,可配置android:largeHeap="true" 3 fork新进程实现功能,避免单个进程heap size过高

5bfdf9483a9fad5c85dbb0704e306f0f.png

2) 案例2【Block GC频繁引起应用卡死】

应用:抖音

场景:长时间压力测试抖音视频切换

问题特征:

1 Log:卡死期间一直打印Starting a blocking GC xx和WaitForGcToComplete blocked xx on xx for xxms

2 Systrace:Heap size触顶,且各线程有很多TAG(GC:Wait For Completion xx)

3 Meminfo:进程此时内存占用2G以上

卡死根因:抖音内存泄漏,各工作线程发起Alloc GC,但HeapTaskDaemon正在运行GC,各工作线程等锁Sleeping,造成卡死。

优化方向:推动应用优化内存泄漏(已反馈,新版本已优化)

c001aab2e5da7c8a2f5f74bf6ca0d660.png

906fc08a1b0393b751edabd674aa3f2f.png

② Explicit GC/显式GC/强制GC/

见3.2.2阻塞类GC的阐述,此类GC多见于应用主动调用Runtime.gc()\System.gc(),以下两个案例分别为OnTrimMemory调用显式GC和工作线程中主动调用显式GC。

##Explicit GC和CollectorTransition GC一样,没有MakingPhase阶段,因为系统设定两种GC都会做最大力度的内存整理,所有region的存活对象都会被拷贝,不受75%存活量占比限制。

b2abbfc818575d074b14897f65fdba92.png

1)案例1【OnTrimMemory 中Explicit GC频繁引起应用卡顿】

应用:平行空间、bima+

场景:后台驻留35应用,自动化用例运行24小时,检查各进程丢帧与系统各指标

特征: systrace文件中发现doFrame的commit callback中执行trimMemory,执行一次concurrent copying GC。导致此次doFrame耗时过长,丢帧卡顿。

根因: 系统低内存触发OnTrimMemory回调,应用在OnTrimMemory中直接调用显式GC接口。

优化方向:1 系统分析优化低内存场景 2 应用实现OnTrimMemory因根据trimLevel和自身应用状态,根据情况按需调用显式GC接口,并且调用操作在子线程中实现。

77909ea7d6485c324a24b10d33e89b10.png

2) 案例2【微信视频号/视频直播页面Explicit GC频繁】

应用:微信 V8.0.11

场景:

1 发现》直播和附近》直播》任意打开一个直播》上下滑动切换,必现一次显式GC

2 发现》点击视频号,必现一次显式GC

3 点击好友头像》进入好友详情页,点击好友的视频号,必现一次显式GC

优化方向:google已明确不建议应用调用显式GC来回收内存,Bg GC已经可以很好的回收垃圾对象,应用逻辑必现的显式GC,应优化内存分配逻辑,去除显式GC调用。减少无效负载。

当前此问题正和微信对接中。

baefc8e566fddfc8d808e18406464f8b.png

d423be3538b932d2fc2f007410723040.png

60479a306fa4fda3f5be550c29fd6947.png

四、总结

本文从Android R版本GC相关配置为入口,以HelloWorld应用为例介绍Heap布局。能够让读者对Android R版本java内存分配和管理有一个概念认识。紧接着以实际GC类型和案例,介绍各种不同gc cause GC的触发逻辑和影响,为系统开发人员和APP客户端开发人员提供项目GC问题优化思路。

参考资料

1) Android R源码

2) 《深入理解android java虚拟机art-邓凡平》

3) 老罗的Android之旅-https://blog.csdn.net/Luoshengyang?t=1&type=blog

4) ART的堆内存布局-https://www.cnblogs.com/YYPapa/p/6851299.html

5) ART虚拟机 | 如何让GC同步回收native内存-https://juejin.cn/post/6894153239907237902

6)Android GC 简史-https://juejin.cn/post/6966205309782065159

4abdb1585d9194b0f05b2a2f51060e41.gif

长按关注

内核工匠微信

Linux 内核黑科技 | 技术文章 | 精选教程


点击全文阅读


本文链接:http://zhangshiyu.com/post/35739.html

内存  回收  进程  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1