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

内存优化

基础概念

  • 几种内存问题的区别?

    1. 内存抖动:在短时间内反复地发生内存增长和回收,导致程序卡顿甚至OOM内存溢出
    2. 内存泄漏:一些内存对象无法按预期的释放
    3. 内存溢出:申请分配内存时,超过系统所能提供的
  • OOM的几种类型

    1. java.lang.StackOverflowError 栈溢出:递归、死循环等,因为每次方法调用都会有一个方法栈压入导致OOM
    2. java.lang.OutOfMemoryError:Java heap space:创建了非常多的对象实例/存储了过大的数据导致无法在新建对象
    3. java.lang.OutOfMemoryError:GC overhead limit exceeded:频繁GC,并且回收的内存空间很少,导致死循环(超过98%的时间用来做GC并且回收了不到2%的堆内存)
    4. java.lang.OutOfMemoryError:unable to create new native thread:创建的线程太多了,一般默认一个进程可以创建的线程数为1024个
    5. java.lang.OutOfMemoryError:Direct buffer memory:本地内存满了,一般是NIO中ByteBuffer.allocateDirect()分配的内存,这个内存不属于GC管辖可能不会被及时回收
  • java堆和native堆的区别

    1. java堆主要是Java代码分配的对象
    2. native堆主要是C代码(malloc)分配的内存
    3. 一些Java代码也会造成native堆内存分配,比如bitmap
  • 虚拟内存

    1. 作为内存地址的抽象,提供一个映射到真实的内存地址,使每个进程认为自己拥有连续的内存地址,提高内存使用的效率
    2. 内存包含和隔离:每个进程只能访问到自己的内存地址(避免直接操作物理内存)
    3. 分页与置换:物理内存与虚拟内存都是以页为单位进行管理(一般是4K或8K),以页为单位的管理使内存管理更加高效
  • 内存优化常见思路

    1. 对象复用/享元模式
    2. 减少不必要的内存分配
    3. 监测是否内存泄漏、是否因为生产者消费者模型未及时处理导致内容堆积
    4. 使用合理的数据结构,例如SparseArray、ArrayMap 来替代 HashMap
  • 内存引用的几种类型

    • 强引用:在内存不足时不会被回收。平常用的最多的对象,如新创建的对象。
    • 软引用:在内存不足时会被回收(无强引用)。用于实现内存敏感的高速缓存。
    • 弱引用:只要GC回收器发现了它,就会将之回收(无强引用)。用于Map数据结构中,引用占用内存空间较大的对象。
    • 虚引用:在任何时候都可能被垃圾回收器回收(无强引用)。

常见内存泄漏

  • Handler(持有外部引用)
  • 单例泄漏(持有Activity而不是Application的Context)
  • 匿名线程(持有外部引用)(但是Lambda表达式没有隐式持有外部类,但是显示持有的会泄漏)
  • 非静态内部类创建出的静态实例对象(持有外部引用)
  • Webview泄漏(渲染页面产生的堆内存,可以单独进程,退出杀进程解决)
  • IO流、Bitmap等未释放、销毁(FD句柄未释放)
  • native内存泄漏
  • 全局Manager持有匿名监听器(可以提供反注册解决)
  • 匿名内部类/lambda/Kotlin高阶函数 持有外部类并且执行耗时任务时,可能存在内存泄漏的风险

Java 内存划分

堆、程序计数器、方法区、本地方法栈、虚拟机栈,其中方法区和堆是线程共享的

  • 各内存划分简介
  1. 方法区:线程共享,存储类信息、静态变量、常量、即时编译出来的代码数据,可造成OOM

  2. 堆:线程共享,存放几乎所有的对象实例,GC的主要区域

  3. 程序计数器

    • 一块较小的内存空间,线程私有,存储当前线程执行的字节码行号指示器
    • 字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令:分支、循环、跳转等
    • 每个线程都有一个独立的程序计数器
    • 唯一一个在java虚拟机中不会OOM的区域
  4. 本地方法栈

    • 为虚拟机中Native方法服务,对本地方法栈中使用的语言、数据结构、使用方式没有强制规定,虚 拟机可自有实现
    • 占用的内存区大小是不固定的,可根据需要动态扩展
  5. 虚拟机栈

    • 线程私有区域,每个java方法在执行的时候会创建一个栈帧用于存储局部变量表、操作数栈、动态 链接、方法出口等信息。方法从执行开始到结束过程就是栈帧在虚拟机栈中入栈出栈过程
    • 局部变量表存放编译期可知的基本数据类型、对象引用、returnAddress类型。所需的内存空间会 在编译期间完成分配,进入一个方法时在帧中局部变量表的空间是完全确定的,不需要运行时改变
    • 若线程申请的栈深度大于虚拟机允许的最大深度,会抛出SatckOverFlowError错误
    • 虚拟机动态扩展时,若无法申请到足够内存,会抛出OutOfMemoryError错误

对象存活判断算法

  1. 引用计数法
  • 给对象添加引用计数器,每当一个地方引用时,计数器加1,引用失效时计数器减1;当引用计数 器为0时即为对象不可用
  • 实现简单,效率高,但是无法解决相互引用问题,主流虚拟机一般不使用此方法判断对象是否存活
  1. 可达性分析法
  • 以GC Roots作为起点,向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链时即为对象不可用,可被回收的
  • 可被称为GC Roots的对象:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中 常量引用的对象、本地方法栈中引用的对象
  1. GC Root对象
  • Class-由系统ClassLoader加载的对象
  • Thread-活着的线程
  • Stack Local-Java方法的local变量或参数
  • JNI Local - JNI方法的local变量或参数
  • JNI Global - 全局JNI引用
  • Monitor Used - 用于同步的监控对象

垃圾回收算法

  • 标记清除算法
  • 首先标记出需要回收的对象,在标记完成后统一回收所有标记的对象
  • 缺点:
    1. 标记和清除的过程效率低
    2. 会产生很多不连续的内存碎片,申请大内存时容易失败而触发GC
  • 标记整理算法
  • 标记整理算法标记过程和标记清除算法一样,但清除过程并不是对可回收对象直接清理,而是将所有存 活对象像一端移动,然后集中清理到端边界以外的内存。
  • 复制算法
  • 将可用内存按空间分为大小相同的两小块,每次只使用其中的一块,等这块内存使用完了将还存活的对 象复制到另一块内存上,然后将这块内存区域对象整体清除掉。每次对整个半区进行内存回收,不会导 致碎片问题,实现简单高效。
  • 缺点:内存可用空间减半
  • 分代收集算法
  • 根据对象存活周期的不同将内存划分为 新生代老年代,根据其特点采用最合适的算法
  • 新生代存活对象较少,每次垃圾回收都有大量对象死去,一般采用复制算法,只需要付出复制少量 存活对象的成本就可以实现垃圾回收
  • 老年代存活对象较多,没有额外空间进行分配担保,就必须采用标记清除算法和标记整理算法进行 回收

Android Studio Profiler

  • AS自带的Profiler也包括内存分析,根据业务场景,当发现有明显的内存波动时,导出Hprof文件,用MAT进行分析;
  • 可以根据实际业务查看内存的分配找到相关的地方

image.png

各项指标实时获取

线程数量

合理的线程使用可提高应用程序的运行效率,过度使用反而会增加CPU及内存的负担。为避免这一情况的发生,可结合进程状态及当前的线程列表进行分析:

  • 当前状态:读取进程状态 /proc/pid/status,并解释Threads字段

  • 具体分析:调用Thread.getAllStackTraces() 获取当前所有线程的信息,包括线程名、调用栈及状态等

    adb获取内存信息

    adb shell dumpsys meminfo <package_name>
    
    字段 含义 备注
    Shallow Size Shallow Size是指实例自身占用的内存, 可以理解为保存该’数据结构’需要多少内存, 注意不包括它引用的其他实例
    Retained Size 实例A的Retained Size是指, 当实例A被回收时, 可以同时被回收的实例的Shallow Size之和
    缩写
    VSS Virtual Set Size 虚拟耗用内存(包含共享库占用的内存
    RSS Resident Set Size 实际使用物理内存(包含共享库占用的内存
    PSS Proportional set size 实际使用的物理内存(比例分配共享库占用的内存
    USS Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
    其他 VSS>=RSS>=PSS>=USS

当前系统的内存信息

ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);

Log.i(TAG,"可用内存:" + memoryInfo.availMem);
Log.i(TAG,"总内存:" + memoryInfo.totalMem);
Log.i(TAG,"是否内存低:" + memoryInfo.lowMemory);

查看JNI 引用数量

private void dumpJNIRef(){
    
    
    try {
    
    
        Class.forName("dalvik.system.VMDebug")
                .getMethod("dumpReferenceTables").invoke(null);
    } catch (Exception e) {
    
    

    }
}

查看日志有JNI引用表

global reference table dump:
      Last 10 entries (of 611):
          610: 0x12c9dca0 java.lang.ref.WeakReference (referent is a android.view.inputmethod.InputMethodManager$ImeInputEventSender)
          609: 0x12c9dc80 android.hardware.input.InputManager$InputDevicesChangedListener
          608: 0x12c9dc60 android.view.ViewRootImpl$AccessibilityInteractionConnection
          607: 0x12c9dc48 android.os.Binder
          606: 0x12c9dc30 java.lang.ref.WeakReference (referent is a android.view.ViewRootImpl$WindowInputEventReceiver)
          605: 0x12c9dc10 android.view.ViewRootImpl$W
          604: 0x12c9dbf0 android.view.accessibility.AccessibilityManager$1
          603: 0x12c9dbd0 android.view.ViewRootImpl$AccessibilityInteractionConnection
          602: 0x12c9dbb8 android.os.Binder
          601: 0x12c9dba0 java.lang.ref.WeakReference (referent is a android.view.ViewRootImpl$WindowInputEventReceiver)
      Summary:
          319 of java.lang.Class (243 unique instances)
          253 of java.nio.DirectByteBuffer (253 unique instances)
            4 of java.lang.ref.WeakReference (4 unique instances)
            3 of android.opengl.EGLContext (2 unique instances)
            3 of android.opengl.EGLDisplay (2 unique instances)
            3 of android.opengl.EGLSurface (2 unique instances)
            2 of dalvik.system.PathClassLoader (1 unique instances)
            2 of java.lang.String (2 unique instances)
            2 of java.lang.ThreadGroup (2 unique instances)
            2 of android.os.Binder (2 unique instances)
            2 of android.app.LoadedApk$ReceiverDispatcher$InnerReceiver (2 unique instances)
            2 of android.view.ViewRootImpl$AccessibilityInteractionConnection (2 unique instances)
            2 of android.view.ViewRootImpl$W (2 unique instances)
            2 of android.view.accessibility.AccessibilityManager$1 (2 unique instances)
            1 of java.nio.ByteOrder
            1 of dalvik.system.VMRuntime
            1 of android.app.ActivityThread

虚拟内存

  • 获取状态: 通过读取 /proc/pid/status 中的 VmSize 字段

  • 具体分析: 读取 /proc/pid/smaps 分析mapping及各个内存大小相关的字段

  • 大小限制: 4GB(32位)或512GB(64位)

    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 shell "getprop | 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]: 0

Native内存

通常说的Native内存是相对于Java堆而言的,Java堆区的内存有虚拟机代为申请和释放,Java层的业务代码无需关心。Native内存主要说的是由业务动态申请的内存,一般是业务so库,业务代码是c/c++实现的,常用的方式就是调用 malloc函数申请内存,调用free释放内存。这些内存的申请都需要合理的释放,否则会导致内存不足。可结合Debug.getMemoryInfo()以及/proc/pid/smap文件来分析。

  • 当前使用:读取nativePss,它是本进程内native层独占的内存和与其他进程共享内存的均摊的总和
  • 具体分析:hook 业务每次malloc和free函数,记录每次内存的申请和释放,并获取到对应的堆栈,最后进行统计分析

native内存泄漏的检测

  • 监测手段
  1. 可以使用KOOM和matrix进行监控
  2. 二者的原理都是通过hook C的内存分配和释放(malloc与free函数)
  3. KOOM使用FP Unwind(frame pointer unwind)进行回溯(并不是所有的库都支持),matrix集成了unwind库进行堆栈回溯
  4. 根据堆栈,再通过add2line工具进行函数定位
  5. KOOM使用和原理
  6. 字节流动-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 使用

    1. 路径:Android\Sdk\ndk\21.0.6113669\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\bin\aarch64-linux-android-addr2line.exe
    2. 命令参考:aarch64-linux-android-addr2line.exe -e xxx.so 2ba0 -f

MAT

  • 首先把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,反馈系统组

猜你喜欢

转载自blog.csdn.net/weixin_41802023/article/details/132543285