【性能优化】安卓性能优化之内存优化

发布时间:2026/10/17 5:27:00
【性能优化】安卓性能优化之内存优化
安卓性能优化之内存优化内存优化基础概念常见内存泄漏Java 内存划分对象存活判断算法垃圾回收算法Android Studio Profiler各项指标实时获取线程数量adb获取内存信息当前系统的内存信息查看JNI 引用数量虚拟内存Java堆FD数量Native内存native内存泄漏的检测MAT线上方案常见问题标记回收算法被标记了一定会被回收吗问题排查案例Java堆暴增导致OOMNative内存不断增长内存优化Android内存优化参考JsonChao内存优化总结常用工具leakcanary、MAT大图检测使用EPIC hook 图片加载ASM对图片加载入口进行代码插桩基础概念几种内存问题的区别内存抖动在短时间内反复地发生内存增长和回收导致程序卡顿甚至OOM内存溢出内存泄漏一些内存对象无法按预期的释放内存溢出申请分配内存时超过系统所能提供的OOM的几种类型java.lang.StackOverflowError 栈溢出递归、死循环等因为每次方法调用都会有一个方法栈压入导致OOMjava.lang.OutOfMemoryError:Java heap space创建了非常多的对象实例/存储了过大的数据导致无法在新建对象java.lang.OutOfMemoryError:GC overhead limit exceeded频繁GC并且回收的内存空间很少导致死循环超过98%的时间用来做GC并且回收了不到2%的堆内存java.lang.OutOfMemoryError:unable to create new native thread创建的线程太多了一般默认一个进程可以创建的线程数为1024个java.lang.OutOfMemoryError:Direct buffer memory本地内存满了一般是NIO中ByteBuffer.allocateDirect()分配的内存这个内存不属于GC管辖可能不会被及时回收java堆和native堆的区别java堆主要是Java代码分配的对象native堆主要是C代码malloc分配的内存一些Java代码也会造成native堆内存分配比如bitmap虚拟内存作为内存地址的抽象提供一个映射到真实的内存地址使每个进程认为自己拥有连续的内存地址提高内存使用的效率内存包含和隔离每个进程只能访问到自己的内存地址避免直接操作物理内存分页与置换物理内存与虚拟内存都是以页为单位进行管理一般是4K或8K以页为单位的管理使内存管理更加高效内存优化常见思路对象复用/享元模式减少不必要的内存分配监测是否内存泄漏、是否因为生产者消费者模型未及时处理导致内容堆积使用合理的数据结构例如SparseArray、ArrayMap 来替代 HashMap内存引用的几种类型强引用在内存不足时不会被回收。平常用的最多的对象如新创建的对象。软引用在内存不足时会被回收(无强引用)。用于实现内存敏感的高速缓存。弱引用只要GC回收器发现了它就会将之回收(无强引用)。用于Map数据结构中引用占用内存空间较大的对象。虚引用在任何时候都可能被垃圾回收器回收(无强引用)。常见内存泄漏Handler持有外部引用单例泄漏持有Activity而不是Application的Context匿名线程持有外部引用但是Lambda表达式没有隐式持有外部类但是显示持有的会泄漏非静态内部类创建出的静态实例对象持有外部引用Webview泄漏渲染页面产生的堆内存可以单独进程退出杀进程解决IO流、Bitmap等未释放、销毁FD句柄未释放native内存泄漏全局Manager持有匿名监听器可以提供反注册解决匿名内部类/lambda/Kotlin高阶函数 持有外部类并且执行耗时任务时可能存在内存泄漏的风险Java 内存划分堆、程序计数器、方法区、本地方法栈、虚拟机栈其中方法区和堆是线程共享的各内存划分简介方法区线程共享存储类信息、静态变量、常量、即时编译出来的代码数据可造成OOM堆线程共享存放几乎所有的对象实例GC的主要区域程序计数器一块较小的内存空间线程私有存储当前线程执行的字节码行号指示器字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令分支、循环、跳转等每个线程都有一个独立的程序计数器唯一一个在java虚拟机中不会OOM的区域本地方法栈为虚拟机中Native方法服务对本地方法栈中使用的语言、数据结构、使用方式没有强制规定虚 拟机可自有实现占用的内存区大小是不固定的可根据需要动态扩展虚拟机栈线程私有区域每个java方法在执行的时候会创建一个栈帧用于存储局部变量表、操作数栈、动态 链接、方法出口等信息。方法从执行开始到结束过程就是栈帧在虚拟机栈中入栈出栈过程局部变量表存放编译期可知的基本数据类型、对象引用、returnAddress类型。所需的内存空间会 在编译期间完成分配进入一个方法时在帧中局部变量表的空间是完全确定的不需要运行时改变若线程申请的栈深度大于虚拟机允许的最大深度会抛出SatckOverFlowError错误虚拟机动态扩展时若无法申请到足够内存会抛出OutOfMemoryError错误对象存活判断算法引用计数法给对象添加引用计数器每当一个地方引用时计数器加1引用失效时计数器减1当引用计数 器为0时即为对象不可用实现简单效率高但是无法解决相互引用问题主流虚拟机一般不使用此方法判断对象是否存活可达性分析法以GC Roots作为起点向下搜索搜索走过的路径称为引用链当一个对象到 GC Roots没有任何引用链时即为对象不可用可被回收的可被称为GC Roots的对象虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中 常量引用的对象、本地方法栈中引用的对象GC Root对象Class-由系统ClassLoader加载的对象Thread-活着的线程Stack Local-Java方法的local变量或参数JNI Local - JNI方法的local变量或参数JNI Global - 全局JNI引用Monitor Used - 用于同步的监控对象垃圾回收算法标记清除算法首先标记出需要回收的对象在标记完成后统一回收所有标记的对象缺点标记和清除的过程效率低会产生很多不连续的内存碎片申请大内存时容易失败而触发GC标记整理算法标记整理算法标记过程和标记清除算法一样但清除过程并不是对可回收对象直接清理而是将所有存 活对象像一端移动然后集中清理到端边界以外的内存。复制算法将可用内存按空间分为大小相同的两小块每次只使用其中的一块等这块内存使用完了将还存活的对 象复制到另一块内存上然后将这块内存区域对象整体清除掉。每次对整个半区进行内存回收不会导 致碎片问题实现简单高效。缺点内存可用空间减半分代收集算法根据对象存活周期的不同将内存划分为新生代和老年代根据其特点采用最合适的算法新生代存活对象较少每次垃圾回收都有大量对象死去一般采用复制算法只需要付出复制少量 存活对象的成本就可以实现垃圾回收老年代存活对象较多没有额外空间进行分配担保就必须采用标记清除算法和标记整理算法进行 回收Android Studio ProfilerAS自带的Profiler也包括内存分析根据业务场景当发现有明显的内存波动时导出Hprof文件用MAT进行分析;可以根据实际业务查看内存的分配找到相关的地方各项指标实时获取线程数量合理的线程使用可提高应用程序的运行效率过度使用反而会增加CPU及内存的负担。为避免这一情况的发生可结合进程状态及当前的线程列表进行分析当前状态读取进程状态 /proc/pid/status并解释Threads字段具体分析调用Thread.getAllStackTraces() 获取当前所有线程的信息包括线程名、调用栈及状态等adb获取内存信息adb shell dumpsys meminfopackage_name字段含义备注Shallow SizeShallow Size是指实例自身占用的内存, 可以理解为保存该’数据结构’需要多少内存, 注意不包括它引用的其他实例Retained Size实例A的Retained Size是指, 当实例A被回收时, 可以同时被回收的实例的Shallow Size之和缩写VSSVirtual Set Size虚拟耗用内存包含共享库占用的内存RSSResident Set Size实际使用物理内存包含共享库占用的内存PSSProportional set size实际使用的物理内存(比例分配共享库占用的内存USSUnique Set Size进程独自占用的物理内存不包含共享库占用的内存其他VSSRSSPSSUSS当前系统的内存信息ActivityManageractivityManager(ActivityManager)getSystemService(ACTIVITY_SERVICE);ActivityManager.MemoryInfomemoryInfonewActivityManager.MemoryInfo();activityManager.getMemoryInfo(memoryInfo);Log.i(TAG,可用内存:memoryInfo.availMem);Log.i(TAG,总内存:memoryInfo.totalMem);Log.i(TAG,是否内存低:memoryInfo.lowMemory);查看JNI 引用数量privatevoiddumpJNIRef(){try{Class.forName(dalvik.system.VMDebug).getMethod(dumpReferenceTables).invoke(null);}catch(Exceptione){}}查看日志有JNI引用表global reference table dump:Last10entries(of611):610:0x12c9dca0java.lang.ref.WeakReference(referent is aandroid.view.inputmethod.InputMethodManager$ImeInputEventSender)609:0x12c9dc80android.hardware.input.InputManager$InputDevicesChangedListener608:0x12c9dc60android.view.ViewRootImpl$AccessibilityInteractionConnection607:0x12c9dc48android.os.Binder606:0x12c9dc30java.lang.ref.WeakReference(referent is aandroid.view.ViewRootImpl$WindowInputEventReceiver)605:0x12c9dc10android.view.ViewRootImpl$W604:0x12c9dbf0android.view.accessibility.AccessibilityManager$1603:0x12c9dbd0android.view.ViewRootImpl$AccessibilityInteractionConnection602:0x12c9dbb8android.os.Binder601:0x12c9dba0java.lang.ref.WeakReference(referent is aandroid.view.ViewRootImpl$WindowInputEventReceiver)Summary:319ofjava.lang.Class(243unique instances)253ofjava.nio.DirectByteBuffer(253unique instances)4ofjava.lang.ref.WeakReference(4unique instances)3ofandroid.opengl.EGLContext(2unique instances)3ofandroid.opengl.EGLDisplay(2unique instances)3ofandroid.opengl.EGLSurface(2unique instances)2ofdalvik.system.PathClassLoader(1unique instances)2ofjava.lang.String(2unique instances)2ofjava.lang.ThreadGroup(2unique instances)2ofandroid.os.Binder(2unique instances)2ofandroid.app.LoadedApk$ReceiverDispatcher$InnerReceiver(2unique instances)2ofandroid.view.ViewRootImpl$AccessibilityInteractionConnection(2unique instances)2ofandroid.view.ViewRootImpl$W(2unique instances)2ofandroid.view.accessibility.AccessibilityManager$1(2unique instances)1ofjava.nio.ByteOrder1ofdalvik.system.VMRuntime1ofandroid.app.ActivityThread虚拟内存获取状态: 通过读取/proc/pid/status中的VmSize字段具体分析: 读取/proc/pid/smaps分析mapping及各个内存大小相关的字段大小限制: 4GB32位或512GB64位Java堆获取更大对空间AndroidManifest 配置application.largeHeap版本差异8.0之前图片缓存容易消耗大量堆空间8.0及之后像素数据被修改到了native层大小限制 Runtime.getRuntime().maxMemory()当前使用 Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(具体分析 Dubug.dumpHprofData(String fileName)FD数量Linux把一切设备都视作文件File Descriptor文件描述符为设备相关的编程提供了统一的方法。当我们执行IO、Socket及线程等相关操作时都存在与之相对应的FD。进程的FD信息可通过读取/proc下的虚拟文件来获取- - 大小限制读取进程状态/proc/pid/limits并解释Max open files字段当前状态读取进程文件/proc/pid/fd计算文件数量具体分析遍历进程文件/proc/pid/fd并通过Os.readlink解释文件链接当前设备限制的各种内存信息adb shellgetprop | grep dalvik[dalvik.vm.appimageformat]:lz4[dalvik.vm.dex2oat-Xms]:64m[dalvik.vm.dex2oat-Xmx]:512m[dalvik.vm.dex2oat-max-image-block-size]:524288[dalvik.vm.dex2oat-minidebuginfo]:true[dalvik.vm.dex2oat-resolve-startup-strings]:true[dalvik.vm.dex2oat-updatable-bcp-packages-file]:/system/etc/updatable-bcp-packages.txt[dalvik.vm.dexopt.secondary]:true[dalvik.vm.heapmaxfree]:8m[dalvik.vm.heapminfree]:512k[dalvik.vm.heapsize]:[512m]# 单个进程可用的最大内存[dalvik.vm.heapstartsize]:[8m]# 堆分配的起始大小[dalvik.vm.heaptargetutilization]:0.75[dalvik.vm.image-dex2oat-Xms]:64m[dalvik.vm.image-dex2oat-Xmx]:64m[dalvik.vm.isa.arm.features]:default[dalvik.vm.isa.arm.variant]:cortex-a9[dalvik.vm.isa.arm64.features]:default[dalvik.vm.isa.arm64.variant]:generic[dalvik.vm.lockprof.threshold]:500[dalvik.vm.minidebuginfo]:true[dalvik.vm.usejit]:true[dalvik.vm.usejitprofiles]:true[persist.debug.dalvik.vm.core_platform_api_policy]:just-warn[persist.sys.dalvik.vm.lib.2]:libart.so[ro.dalvik.vm.native.bridge]:0Native内存通常说的Native内存是相对于Java堆而言的Java堆区的内存有虚拟机代为申请和释放Java层的业务代码无需关心。Native内存主要说的是由业务动态申请的内存一般是业务so库业务代码是c/c实现的常用的方式就是调用 malloc函数申请内存调用free释放内存。这些内存的申请都需要合理的释放否则会导致内存不足。可结合Debug.getMemoryInfo()以及/proc/pid/smap文件来分析。当前使用读取nativePss它是本进程内native层独占的内存和与其他进程共享内存的均摊的总和具体分析hook 业务每次malloc和free函数记录每次内存的申请和释放并获取到对应的堆栈最后进行统计分析native内存泄漏的检测监测手段可以使用KOOM和matrix进行监控二者的原理都是通过hook C的内存分配和释放malloc与free函数KOOM使用FP Unwind(frame pointer unwind)进行回溯并不是所有的库都支持matrix集成了unwind库进行堆栈回溯根据堆栈再通过add2line工具进行函数定位KOOM使用和原理字节流动-LeakTracer集成检测// 配置检测 Android native 内存泄漏的工具MemoryHook.INSTANCE//单独配置so的方式 .*libasr\\.so$,.*libgram\\.so$,.*libvad\\.so$// .addHookSo(.*com\\.hjl\\.test.*\\.so$) //全匹配的方式 .*com\\.byd\\.dm.*\\.so$.addHookSo(.*).enableStacktrace(true).stacktraceLogThreshold(0).enableMmapHook(true);add2line 使用路径Android\Sdk\ndk\21.0.6113669\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\bin\aarch64-linux-android-addr2line.exe命令参考aarch64-linux-android-addr2line.exe -e xxx.so 2ba0 -fMAT首先把profiler导出的内存文件转换成hprof文件Android/sdk/platorm-tools路径下,执行hprof-conv 刚刚生成的hprof文件 memory-mat.hprof使用MAT分析上面链接有教程 Android内存优化参考线上方案修改leakcanary在可能泄露的地方监控KOOM 在内存达到一定预警值的时候新开一个进程dump内存信息Matrix 接入根据上述指标自行进行监控与上报常见问题标记回收算法被标记了一定会被回收吗不一定。第一次标记如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链那它将会被第一次标记第二次标记第一次标记后接着会进行一次筛选筛选的条件是此对象是否有必要执行 finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的将被进行第二次标记。第二次标记成功的对象将真的会被回收如果对象在 finalize() 方法中重新与引用链建立了关联关系那么将会逃离本次回收继续存活。问题排查案例Java堆暴增导致OOM由于项目使用消息队列的方式进行处理因为某些消息处理比较耗时导致消息堆积消息携带byte数组最终导致内存暴增Native内存不断增长抓取内存信息后发现是native的内存不断增长进行内存拆解后基本上排除APK内的so库定位到只要不停的建立websocket连接再断开就会增长未释放使用matrix抓取所有库的分配前后对比后发现有两个系统的debug库一直增长推测是测试的系统库有bug反馈系统组