前言
之前学习过一段时间关于 Android 内存泄漏的知识,大致了解了导致内存泄漏的一些原因,但是没有深入去探究,很多细节也理解的不够透彻,基本上处于一种似懂非懂的状态,最近又碰到了相关问题,发现有很多新的收获,遂在此记录一些心得体会
知识储备
手机运行内存(RAM)其实相当于我们的 PC 中的内存,是手机中作为 App 运行过程中
临时性数据暂时存储的内存介质。不过考虑到体积和功耗,手机不使用 PC 的 DDR 内存,
采用的是 LPDDR RAM,全称是“低功耗双倍数据速率内存”,其中 LP 就是“Lower
Power”低功耗的意思。
以 LPDDR4 为例,带宽 = 时钟频率 × 内存总线位数 ÷ 8,即 1600 × 64 ÷ 8 =
12.8GB/s,因为是 DDR 内存是双倍速率,所以最后的带宽是 12.8 × 2 = 25.6GB/s。
内存并不是一个孤立的概念,它跟操作系统、应用生态这些因素都有关。同样是 1GB
内存,使用 Android 9.0 系统会比 Android 4.0 系统流畅,使用更加封闭、规范的 iOS
系统也会比“狂野”的 Android 系统更好。
异常率
内存造成的第一个问题是异常。异常包括OOM、内存分配失败这些崩溃,也包括因为整体内存不足导致应用被杀死、设备重启等问题
UV 崩溃率
UV 崩溃率 = 发生崩溃的 UV / 登录 UV
内存造成的第二个问题是卡顿。Java 内存不足会导致频繁 GC,这个问题在 Dalvik 虚拟机
会更加明显。而 ART 虚拟机在内存管理跟回收策略上都做大量优化,内存分配和 GC 效率
相比提升了 5~10 倍。如果想具体测试 GC 的性能,例如暂停挂起时间、总耗时、GC 吞
吐量,我们可以通过发送SIGQUIT 信号获得 ANR 日志。
1.adb shell kill -S QUIT PID
2.adb pull /data/anr/traces.txt
Java 内存分配策略
Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
栈与堆的区别
在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。
堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。
Java是如何管理内存
Java的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。
监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。
什么是Java中的内存泄漏
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
(以上内容引用自开源项目LearningNotes)
Android 中内存泄漏的原因
在 Android 中内存泄漏的原因其实和在 Java 中是一样的,即某个对象已经不需要再用了,但是它却没有被系统所回收,一直在内存中占用着空间,而导致它无法被回收的原因大多是由于它被一个生命周期更长的对象所引用。其实要分析 Android 中的内存泄漏的原因非常简单,只要理解一句话,那就是生命周期较长的对象持有生命周期较短的对象的引用。
内存泄漏主要分两种情况,一种是同一个对象泄漏,还有一种情况更加糟糕,就是每次都
会泄漏新的对象,可能会出现几百上千个无用的对象。
很多内存泄漏都是框架设计不合理所导致,例如单例造成的内存泄漏,MVC 中 Controller
的生命周期远远大于 View等。
Bitmap 优化
Bitmap 内存一般占应用总内存很大一部分,所以做内存优化永远无法避开图片内存这
个“永恒主题”。
在 Android 3.0 之前,Bitmap 对象放在 Java 堆,而像素数据是放在 Native 内存中。
如果不手动调用 recycle,Bitmap Native 内存的回收完全依赖 finalize 函数回调,熟
悉 Java 的同学应该知道,这个时机不太可控。
Android 3.0~Android 7.0 将 Bitmap 对象和像素数据统一放到 Java 堆中,这样就算
我们不调用 recycle,Bitmap 内存也会随着对象一起被回收。不过 Bitmap 是内存消耗
的大户,把它的内存放到 Java 堆中似乎不是那么美妙。
Java 堆内存不足导致 OOM。Bitmap 放到 Java 堆的另外一个问题会引起大量的 GC,
对系统内存也没有完全利用起来。
有没有一种实现,可以将 Bitmap 内存放到 Native 中,也可以做到和对象一起快速释
放,同时 GC 的时候也能考虑这些内存防止被滥用?NativeAllocationRegistry 可以一
次满足你这三个要求,Android 8.0 正是使用这个辅助回收 Native 内存的机制,来实现
像素数据放到 Native 内存中。Android 8.0 还新增了硬件位图 Hardware Bitmap,它
可以减少图片内存并提升绘制效率。
low memory killer 的设计
在 Android 上我们有个机制叫 Low Memory Killer,当 Cached Pages 太少时,就会被触发。它的工作方式是根据进程的优先级,选择性地杀死某个进程,释放该进程占用的所有资源以满足内存分配需要:
如果 LMK 杀掉的是用户正在交互或可以感知的进程,将会导致非常不友好的用户体验。所以 Android SystemServer 进程维护了一张进程优先级列表,LMK 根据这张表来决定先杀死哪个进程:
Perceptible 指的是非用户直接交互的进程,比如在后台播放音乐的音乐播放器进程;
Previous 指的是切换至当前前台应用前的应用进程;
Cached 指缓存的进程,这可能是退至后台的应用进程,也可能是已经退出的应用进程,目的是为了实现应用间的快速切换。所以,Cached 进程也是优先级最低的进程。
性能优化的建议
避免创建过多的对象
不要过多使用枚举,枚举占用的内存空间比整型大
常量用 static final 来修饰
使用一些Android特有的数据结构 ,如 sparearray和 pair 它们拥有更好的性能
适当使用软引用和弱引用
采用内存缓存和磁盘缓存
尽量采用静态内部类 这样可以避免潜在的由于内部类而导致的内存泄漏
...