Android 性能优化:内存优化(理论篇)

ops/2024/11/26 9:52:40/

内存作为App程序运行最重要的资源之一,需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏,重则导致用户应用程序发生 OOM(out of memory)崩溃。喜马直播随着近些年的业务迭代功能不断完善玩法丰富,需要在各种机器资源上保持优秀的流畅性和稳定性,内存优化是必须要重视的环节。

从目前的内存水位和APM建设入手,梳理统计规则和项目真实内存水位,在代码和业务多方面熟悉和归因,搭配工具建设和使用为抓手,最后通过内存专项治理将应用的 OOM优化到合理水位。

限于篇幅和功能性,文章总共分为理论篇实践篇两个部分。

写这篇文章动机,主要是工作中进行内存优化专项,便于将以往琐碎的内存知识和优化思路整合,所以有了这篇文章,感谢前人的经验。谨以此篇文章记录工作思路及基础知识,温故知新。

思维导图

在这里插入图片描述

内存基础知识

Android 最新系统都运行在ART虚拟机上,基于 Linux 内核实现,Linux的内存管理哲学是:Free memory is wasted memory。即内存没有得到充分利用就是在浪费内存。因此 Linux 希望尽可能多的使用内存,较少磁盘 IO 。Android 系统继承了 Linux 的优点,同样是尽最大限度使用原则。

与Linux不同的是 Android 侧重于可能多的缓存进程以提高应用启动和切换速度。即,Android系统会在内存中尽量的长时间的保持应用进程,直到系统分配内存不足时才会去根据进程优先级、内存代销等条件回收进程。这些保留在内存中的进程通常不会影响系统整体的运行速度,反而会在用户再次激活这些进程时,加快进程的启动速度。

在内存管理上,JVM拥有垃圾内存回收的机制,自身会在虚拟机层面自动分配和释放内存,因此不需要像使用C/C++一样在代码中分配和释放某一块内存。Android系统的内存管理原理基础就是JVM,通过new关键字来为对象分配内存,内存的释放由GC来回收。并且Android系统在内存管理上有一个 Generational Heap Memory模型,当内存达到某一个阈值时,系统会根据不同的规则自动释放可以释放的内存。即便有了内存管理机制,但是,如果不合理地使用内存,也会造成一系列的性能问题,比如 内存泄漏、内存抖动、短时间内分配大量的内存对象,接下来详细记录下Android内存管理模式及知识点。

Jvm内存分配模型

JVM 将整个内存划分为了几块:

  1. 方法区:存储类信息、常量、静态变量等。(所有线程共享)
  2. 虚拟机栈:存储局部变量表、操作数栈等。
  3. 本地方法栈:不同于虚拟机栈为 Java 方法服务、它是为 Native 方法服务的。
  4. 堆:内存最大的区域,每一个对象实际分配内存都是在堆上进行分配的,,而在虚拟机栈中分配的只是引用,这些引用会指向堆中真正存储的对象。此外,堆也是垃圾回收器(GC)所主要作用的区域,并且,内存泄漏也都是发生在这个区域。(所有线程共享)
  5. 程序计数器:存储当前线程执行目标方法执行到了第几行。

Android内存分配

Android Runtime有两种虚拟机,Dalvik 和 ART,实际上就是一块匿名共享内存。Android虚拟机仅仅只是把它封装成一个 mSpace由底层C库来管理,并且仍然使用libc提供的函数malloc和free来分配和释放内存

大多数静态数据会被映射到一个共享的进程中。常见的静态数据包括Dalvik Code、app resources、so文件等等。Android通过显示分配共享内存区域(如Ashmem或者Gralloc)来实现动态RAM区域能够在不同进程之间共享的机制。例如,Window Surface在App和Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider和Clients之间共享内存。下面简单总结下我理解的堆结构:

Dalvik

Linear Alloc 、 Zygote Space 和 Alloc Space

ART

(重点汇总下)

  • Zygote Space: 包含由 Zygote 进程预加载并在所有应用程序之间共享的对象。

  • Allocation Space:用于存储应用程序在运行时创建的对象,主要的GC工作区域,为每个进程独自使用。

  • Non-Moving Space:存储不需要移动的对象,如 ART 内部数据结构,和Dalvik中的Linear Alloc类似。

  • Large Object Space :用于存储大对象(通常大于 12KB)使用单独的分配策略和GC机制。

  • Region Space :用于新生代对象的分配和管理,是离散地址的集合,使用更高效的垃圾回收机制。

  • Image Space:包含预编译的系统类和应用程序类,从预生成的映像文件中内存映射而来,在Zygote和其他应用程序进程之间共享,不参与GC。

Android系统的第一个虚拟机由Zygote进程创建并且只有一个Zygote Space。但是当Zygote进程在fork第一个应用程序进程之前,会将已经使用的那部分堆内存划分为一部分,还没有使用的堆内存划分为另一部分,也就是Allocation Space。但无论是应用程序进程,还是Zygote进程,当他们需要分配对象时,都是在各自的Allocation Space堆上进行

单进程内存上限

Android 系统的 Java虚拟机会对单个进程使用的最大内存做限制,该属性值定义在/system/build.prop文件中,厂商一般会根据设备自身内存大小来设定这个值,不同的设备分配给APP的最大可用内存是不相同的。进程启动时,系统会先为APP分配一定的内存空间,当分配的内存快要耗尽时,系统会再次为App 分配更多的内存,但是每个APP都有内存使用上限,一旦进程分配了最大可用内存后,内存依然不足则会直接抛出OOM异常,终止程序的运行。可以通过调用 getMemoryClass() 向系统查询此数值。此方法返回一个整数,表示应用堆的可用兆字节数。

内存垃圾回收

当进程使用内存达到设定的阈值时,就会触发虚拟机的GC机制,虽然新一代的ART对于通过分代垃圾回收和高效的内存分配机制,ART 能够更加高效地使用内存,减少内存碎片和内存泄漏等方面的优化,但是GC的时候,还是会导致 STW (Stop The World),所以为了提升程序性能,有必要进行内存优化。下边列举一下Java的内存回收算法。

标记清除算法

标记清除算法是一种垃圾回收算法,用于管理动态分配的内存。它的主要思想是,遍历整个堆,标记所有被引用的对象,当某个对象不再被程序所引用时,它就可以被认为是“垃圾”,并被回收以便后续的内存分配。 优缺点:自动管理动态分配的内存,但是清除后可能导致大量的内存碎片,降低堆利用率。

复制算法

复制算法是一种将内存分为两个区域的算法,其中一个区域用于存储活动对象,另一个区域用于存储不再使用的对象。 优缺点:运行效率高,但是浪费一半空间,代价较高。

标记整理算法

标记整理算法是标记清除算法和复制算法的结合,其工作原理是先标记出不再使用的对象,再整理内存使得活动对象的内存分配连续,优缺点:解决了标记清除算法导致的内存碎片问题,但是也产生了一些问题,由于进行了两次扫描,增加了时间开销。 相较其他垃圾回收算法,速度较慢,不适合新生代场景,并且标记整理算法的效率也受内存使用情况影响,效率不稳定等问题。

分代收集算法

分代回收算法是一种将内存分为几个代的算法,并对每个代进行不同的回收策略,这里就需要借一张图了,一图胜千言

image.png

新创建的对象 , 放在年轻代内存块中 , 开始时放在 Eden 区域 , 当 Eden 区域存满后 , 会将存活的对象转移到 From 区域 和 To 区域,对象每经过一次 GC 垃圾回收 , 其年龄就会加 1 ; 当年龄到达虚拟机设置的阈值之后 , 就会被放入老年代内存块中 ,持久代内存区域,主要存放着类加载器加载的Class和常量池等对象。

image.png

  • 年轻代内存区域的垃圾回收器 : Minor GC (Serial,ParNew 和 Parallel Scavenge)

  • 老年代内存区域的垃圾回收器 : Major GC (CMS ,Serial Old和 Parallel Old)

  • 整个内存区域的垃圾回收器 : Full GC

持久代内存区域的内存不回收 ;年轻代内存区域与老年代内存区域的垃圾回收机制不同的。

Serial ParNew都是 使用的复制算法,主要在年轻代中收集要回收的内存,但是Serial是单线程串行,而ParNew则是多线程运行。

CMS Concurrent Mark Sweep ,并发标记清除收集器,采用标记清除算法,gc过程,用户线程仅做最短停顿,具体流程如下:

  • 初始标记 : 标记与 GC Roots 有引用链的对象 ; 该操作速度快 , 该步骤需要暂停用户线程。

  • 并发标记 : GC Roots 追踪,从初始标记结果集合中标记出存活对象,不能保证所有的存活对象都被标记 ; 该步骤与应用程序并发执行。

  • 重新标记 : 上一步并发标记 GC 线程与用户程序并发期间的标记有部分变化 , 修正这部分标记信息之后暂停用户线程 开始标记,该暂停操作要比初始标记步骤暂停时间长。

  • 并发清除 : 回收所有 GC Roots 不可达对象

CMS的优点是,并发收集,低停顿。有一个明显缺点就是因为采用了“标记-清除”算法,最后会出现大量碎片,有可能会出现在某一个时刻,当有大对象生成,不得不进行一次Full GC来解决这个问题。为了解决该问题CMS有一个参数-XX:UseCmsCompactAtFullCollection来解决因为空间不足进行Full GC。这个参数默认开启,用于在CMS收集器顶不住要进行Full GC是开启内存碎片合并整理的过程,内存整理过程是无法并发的,因此就会耗时。同时还有一个参数是-XX:CMSFullGCsBeforeCompaction,这个参数是用于执行多次不压缩的GC后,跟着来一次压缩的(默认是0,表示每次进入Full GC都进行碎片整理)。

G1收集器

Garbage-First收集器:并行和并发,分代收集。 G1和其他收集器不同的是,其他收集器收集范围都是整个新生代和老年代,而G1不再是这样。使用G1收集器的时候,Java堆分为多个大小相等的区域(Region),虽然也保留了新生代和老年代的概念,但是不再是物理隔离了,他们都是一部分Region的集合(这些Region不一定是连续的)。G1保留了Eden和Survivor的比例也是8:1:1。

ZGC

JDK11引入的ZGC收集器,在物理和逻辑上已经没有新/老年代的概念了,会分为一个个page,当进行GC操作时候会对page进行压缩,因此没有碎片问题,只能在64位linux上使用。

  • 可以达到10ms以内的停顿要求
  • 支持TB级别的内存
  • 堆内存变大后停顿时间还是在10ms以内

Java 引用类型

既然上边列举了主要的回收算法,这里简单带过一下引用类型,也是内存优化过程中,经常使用到的知识点:

  • 强引用 :强引用是 Java 中最常见的引用类型,当对象具有强引用时,它永远不会被垃圾回收。只有在程序结束或者手动将对象设置为 null 时,才会释放强引用,像常用的 new 方式。
  • 软引用 :当 Java 堆内存不足时,软引用可能会被回收,以腾出内存空间。如果内存充足,则软引用可以继续存在,使用SoftReference创建,gc的时候可能并不会释放软引用持有对象。
  • 弱引用 :在垃圾回收器线程扫描它所管辖的内存区域的过程中,发现具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。
  • 虚引用 : 只能用于跟踪即将对被引用对象进行的收集。虚拟机必须与ReferenceQueue类联合使用。因为它能够充当通知机制。

内存优化的必要性

经过上述基础知识的分析,我们认识到内存在手机侧的宝贵和重要性,安卓系统对每个应用程序都有一定的内存限制,当应用程序的内存超过了上限,就会出现 OOM,造成App的异常退出。

因此,要改善系统的运行效率、改善用户体验、降低系统资源占用、延长电池寿命、降低系统故障的危险。Android通过内存优化,可以减少系统内存使用 提高应用后台运行存活率,让系统更加流畅,秒开率更高,减少系统GC次数,减少页面卡顿率,降低内存泄漏率,从而提高App的整体体验和功耗优化。

内存优化思路及SOP

在这里插入图片描述

数据收集及水位分析

关于性能或功耗方面的优化问题,都要先做到摸清大盘数据,特别是我这种新的公司,新的APM系统的,一定要摸清APM上报原理和逻辑,并且输出自己的文档,好记性不如烂笔头。

查看内存泄露、内存溢出和线程超标等方面的数据后,动手改代码前要确定好优化后的目标水位是多少,定好里程碑,及时同步领导,避免大方向错误。

借助工具排查问题

在线上大盘水位摸清之后,比如我们是基于Koom进行了魔改,就可以将上报的内存泄露问题统一汇总并解决,但是内存的水位偏高和内存抖动问题,就需要借助本地检测工具进行排查。下篇具体汇总下,通过工具查看问题。 比如图片使用方面,可以使用 Hook native bitmap (新版本都是用Native内存,暂不考虑 8.0 之前的版本情况了)检测图片在退出页面后,是否清理完成。

问题优化

针对不同类型问题需要使用不同的方式,比如内存抖动问题,存在一些大对象或者过多小对象的情况,可能存在频繁创建对象等问题。如果存在内存泄露等问题,则需要根据堆栈调用链,排查那些代码存在纰漏。针对问题优化后,做好兜底策略,并后续关注新版本是否有新的上报。

长效治理策略

问题如果解决后,可以将常见的内存泄露问题进行汇总,在排查问题过程中,使用到的工具及操作流程进行整理,汇总成内存优化SOP,可以在组内进行技术分享,提高团队的内存优化意识。

关于长期卡口,可以尝试在Git Hook的办法,针对提交的代码进行监控,如果出现内存泄露的写法阻止代码提交,并展示警告信息。

参考文章

Android性能优化之内存优化 - JsonChao

分代收集算法

深入探索 Android 内存优化(炼狱级别-上)

深入探索 Android 内存优化(炼狱级别-下)


http://www.ppmy.cn/ops/136801.html

相关文章

【手写一个spring】spring源码的简单实现--BeanPostProcessor(实现AOP)以及JDK动态代理/CGLIB动态代理

文章目录 BeanPostProcessor概念BeanPostProcessor接口中的两个方法BeanPostProcessor的实例化与注册BeanPostProcessor的执行时机实现AOP动态代理技术JDK动态代理:CGLIB动态代理: JDK动态代理和CGLIB动态代理的区别JDK动态代理CGLIB动态代理使用场景性能…

一个全面的Vue 3组件通信演示展示

✍️ 记录 一、Vue2 中组件通信方式 props: 实现父子组件、子父组件、甚至兄弟组件通信自定义事件: 可以实现子父组件通信全局事件总线$bus: 可以实现任意组件通信pubsub: 发布订阅模式实现任意组件通信vuex: 集中状态管理容器,实现任意组件通信ref: 父组件获取子…

【C语言】野指针问题详解及防范方法

博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C语言 文章目录 💯前言💯什么是野指针?💯未初始化的指针代码示例问题分析解决方法 💯指针越界访问代码示例问题分析解决方法 💯指向已释放内存的…

简单的爬虫脚本编写

一、数据来源分析 想爬取一个网站的数据,我们首先要进行数据分析。通过浏览器F12开发者工具栏进行抓包,可以分析我们想要的数据来源。 通过关键字搜索,可以找到相对应的数据包 二、爬虫实现 需要用到的模块为:request&#xf…

NVR小程序接入平台EasyNVR多品牌NVR管理工具:高效管理分散视频资源的解决方案

在当今数字化、智能化的时代背景下,视频监控已成为各行各业不可或缺的一部分,从公共安全到企业运维,再到智慧城市建设,视频资源的管理与应用正面临着前所未有的挑战。如何高效整合、管理这些遍布各地的分散视频资源,成…

单机部署kubernetes环境下Overleaf-基于MicroK8s的Overleaf应用部署指南

在本文中,我们将探讨如何使用MicroK8s在本地或云环境中快速部署Overleaf应用。MicroK8s是一个轻量级的Kubernetes发行版,它为开发者提供了一个简单的方式来部署和管理容器化应用。 需要使用Kompose转换Overleaf官方Docker Compose配置得到适用于kuberne…

数据结构——排序算法第一幕(插入排序:直接插入排序、希尔排序 选择排序:直接选择排序,堆排序)超详细!!!!

文章目录 前言一、排序1.1 概念1.2 常见的排序算法 二、插入排序2.1 直接插入排序2.2 希尔排序希尔排序的时间复杂度 三、选择排序3.1 直接选择排序3.2 堆排序 总结 前言 时间很快,转眼间已经到数据结构的排序算法部分啦 今天我们来学习排序算法当中的 插入排序 和 …

docker部署redis,并设置密码

获取官方镜像 redis:7.4.1 准备配置文件 文件 redis.conf requirepass xxx密码请自行设置 准备启动脚本 start_redis.sh 注意: 要把这个脚本和上面的redis.conf放到同一个目录下。 workdir$(cd $(dirname $0); pwd) redis_id"$(docker ps -a| grep red…