Android内存优化的思考

3,280 阅读14分钟

基础知识

内存概念

VSS- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)

RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)

PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存

虚拟内存

虚拟地址空间大小

影响大小的3个因素

页大小/地址长度/页表级数,大的在前小的在后 L3~L0

Android默认的页面大小配置为CONFIG_ARM64_4K_PAGES,即4KB,如果使用48位的地址空间需要4级页表来支持。

Android 默认的虚拟地址的长度配置为CONFIG_ARM64_VA_BITS=39

同时使用3级页表CONFIG_PGTABLE_LEVELS=3进行管理,

故Android的64位应用可使用的地址空间一般为2^39=512GB

进程空间分布

  • 代码段:存放用户进程代码,以及部分常量;
  • 数据段:存放用户进程初始化全局变量和静态变量;
  • BSS段:存放用户进程未初始化全局变量和静态变量;
  • 堆:存放用户进程动态申请的数据,堆是从低地址向高地址生长;
  • 内存映射区:用户动态链接库的加载、文件的映射,起始地址存储在mm结构体中的mmap_base变量;
  • 栈:存放环境变量、参数字符串、局部变量、函数栈帧。栈的首地址为STACK_TOP,一般默认开启栈随机化,因此实际的起始地址是STACK_TOP减去一个随机值。

Java堆

堆配置

dalvik.vm.heapgrowthlimit: [256m] (每个应用程序一般情况下堆最大内存可分配到内存)

dalvik.vm.heapsize: [512m] (每个应用程序最大堆内存可分配到内存,largeHeap=true的情况)

dalvik.vm.heapmaxfree: [8m] (堆最大空闲内存)

dalvik.vm.heapminfree: [6M] (堆最小空闲内存)

dalvik.vm.heapstartsize: [8m] (表示应用程序启动后为其分配的初始大小)

dalvik.vm.heaptargetutilization]: [0.75] (堆的目标利用率)

堆内存分布

Graphic

EGL mtrack: gralloc分配的内存,主要是窗口系统,SurfaceView/TextureView和其他的由gralloc分配的GraphicBuffer总和

GL mtrack: 驱动上报的GL内存使用情况。 主要是GL texture大小,GL command buffer,固定的全局驱动程序RAM开销等的总和

查看内存命令/工具

procrank (only for Android)

它从/proc/pid/maps中读取信息来进行统计。源码位于:/system/extras/procrank

内存耗用:VSS/RSS/PSS/USS

• VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)

• RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)

• PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

• USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

cat /proc/pid/status

VmPeak 虚拟内存使用量的峰值,取mm->total_vm和mm->hiwater_vm的大值。

VmSize:当前虚拟内存的实际使用量。

VmLck:PG_mlocked属性的页面总量,常被mlock()置位。

VmPin:不可被移动的Pined Memory内存大小。

VmHWM:HWM是High Water Mark的意思,表示rss的峰值。

VmRSS:应用程序实际占用的物理内存大小,这里和VmSize有区别。VmRss要小于等于VmSize。

RssAnon:匿名RSS内存大小。

RssFile:文件RSS内存大小。

RssShmem:共享内存RSS内存大小。

VmData:程序数据段的所占虚拟内存大小,存放了初始化了的数据。

VmStk:进程在用户态的栈大小。

VmExe:进程主程序代码段内存使用量,即text段大小。

VmLib:进程共享库内存使用量。

VmPTE:进程页表项Page Table Entries内存使用量。

VmPMD:进程PMD内存使用量。

VmSwap:进程swap使用量。
Name:   com.xxx.xxx

Umask:  0077

State:  S (sleeping)

Tgid:   16962

Ngid:   0

Pid:    16962

PPid:   629

TracerPid:      0

Uid:    10215   10215   10215   10215

Gid:    10215   10215   10215   10215

FDSize: 1024

Groups: 3001 3002 3003 9997 20215 50215 

VmPeak:  3854080 kB

VmSize:  3072956 kB

VmLck:         0 kB

VmPin:         0 kB

VmHWM:    750696 kB

VmRSS:    371612 kB

RssAnon:          229320 kB

RssFile:          141892 kB

RssShmem:            400 kB

VmData:  2078184 kB

VmStk:      8192 kB

VmExe:        24 kB

VmLib:    304248 kB

VmPTE:      3612 kB

VmPMD:        16 kB

VmSwap:    11652 kB

Threads:        275

SigQ:   0/21576

SigPnd: 0000000000000000

ShdPnd: 0000000000000000

SigBlk: 0000000000001204

SigIgn: 0000000000000001

SigCgt: 00000006400084f8

CapInh: 0000000000000000

CapPrm: 0000000000000000

CapEff: 0000000000000000

CapBnd: 0000000000000000

CapAmb: 0000000000000000

Seccomp:        2

Speculation_Store_Bypass:       unknown

Cpus_allowed:   ff

Cpus_allowed_list:      0-7

Mems_allowed:   1

Mems_allowed_list:      0

voluntary_ctxt_switches:        670221

nonvoluntary_ctxt_switches:     198816

/proc/< pid> /status简要分析 - ArnoldLu - 博客园

maps文件读取

只能被自己读取,其他用户没权限

adb shell run-as com.xxx.xxx showmap 23308

adb shell run-as com.xxx.xxx cat /proc/16962/smaps

adb shell run-as com.xxx.xxx pmap xx

linux proc maps文件分析_jiazheng.li的CSDN博客-CSDN博客_maps文件

eac52000-eac8a000 rw-p 00000000 00:00 0 [anon:dalvik-threa local mark ]

[anon:dalvik-threa local mark ]

对有名来说,是映射的文件名。对匿名映射来说,是此段虚拟内存在进程中的角色。[stack]表示在进程中作为栈使用,[heap]表示堆。其余情况则无显示

adb shell dumpsys meminfo

adb shell dumpsys meminfo

Applications Memory Usage (in Kilobytes):

Uptime: 289354670 Realtime: 348478673



** MEMINFO in pid 16962 [com.xxxx] **

                   Pss  Private  Private  SwapPss     Heap     Heap     Heap

                 Total    Dirty    Clean    Dirty     Size    Alloc     Free

                ------   ------   ------   ------   ------   ------   ------

  Native Heap   200662   200532        0      918   245504   222307    23196

  Dalvik Heap    69943    69860        0       74    50813    26237    24576

 Dalvik Other    18671    18668        0        0                           

        Stack      108      108        0        0                           

       Ashmem      242        4        0        0                           

      Gfx dev    23460    23276      184        0                           

    Other dev      131        4      124        0                           

     .so mmap    29702     2232    21904       24                           

    .jar mmap        6        0        4        0                           

    .apk mmap    28506    11156     6704        0                           

    .ttf mmap      247        0       36        0                           

    .dex mmap    64055    55240     4752        0                           

    .oat mmap     3793        0      612        0                           

    .art mmap     8124     5644      768        2                           

   Other mmap     3260       32     2076        1                           

   EGL mtrack    35136    35136        0        0                           

    GL mtrack   104280   104280        0        0                           

      Unknown     6002     5976        0       14                           

        TOTAL   597361   532148    37164     1033   296317   248544    47772

 

 App Summary

                       Pss(KB)

                        ------

           Java Heap:    76272

         Native Heap:   200532

                Code:   102640

               Stack:      108

            Graphics:   162876

       Private Other:    26884

              System:    28049

 

               TOTAL:   597361       TOTAL SWAP PSS:     1033

 

 Objects

               Views:     1605         ViewRootImpl:        1

         AppContexts:       11           Activities:        2

              Assets:       13        AssetManagers:        0

       Local Binders:      306        Proxy Binders:       56

       Parcel memory:       52         Parcel count:      208

    Death Recipients:        5      OpenSSL Sockets:        6

            WebViews:       20

 

 SQL

         MEMORY_USED:     7370

  PAGECACHE_OVERFLOW:     5553          MALLOC_SIZE:      215

 

 DATABASES

      pgsz     dbsz   Lookaside(b)          cache  Dbname

         4      308            103      765/61/25  /data/user/0/com.xxx.xxx/databases/mbridge.msdk.db

         4       24             34         7/25/4  /data/user/0/com.xxx.xxx/databases/omDB.db

         4      128            109      293/39/22  /data/user/0/com.xxx.xxx/databases/vungle_db

         4      108             85      416/82/25  /data/user/0/com.xxx.xxx/databases/google_app_measurement.db

         4       40             33         1/24/2  /data/user/0/com.xxx.xxx/databases/com.google.android.datatransport.events

         4       40             70       315/23/6  /data/user/0/com.xxx.xxx/databases/dt_event.db

         4      304             49       77/130/4  /data/user/0/com.xxx.xxx/databases/db_we_show

         4        8                         0/0/0    (attached) temp

         4      304             44        29/17/2  /data/user/0/com.xxx.xxx/databases/db_we_show (2)

         4       20             64         1/19/2  /data/user/0/com.xxx.xxx/databases/godap_download.db

         4    18244             54      2058/20/5  /data/user/0/com.xxx.xxx/no_backup/androidx.work.workdb (2)

                                   13877622/11861/25  /data/user/0/com.xxx.xxx/no_backup/androidx.work.workdb

 

 Asset Allocations

    : 155K

    : 157K

    : 160K

    : 1494K

    : 5873K

EGL mtrack/GL mtrack

EGL mtrack: gralloc分配的内存,主要是窗口系统,SurfaceView/TextureView和其他的由gralloc分配的GraphicBuffer总和

GL mtrack: 驱动上报的GL内存使用情况。 主要是GL texture大小,GL command buffer,固定的全局驱动程序RAM开销等的总和

android Profiler

developer.android.com/studio/prof…

内存信息

  • Java:从 Java 或 Kotlin 代码分配的对象的内存。
  • Native:从 C 或 C++ 代码分配的对象的内存。

即使您的应用中不使用 C++,您也可能会看到此处使用了一些原生内存,因为即使您编写的代码采用 Java 或 Kotlin 语言,Android 框架仍使用原生内存代表您处理各种任务,如处理图像资源和其他图形。

  • Graphics:图形缓冲区队列为向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
  • Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。
  • Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。
  • Others:您的应用使用的系统不确定如何分类的内存。
  • Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象

内存指标采集

内存

1.通过 ActivityManager 的 getProcessMemoryInfo => Debug.MemoryInfo 获取内存信息数据。

2.通过 Debug.MemoryInfo 的 getMemoryStats() 方法(os v23 及以上)可以获得 Memory Profiler 中的多项数据,进而获得 细分内存的使用情况。

3.接着,通过 Runtime 获取 DalvikHeap信息

4.最后,通过 Debug.getNativeHeapAllocatedSize 获取 NativeHeap

虚拟内存

读取/proc/self/status文件

读取/proc/self/smap文件

内存监测

OOM类型

1.pthread_create问题

2.文件描述符超限问题

3.堆内存超限

堆内存超限原因

1.单次配过大

2.累计使用过大

如何发现问题

提供场景信息

activity 事件信息

fragment 事件信息

WebView/SurfaceView/TextView attach/detach事件信息

场景进入退出信息

监测水位触顶

方法

1.通过 ActivityManager 的 getProcessMemoryInfo => Debug.MemoryInfo 获取内存信息数据。

2.通过 hook Debug.MemoryInfo 的 getMemoryStat 方法(os v23 及以上)可以获得 Memory Profiler 中的多项数据,进而获得 细分内存的使用情况。

3.接着,通过 Runtime 获取 DalvikHeap。

4.最后,通过 Debug.getNativeHeapAllocatedSize 获取 NativeHeap

阀值

1.堆内存超过最大值的85%的限制,GC 会变得更加频发,容易造成 OOM 和 卡顿。

2.32位机器虚拟内存超过3.7G会大概率挂

监测泄漏

1.Activity/Fragment/Service/Provider组件

Application.ActivityLifecycleCallbacks;

FragmentManager.FragmentLifecycleCallbacks;

需要注册三个包之下

android.app.Fragment

androidx.fragment.app.Fragment

android.support.v4.app.Fragment

2.大对象主动监测

播放对象/大的缓存对象等在业务结束的时候加入监控

监测大图

AOP插桩方式

1.监测 ImageVIew的setImageResource/setImageDrawable/setImageBitmap,判断图片大小是否大于View的宽高

2.监测Bitmap的createBitmap()/createScaledBitmap,记录大小超过阀值的图片信息,堆栈,

3.监测BitmapFactory.decodeFile()/decodeResuorce/decodeStream,记录大小超过阀值的图片信息,堆栈

Java hook方式

使用epic 同样直接hook以上方法

监测GC事件

1.Cleaner机制 ART虚拟机 | Finalize的替代者Cleaner - 掘金

2.bhook __android_log_write 接口获取GC日志信息

3.从 API 级别 30 开始,可通过调用 __android_set_log_writer 更改日志记录函数

developer.android.com/studio/comm…

4.参考ActivityThread.attach()中的方法,使用WeakReference持有覆盖finalize()方法的对象,在finalize()再重新生成WeakReference,参考

BinderInternal.addGcWatcher(new Runnable() {

                @Override public void run() {

                    if (!mSomeActivitiesChanged) {

                        return;

                    }

                    Runtime runtime = Runtime.getRuntime();

                    long dalvikMax = runtime.maxMemory();

                    long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();

                    if (dalvikUsed > ((3*dalvikMax)/4)) {

                        if (DEBUG_MEMORY_TRIM) Slog.d(TAG, "Dalvik max=" + (dalvikMax/1024)

                                + " total=" + (runtime.totalMemory()/1024)

                                + " used=" + (dalvikUsed/1024));

                        mSomeActivitiesChanged = false;

                        try {

                            mgr.releaseSomeActivities(mAppThread);

                        } catch (RemoteException e) {

                        }

                    }

                }

            });

监测内存分配

JVMTI功能

Android JVMTI实现应用内存动态检测

利用Android9.0虚拟机的JVMTI技术实现一些黑科技_z1032689332的博客-CSDN博客_安卓9虚拟机

1.类加载事件

2.GC启动和结束事件

3.内存分配事件

堆栈聚合

基于对象聚合

基于业务聚合

内存归因

ClassLoader

StatisticLogger

ImageLoader

ALog

监控线程创建

1.使用epic hook thread的start方法

2.记录线程创建的堆栈和信息

3.定时dump所有线程信息

分析heap时分析大对象的 RetainedSize/小对象的引用链路

1.加载全部对话信息

2.增加多层缓存,内存->磁盘缓存

3.缓存失效和清理

实际例子

1.Feed流中,LoadMore每次都加载新的视频数据,缓存数据可能一直增大

2.可以使用多级缓存,内存缓存超过一定大小,写到磁盘中

开源工具

Java hook

Epic

github.com/tiann/epic/…

PLT/GOT hook

bhook

github.com/bytedance/b…

JavaHeapDump

hprof的格式是什么

Hprof文件解析

工具

KOOM
Tailor

西瓜稳定建设Tailor

androidperformance.com/2015/04/11/… 内存优化之二 - MAT使用进阶](androidperformance.com/2015/04/11/…)

github.com/bytedance/t…

还原hprof文件

python3 ~/Desktop/work/opesource/tailor/library/src/main/python/decode.py -i ~/Desktop /mini.hprof -o ~/Desktop/target.hprof

hprof-conv转换

/Users/cy/Library/Android/sdk/platform-tools/hprof-conv /Users/cy/Desktop/target.hprof ~/Desktop/mat.hprof

Native内存

西瓜稳定建设 native

Raphel

python3 ~/Desktop/work/opesource/memory-leak-detector/library/src/main/python/raphael.py -r /Users/cy/Desktop/oom/report -o /Users/cy/Desktop/oom/leak.txt

python3 ~/Desktop/work/opesource/memory-leak-detector/library/src/main/python/mmap.py -m /Users/cy/Desktop/oom/maps

adb shell am broadcast -a com.apm.ACTION_DUMP_ALL

adb shell am broadcast -a com.apm.ACTION_NATIVE_DUMP

优化

泄漏问题

1.activity/fragment/service/provider泄漏问题

2.view泄漏问题

  1. 主动释放泄漏的 Activity 持有的 主动回收View和View使用的资源

1.activity可以调用setContentView(null)

2.遍历view释放ViewTree 的背景图和 ImageView 中的图

图片合理性

1.图片大小的合理性

资源图片:

资源图片放错目录会放大

资源图片直接把留白和核心内容切了一整张大图,可以只切核心内容部分

能有drawable写,比如规则渐变色等直接使用代码写

其他图片:

其他图片加载需要考虑显示区域大小

2.缓存的图片不要过多

1.RecycleView item回收的时候,要清空使用状态

2.控制图片框架加载的 Cache 大小一般不超过2屏大小

3.Android O以上可以使用硬件位图

4.质量要求不要情况下,inPreferConfig可以降低到565配置

3.复用

1.底图或者框架图,可以使用全局实例

4.内存吃紧的情况下主动释放图片缓存。

onLowMemory/onTrimMemory中调用Glide.clearMemoryCache()

系统资源限制

1.文件描述符限制,O 以下设置为1k,新的一般设置为32k

2.线程个数限制,pthread_create问题

3.收敛线程

EL/EGL 内存

1.Window的个数不要太多

2.GPU显存使用问题,硬件加速中图片是先上传到纹理中的

3.SurfaceView和TextureView问题

Native内存

线程收敛、监控

线程栈泄漏自动修复

FD 泄漏监控

虚拟内存监控、优化

64 位专项

保障体系建设

1.提供哪些数据能分析得到准确的问题场景和原因

2.如何防止劣化

开发阶段

测试阶段

灰度阶段

上线阶段

案例

1. CursorWindow from Parcel due to error -12, process fd count=707

android.database.CursorWindowAllocationException: Could not create CursorWindow from Parcel due to error -12, process fd count=707

        at android.database.CursorWindow.nativeCreateFromParcel(Native Method)

        at android.database.CursorWindow.<init>(CursorWindow.java:167)

        at android.database.CursorWindow.<init>(CursorWindow.java:45)

        at android.database.CursorWindow$1.createFromParcel(CursorWindow.java:716)

        at android.database.CursorWindow$1.createFromParcel(CursorWindow.java:714)

        at android.database.BulkCursorDescriptor.readFromParcel(BulkCursorDescriptor.java:75)

        at android.database.BulkCursorDescriptor$1.createFromParcel(BulkCursorDescriptor.java:34)

        at android.database.BulkCursorDescriptor$1.createFromParcel(BulkCursorDescriptor.java:30)

        at android.content.ContentProviderProxy.query(ContentProviderNative.java:426)

        at android.content.ContentResolver.query(ContentResolver.java:946)

        at android.content.ContentResolver.query(ContentResolver.java:881)

        at androidx.core.content.ContentResolverCompat.query(SourceFile:81)

        at androidx.loader.content.CursorLoader.loadInBackground(SourceFile:63)

        at androidx.loader.content.CursorLoader.loadInBackground(SourceFile:41)

        at androidx.loader.content.AsyncTaskLoader.onLoadInBackground(SourceFile:307)

        at androidx.loader.content.AsyncTaskLoader$LoadTask.doInBackground(SourceFile:60)

        at androidx.loader.content.AsyncTaskLoader$LoadTask.doInBackground(SourceFile:48)

        at androidx.loader.content.ModernAsyncTask$2.call(SourceFile:141)

        at java.util.concurrent.FutureTask.run(FutureTask.java:266)

        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)

        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)

        at java.lang.Thread.run(Thread.java:919)

2.pthread_create (1040KB stack) failed

java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again

        at java.lang.Thread.nativeCreate(Native Method)

        at java.lang.Thread.start(Thread.java:883)

        at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975)

        at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1382)

        at androidx.arch.core.executor.DefaultTaskExecutor.executeOnDiskIO(SourceFile:59)

        at androidx.arch.core.executor.ArchTaskExecutor.executeOnDiskIO(SourceFile:96)

        at androidx.arch.core.executor.ArchTaskExecutor$2.execute(SourceFile:53)

        at androidx.room.InvalidationTracker.refreshVersionsAsync(SourceFile:442)

        at androidx.room.RoomDatabase.endTransaction(SourceFile:368)

        at me.vd.lib.file.manager.db.dao.PrivateFileDao_Impl.insertPrivateFiles(SourceFile:204)

        at me.vd.lib.file.manager.manager.PrivateFolderManager.getPrivateFiles(SourceFile:597)

        at me.vd.lib.file.manager.manager.PrivateFolderManager$getPrivateFiles$1.invokeSuspend(Unknown Source:11)

        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(SourceFile:33)

        at kotlinx.coroutines.DispatchedTask.run(SourceFile:56)

        at kotlinx.coroutines.scheduling.CoroutineScheduler.a(SourceFile:571)

        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(SourceFile:2738)

3.graphic内存暴涨

1.基于场景切换发现

4.在Native层,Crash是vss超了3.7G后,随机出现某个场景

5.HandlerThread泄漏

6.EventBus泄漏

7.Feed流页面

首页推荐列表的每一次 Loadmore 操作,都不会清理之前缓存起来的视频对象,导致用户长时间停留在推荐 Feed 时,存起来的视频对象过多会导致内存方面的压力