Android-内存优化

JVM

程序计数器:记录字节码的地址。

:FILA(先进后出)存储当前线程(线程独享)运行方法需要的数据局部变量表(基本数据类型、对象的引用)、操作数栈、返回地址等。

方法区:存放类信息(ClassLoader加载的类)、常量、静态变量。

堆区:创建的实例对象与数组。(-Xmx堆的分配上限 -Xms堆的初始容量)

对象与垃圾回收机制

判断对象是否存活?

  1. 引用计数算法(Reference Counting)通过为每个对象维护一个整型计数器,实时追踪其被引用的次数。具体规则如下:

    • 计数器增减:当对象被引用时,计数器 +1;引用失效时,计数器 -1
    • 回收条件:当计数器归零时,表示对象不再被使用,立即回收其内存空间
    • 算法缺点: 循环引用问题,若多个对象互相引用,计数器无法归零,导致内存泄漏可使用弱引用解决。

    (弱引用(Weak Reference)是一种 不增加对象引用计数不阻止垃圾回收 的引用机制。当对象仅被弱引用关联时,若没有其他强引用存在,该对象会被垃圾回收器立即回收,在对象互相强引用的场景(如父类与子类双向依赖),将其中一方改为弱引用,可避免内存泄漏)

  2. 可达性分析(GC roots ):可达性分析(Reachability Analysis)是 JVM 垃圾回收的核心算法,通过 追踪对象引用链 判断对象是否存活。

  • GC Roots 起点:以特定根对象(如线程栈变量、静态字段、JNI 引用等)为起点,构建对象引用链。
  • 不可达判定:若对象无法通过任何引用链与 GC Roots 关联,则标记为可回收对象。
  • 误判风险finalize() 方法可能使不可达对象“复活”,但 JVM 仅允许一次复活机会

GC Roots 的类型

类别 示例
虚拟机栈局部变量 线程方法中的参数、局部变量、临时变量
方法区静态属性 类的静态变量(如 public static Object obj
方法区常量 字符串常量池(String Table)中的引用
本地方法栈 JNI 引用 Native 方法引用的 Java 对象

可达性分析与引用计数法的区别

算法 优势 缺陷 适用场景
引用计数法 实现简单、实时性高 无法解决循环引用问题 非主流 JVM 实现
可达性分析 规避循环引用、精确判断对象存活状态 需暂停用户线程(STW)进行扫描 现代 JVM(HotSpot 等)

Java引用方式

引用类型 回收条件 典型场景 实现类/接口
强引用 除非显式置为 null,否则永不回收 核心业务对象(如数据库连接) 默认赋值(Object obj = new Object()
软引用 内存不足时回收 内存敏感型缓存(如图片缓存) SoftReference<T>
弱引用 GC 运行时立即回收(无强引用关联) 监听器、临时元数据存储 WeakReference<T>
虚引用 对象回收后触发通知 资源释放跟踪(如堆外内存管理) PhantomReference<T>

弱引用的使用

Object target = new Object();  
WeakReference<Object> weakRef = new WeakReference<>(target);  
//对象一旦失去强引用,GC 运行时弱引用指向的对象会被立即回
if (weakRef.get() != null) {  
    System.out.println("对象存活");  
} else {  
    System.out.println("对象已被回收");  
}  

引用队列(ReferenceQueue)

ReferenceQueue<Object> queue = new ReferenceQueue<>();  
WeakReference<Object> trackedRef = new WeakReference<>(new Object(), queue);  

// 异步处理回收事件  
new Thread(() -> {  
    try {  
        Reference<?> ref = queue.remove();  
        System.out.println("对象已回收,执行清理操作");  
    } catch (InterruptedException e) {  
        Thread.currentThread().interrupt();  
    }  
}).start();  

弱引用缓存(WeakHashMap)

Map<Object, String> cache = new WeakHashMap<>();  
Object key = new Object();  
cache.put(key, "临时数据");  

key = null;  // 移除强引用  
System.gc();  
System.out.println(cache.size());  // 输出可能为 0  

堆的分区:

分区 描述 占比
年轻代 存放新创建的对象,频繁触发 Minor GC(复制算法)回收短生命周期对象 默认占堆总空间的 1/3(可通过 -XX:NewRatio 调整)
老年代 存储长期存活的对象,触发 Full GC(标记-清除/整理算法) 默认占堆总空间的 2/3

年轻代进一步划分为以下子区域:

  1. Eden 区
    • 新创建的对象优先分配在 Eden 区
    • 默认占年轻代 80% 空间(通过 -XX:SurvivorRatio=8 设定)。
  2. Survivor 区(From/To)
    • 存放从 Eden 区或另一 Survivor 区复制过来的存活对象。
    • 两块 Survivor 区交替使用,总占年轻代 20% 空间(每块 10%)。

示例流程

  1. 对象首次分配至 Eden 区;
  2. Minor GC 后存活对象复制到 Survivor From 区;
  3. 再次 Minor GC 时,存活对象从 From 区复制到 To 区(年龄计数器+1);
  4. 对象年龄达到阈值(默认 15)后晋升至老年代。

GC的方式:Minor Gc(回收新生代)、Major Gc(回收老年代) 、Full Gc(回收所有堆以及方法区等)

GC垃圾清理算法

算法类型 代表实现 原理
标记-清除 CMS(已淘汰) 标记存活对象,清除未标记对象(产生内存碎片)
标记-复制 Serial / ParNew 将存活对象复制到新内存区域,清除旧区域(无碎片,牺牲空间)
标记-整理 Parallel Old 标记存活对象后整理内存,消除碎片(耗时较长)
分代收集 G1(主流) 将堆划分为多个 Region,优先回收年轻代(预测停顿时间)
指标 标记-复制 标记-整理 分代收集(G1) ZGC/Shenandoah
最大停顿时间 低(年轻代) 高(老年代) 可控(10-200ms) 极低(亚毫秒级)3
内存占用 50% 额外空间 无额外空间 无额外空间 少量元数据开销4
吞吐量 中(并发处理开销)
适用堆大小 <100GB <4TB <4TB 支持 PB 级3
垃圾回收器根据 算法设计硬件适配性 划分为四代演进:
阶段 代表回收器 核心特性 适用场景
单线程时代 Serial / Serial Old 单线程回收,全程 STW(Stop-The-World)3 低资源环境(嵌入式设备)
多线程时代 Parallel Scavenge 多线程并行回收,以吞吐量优先13 离线计算、大数据批处理
并发时代 CMS / G1 并发标记与部分清理,减少 STW 时间23 Web 服务、实时性要求中等
智能低延迟时代 ZGC / Shenandoah 亚毫秒级 STW,支持 TB 级堆内存,依赖染色指针和读屏障技术12 金融交易、实时数据分析

内存标记名称

USS --物理内存   进程独占内存
PSS --物理内存   USS+按比例包含共享库(常用)
RSS --物理内存   USS+包含共享库
VSS --虚拟内存   RSS+未分配的实际物理内存

命令:

adb shell cat/system/build.prop --查看APP内存分配信息与分配限制

adb shell cat proc/meminfo --设备的物理内存信息

adb shell cat proc/PID/ppm_adj --查看oom_adj(oom_adj[-16,15] 该值越大越容易被 OOM Killer杀进程)

adb shell dumpsys meminfo --查看各个APP内存使用排序情况
adb shell dumpsys meminfo -packageName --查看当前应用的内存使用情况 (主要看Heap Total NativeHeap这几类)

onLowMemory & onTrimMemory

方法 触发时机 参数与粒度 典型应用场景
onLowMemory 系统内存严重不足,后台进程已被终止时触发 无参数,仅全局内存告警 释放所有非核心资源(如大图缓存)12
onTrimMemory 内存紧张但未达到临界值,系统主动通知应用 int level 区分内存压力等级 按等级精细化释放资源(如UI隐藏时释放控件)23

** onTrimMemory 等级与响应策略**

  • 等级分类(关键级别)
    • TRIM_MEMORY_UI_HIDDEN(UI不可见):释放与UI相关的资源(如Activity背景图、控件缓存)。
    • TRIM_MEMORY_MODERATE/RUNNING_CRITICAL(中度/严重内存压力):释放部分缓存数据(如网络请求缓存、临时数组)。
    • TRIM_MEMORY_COMPLETE(应用即将被终止):强制释放所有非必要资源
@Override  
public void onTrimMemory(int level) {  
    if (level >= TRIM_MEMORY_UI_HIDDEN) {  
        // 释放UI资源(如清空ImageView的Bitmap引用)  
        imageView.setImageBitmap(null);  
    }  
    if (level >= TRIM_MEMORY_MODERATE) {  
        // 释放数据缓存(如清空LruCache)  
        memoryCache.evictAll();  
    }  
}  

内存的问题:

1.内存抖动:频繁地创建销毁对象会导致内存抖动,使内存不连续。当有新的对象创建而内存不够时会频繁触发GC,导致卡顿。

2.内存泄漏:创建的对象不再使用但是还是被GC Roots强引用导致无法回收,就会造成内存泄漏,使可用内存减少。

3.内存溢出 :当使用的内存超出JVM分配的最大内存后会导致OOM。(还有一种OOM是线程溢出,当你创建的线程太多也会导致OOM)

Bitmap的优化处理

1.质量压缩

采样率压缩是一种通过降低图像分辨率来减少内存占用的技术,核心逻辑为 间隔像素采样

val options = BitmapFactory.Options().apply {  
    inJustDecodeBounds = true  // 仅读取图片尺寸信息,不加载像素  
    BitmapFactory.decodeResource(resources, R.drawable.image, options)  
}  
val originalWidth = options.outWidth  
val originalHeight = options.outHeight  
//获取采样率
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {  
    val (width, height) = options.run { outWidth to outHeight }  
    var inSampleSize = 1  
    if (height > reqHeight || width > reqWidth) {  
        val halfHeight = height / 2  
        val halfWidth = width / 2  
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {  
            inSampleSize *= 2  
        }  
    }  
    return inSampleSize  
}  
//压缩Bitmap
options.inJustDecodeBounds = false  
options.inSampleSize = calculatedSampleSize  
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image, options)  

矩阵缩放(Matrix)
加载后通过 Bitmap.createScaledBitmap() 二次调整尺寸,适配显示需求

2.修改图片格式

在不强制要求图片像素的情况下,优先使用 Bitmap.Config.RGB_565(每像素2字节)替代默认的 ARGB_8888(每像素4字节),可减少50%内存。

BitmapFactory.Options options = new BitmapFactory.Options();  
options.inPreferredConfig = Bitmap.Config.RGB_565;  
3.Bitmap缓存 (Lru缓存)
// 初始化LRU缓存(内存缓存)
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8  // 分配可用内存的1/8作为缓存池

val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
    override fun sizeOf(key: String, bitmap: Bitmap): Int {
        // 计算Bitmap占用的内存大小(单位:KB)
        return bitmap.allocationByteCount / 1024
    }

    override fun entryRemoved(evicted: Boolean, key: String, 
                              oldValue: Bitmap, newValue: Bitmap?) {
        // 可选:Bitmap被移除时触发回收(适配低端设备)
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            oldValue.recycle()
        }
    }
}

// 加载Bitmap并缓存
fun loadBitmap(resId: Int, imageView: ImageView) {
    val key = resId.toString()
    // 1. 尝试从缓存读取
    val cachedBitmap = memoryCache.get(key)
    if (cachedBitmap != null) {
        imageView.setImageBitmap(cachedBitmap)
        return
    }

    // 2. 缓存未命中,异步解码
    val options = BitmapFactory.Options().apply {
        inPreferredConfig = Bitmap.Config.RGB_565  // 降低内存占用
        inSampleSize = 2  // 缩放宽高为原图的1/2
        // Android 4.4+ 复用Bitmap内存(需尺寸一致)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            inBitmap = memoryCache.snapshot().values.firstOrNull()
        }
    }

    CoroutineScope(Dispatchers.IO).launch {
        val bitmap = BitmapFactory.decodeResource(resources, resId, options)
        bitmap?.let {
            // 3. 加入缓存并更新UI
            memoryCache.put(key, it)
            withContext(Dispatchers.Main) {
                imageView.setImageBitmap(it)
            }
        }
    }
}

LRU的原理

LRU(Least Recently Used) 是一种基于时间局部性原理的缓存淘汰策略,核心规则:
当缓存空间不足时,优先移除最久未被访问的数据

LRU通常通过 哈希表(Hash Table) + 双向链表(Doubly Linked List) 实现高效操作:

  • 哈希表:存储键值对的快速访问(O(1)时间查找)
  • 双向链表:维护数据访问顺序,头部为最近访问,尾部为最久未访问
操作 步骤 时间复杂度
数据访问 1. 通过哈希表找到节点 2. 将节点移到链表头部 O(1)
数据插入 1. 若键已存在:更新值并移到头部 2. 若键不存在:创建节点插入头部,若超容则删除尾部节点 O(1)
数据淘汰 1. 删除链表尾部节点 2. 同步移除哈希表中对应键 O(1)

LinkedHashMap 内部通过 双向链表 维护键值对的顺序,支持两种模式:

  • 插入顺序(默认):按数据添加顺序排列
  • 访问顺序(accessOrder=true):每次访问(get/put)将节点移到链表尾部,尾部为最近使用,头部为最久未使用

LRU淘汰触发条件
重写 removeEldestEntry 方法,当缓存容量超过阈值时,自动移除链表头部节点

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int maxCapacity;  // 最大缓存容量

    public LRUCache(int maxCapacity) {
        // 设置accessOrder=true启用访问顺序模式
        super(maxCapacity, 0.75f, true);
        this.maxCapacity = maxCapacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当当前大小超过容量时触发淘汰
        return size() > maxCapacity;
    }

    // 可选:线程安全封装(2025年推荐使用原子类)
    public synchronized V safeGet(K key) {
        return super.get(key);
    }

    public synchronized V safePut(K key, V value) {
        return super.put(key, value);
    }
}

4.硬件加速

针对 Android 8.0+ 设备,使用 Bitmap.Config.HARDWARE 将 Bitmap 直接存储在显存,减少主内存占用

// 启用硬件加速(显存占用更低)  
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {  
    options.inPreferredConfig = Bitmap.Config.HARDWARE;  
} 
5.Bitmap的内存大小计算与Bitmap的文件位置

Bitmap 内存大小计算公式

Bitmap 内存占用由 像素总量 × 单像素内存 决定,具体公式为:

内存大小=宽度×高度×每像素字节数(Bytes per pixel)

不同配置的每像素内存

Bitmap.Config 每像素字节数 内存计算示例(1080×1920)
ALPHA_8 1字节 1080×1920×1 = 2.07MB
RGB_565 2字节 1080×1920×2 = 4.15MB
ARGB_8888(默认) 4字节 1080×1920×4 = 8.29MB
RGBA_F16(HDR场景) 8字节 1080×1920×8 = 16.58MB

注意:Android 系统会根据资源目录的屏幕密度(dpi)自动缩放图片尺寸,实际加载的宽高可能与文件尺寸不同!

Bitmap 的文件存储位置直接影响 系统是否自动缩放尺寸,进而改变内存占用:

文件位置 缩放逻辑 内存影响示例(原图 1000×1000)
res/drawable-hdpi 高密度设备(如xhdpi)会放大图片(×1.5) 1500×1500 → 内存增加 125%
res/drawable-hdpi 低密度设备(如mdpi)会缩小图片(×0.75) 750×750 → 内存减少 43.75%

资源目录策略

  • 优先使用高密度目录:将图片放在 res/drawable-xxxhdpi,低密度设备自动缩小尺寸,减少内存占用(如原图尺寸 1000×1000,mdpi设备加载时缩小至 250×250)24
  • 避免低密度目录:若图片仅存于 res/drawable-mdpi,高密度设备会放大图片,导致内存浪费。
Glide用了哪些Biymap优化技术
Glide作为Android主流图片加载框架,在Bitmap处理上采用了多项核心技术以降低内存占用并提升性能,具体优化策略如下:

1. 智能尺寸适配

  • 按控件尺寸解码:根据ImageView的实际宽高动态调整解码尺寸,避免加载超大图导致内存浪费(如200x200原图显示在100x100控件时仅解码100x100)

2. 格式优化与解码策略

  • 动态格式选择
    • 默认使用ARGB_8888保证色彩精度,但若检测到图片无透明通道且无Alpha变换操作,可切换至RGB_565格式减少50%内存占用。
    • Android 8.0+启用Bitmap.Config.HARDWARE硬件加速位图,通过减少Java堆内存拷贝降低内存消耗(需注意硬件位图不支持像素级修改)。
  • 采样压缩(Downsampling):根据目标尺寸自动计算inSampleSize,通过二次采样缩小原始图片分辨率

3. 内存缓存与复用机制

  • 四级缓存架构
    • 活动缓存(Active Resources):存储当前显示中的Bitmap,避免频繁GC。
    • 内存缓存(Memory Cache):LRU策略缓存最近使用的Bitmap,快速响应重复请求。
    • Bitmap池(BitmapPool):复用已释放的Bitmap内存,减少分配开销(需通过GlideModule自定义池大小)。
    • 磁盘缓存:存储原始数据及转换后的结果,减少网络请求和解码次数。

动态请求管理

  • 滑动优化:列表滑动时自动暂停请求(pauseRequests()),停止后恢复(resumeRequests()),避免无效加载造成的CPU/内存抖动。
  • 生命周期绑定:通过with()关联Activity/Fragment生命周期,及时释放无用资源,防止内存泄漏。

内存溢出监测工具

1.AndroidStudio ProfIler:检查实时内存变化,可视化的查看内存都懂、内存是否销毁、内存增长不正常等信息,还可以点击Dump Heap生成堆快照。选择可以对象->Path to GC Root 检查引用链等

2.MAT:对于比较负责的内存泄漏情况我们需要使用Eclipse下的插件MAT来查看对象具体的引用。首先需要将Java Heap文件转为hprof文件并导入MAT中。然后Dominator Tree排序选择需要查看的具体类(右键目标类 → Merge Shortest Paths to GC Roots → 勾选 exclude all phantom/weak/soft references,仅保留强引用链)。

outgoing references:被该对象引用的对象

incoming references:引用该对象的对象

3.手机抓取trace文件,使用Perfetto网站打开,可以查看CPU、Memory、Binder调用栈、执行方法时间等信息。

4.LeakCanary

LeakCanary 通过 弱引用(WeakReference)引用队列(ReferenceQueue) 实现内存泄漏检测。当监控对象(如 Activity)调用 onDestroy() 后,LeakCanary 会为其创建一个带引用队列的弱引用,并将唯一标识符(Key)存入 retainedKeys 集合。

  • 回收检查逻辑
    1. 若对象被回收,弱引用会被自动加入引用队列,对应的 Key 从集合中移除。
    2. 若对象未被回收,强制触发 GC 后再次检查,若 Key 仍存在集合中,则判定为内存泄漏。

内存泄漏检测流程

  1. 监控对象注册
    • 通过 RefWatcher.watch() 监控目标对象(如 Activity、Fragment),创建 KeyedWeakReference 并关联引用队列。
  2. 主动触发 GC
    • 在后台线程中强制调用 System.gc(),确保弱引用对象有机会被回收。
  3. 判定泄漏
    • 若引用队列未包含该弱引用,且 retainedKeys 集合仍保留 Key,则确认泄漏。
  4. 堆转储与分析
    • 生成 .hprof 堆转储文件,通过独立进程(如 HeapAnalyzerService)解析文件,使用 Shark 库(或旧版 HAHA)构建泄漏对象到 GC Roots 的引用链。

引用链分析与报告

  • 可达性分析:基于垃圾回收器的可达性算法,若泄漏对象存在到 GC Roots 的强引用链(如静态变量、未解绑的监听器),则判定泄漏路径成立。
  • 报告生成:展示泄漏对象的引用路径,并标记关键引用节点(如静态单例、匿名内部类)。

** 触发条件与优化**

  • 触发时机
    • 默认监控 Activity/Fragment 的 onDestroy() 生命周期。
    • 支持自定义监控任意对象(如 ViewModel、Service)
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容