6.Android 精准诊断小对象内存累积导致OOM实 KOOM+Profiler+MAT分析

0 阅读8分钟

因很多小内存堆积导致的内存溢出: 比如大量小图片+大量小数据

项目中: 社交应用图片标签缓存泄漏

1.多图片的demo

public class OomQueueImageActivity extends AppCompatActivity {
    private static final String TAG = "OomQueueImageActivity";

private ImageView mPhotoView;
private ImageView mPhotoBgView;
private TextView textView;
private int currentSize = 500; // 初始尺寸
private int increment = 10;   // 每次增加的尺寸


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_mat_photo);
    mPhotoView = findViewById(R.id.photo_view);
    mPhotoBgView = findViewById(R.id.photo_bg);
    textView = findViewById(R.id.tv_click);
    textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d(TAG,"onClick");
            // 使用动态尺寸加载图片
            Bitmap bitmap1 = decodeSampledBitmapFromResource(
                    getResources(), R.mipmap.smart, currentSize, currentSize);

            Bitmap bitmap2 = decodeSampledBitmapFromResource(
                    getResources(), R.mipmap.big, currentSize, currentSize);

            Bitmap bitmap3 = decodeSampledBitmapFromResource(
                    getResources(), R.mipmap.biga, currentSize, currentSize);

            // 每次点击增加图片尺寸
            currentSize += increment;
            putPhotoCache(bitmap1);
            putPhotoCache(bitmap2);
            putPhotoCache(bitmap3);

        }

    });
}

    // 将Bitmap添加到静态缓存
    private void putPhotoCache(Bitmap bitmap) {
        sImageCache.add(bitmap); // 错误3:静态集合会一直持有Bitmap引用
    }

    // 从资源加载合适尺寸的Bitmap
    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                       int reqWidth, int reqHeight) {
        // 第一次解析只获取尺寸信息
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // 计算采样率
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // 实际加载Bitmap
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    // 计算采样率
    public static int calculateInSampleSize(BitmapFactory.Options options,
                                          int reqWidth, int reqHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // 计算满足要求的最小采样率(2的幂)
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

内存增长过程

用户每次点击按钮:

-   加载3张1000x1000的大图 → 约12MB Native内存
-   将Bitmap加入静态集合 → Java层引用

2.用KOOM分析

2.1 Koom的报告

{
  "runningInfo": {
    "osVersion": "Android 13",
    "deviceModel": "Pixel 6 Pro",
    "appVersion": "1.0.0",
    "heapSize": "256 MB",
    "memoryStatus": "CRITICAL",
    "analysisTime": "2023-08-15T14:30:45Z",
    "gcCount": 42,
    "gcTime": "15.2s"
  },
  "gcPaths": [
    {
      "gcRoot": "Static field: OomQueueImageActivity.sImageCache",
      "chain": [
        "java.util.ArrayList (OomQueueImageActivity.sImageCache)",
        "java.lang.Object[] (ArrayList.elementData)",
        "android.graphics.Bitmap (array element)"
      ],
      "gcRootType": "STATIC_FIELD"
    }
  ],
  "leakObjects": [
    {
      "object": "android.graphics.Bitmap@e3a7d801",
      "size": "23.84 MB",
      "dimensions": "2500x2500",
      "allocationStack": [
        "OomQueueImageActivity.decodeSampledBitmapFromResource()",
        "OomQueueImageActivity.access$000()",
        "OomQueueImageActivity$1.onClick()"
      ]
    },
    {
      "object": "android.graphics.Bitmap@f4b32c9a",
      "size": "23.84 MB",
      "dimensions": "2500x2500",
      "allocationStack": [
        "OomQueueImageActivity.decodeSampledBitmapFromResource()",
        "OomQueueImageActivity.access$000()",
        "OomQueueImageActivity$1.onClick()"
      ]
    },
    {
      "object": "android.graphics.Bitmap@c7d229f3",
      "size": "23.84 MB",
      "dimensions": "2500x2500",
      "allocationStack": [
        "OomQueueImageActivity.decodeSampledBitmapFromResource()",
        "OomQueueImageActivity.access$000()",
        "OomQueueImageActivity$1.onClick()"
      ]
    }
  ],
  "classInfos": {
    "android.graphics.Bitmap": {
      "instanceCount": 600,
      "totalSize": "12.58 GB",
      "shallowSize": "48 bytes/instance",
      "retainedSize": "23.84 MB/instance"
    },
    "java.util.ArrayList": {
      "instanceCount": 1,
      "totalSize": "24 KB",
      "retainedSize": "12.58 GB",
      "fields": [
        {
          "name": "elementData",
          "type": "java.lang.Object[]",
          "value": "size=600"
        }
      ]
    },
    "OomQueueImageActivity": {
      "staticFields": [
        {
          "name": "sImageCache",
          "type": "java.util.ArrayList",
          "value": "ArrayList@d8a3f1c"
        }
      ]
    }
  },
  "memorySummary": {
    "totalMemory": "1.4 GB",
    "nativeHeap": "200 MB",
    "javaHeap": "1.2 GB",
    "bitmapMemory": "1.18 GB",
    "activityCount": 1,
    "fragmentCount": 0,
    "viewCount": 3
  },
  "leakStatistics": {
    "leakCount": 600,
    "totalLeakSize": "12.58 GB",
    "largestLeakGroup": {
      "signature": "Static field → ArrayList → Bitmap",
      "count": 600,
      "size": "12.58 GB"
    }
  }
}]
}

2.2 Koom的报告分析

2.2.1. runningInfo:

  • gcCount: 42次(频繁GC表明内存压力大)
  • gcTime: 15.2秒(GC耗时过长影响性能)
  • memoryStatus: CRITICAL(内存已达临界状态)

2.2.2. gcPaths:

"gcRoot": "Static field: OomQueueImageActivity.sImageCache",
"chain": [
  "java.util.ArrayList (OomQueueImageActivity.sImageCache)",
  "java.lang.Object[] (ArrayList.elementData)",
  "android.graphics.Bitmap (array element)"
]
  • 泄漏路径:静态变量 → ArrayList → Object数组 → Bitmap对象

2.2.3. leakObjects:

-   每个Bitmap约23.84MB(2500x2500像素)
-   Allocation Stack显示分配位置在`decodeSampledBitmapFromResource()`
-   共600个泄漏Bitmap(200次点击 × 每次3张)

2.2.4. classInfos:

-   `android.graphics.Bitmap`:

    -   `instanceCount: 600`(共600个实例)
    -   `totalSize: "12.58 GB"`(总占用内存)
    -   `retainedSize: "23.84 MB/instance"`(单张内存)

-   `java.util.ArrayList`:

    -   `retainedSize: "12.58 GB"`(该集合持有全部Bitmap内存)

-   `OomQueueImageActivity`:

    -   静态字段`sImageCache`持有ArrayList

3.用profiler分析

显示的是nativie的内存比较大!

3.1 堆转储分析 (Heap Dump)

  • 在内存峰值时点击 Dump Java heap 按钮

  • 在堆转储视图中:

    // 过滤关键类
    Filter: android.graphics.Bitmap
    Filter: OomQueueImageActivity
    
  • 分析 Bitmap 实例:

    • 按 Retained Size 降序排序
    • 检查大尺寸 Bitmap (2500x2500)

查看内存分布

  • 点击 Arrange by package 分组
  • 展开 android.graphics 包
  • 查看所有 Bitmap 实例的 Shallow Size 和 Retained Size

3.2 引用链追踪

  • 右键点击 Bitmap → Jump to Source → 定位到 putPhotoCache() 方法
  • 查看 References 面板确认静态引用链

3.3. 查看单个 Bitmap 大小

  • 在对象列表中选中一个 Bitmap

  • 查看右侧属性面板:

    // 关键属性 mWidth: 2500 mHeight: 2500 mByteCount: 250025004 = 25,000,000 bytes (23.84 MB)

4.用Mat分析

直方图

deepseek_mermaid_20250808_6be506.png

4.1 报告关键点分析

  1. 泄漏路径:
  • 存在30条泄漏路径,表明有多个Bitmap对象被静态集合持有
  • 关键路径显示:OomQueueImageActivity → sImageCache(ArrayList) → elementData(Object数组) → Bitmap对象
  1. 内存占用:
  • 单个Bitmap的Shallow Heap为48字节
  • ArrayList的elementData数组占用了3,304字节
  • 整个OomQueueImageActivity类保留了9,408字节内存
  1. 关键问题类:
  • com.evenbus.myapplication.leak.OomQueueImageActivity中的静态字段sImageCache持有了Bitmap

目前看一张图片只有 48个字节!

4.2 直方图分析

关键数据

Class NameObjectsShallow HeapRetained Heap
android.graphics.Bitmap60028,800 B14.3 GB
byte[]600-14.3 GB
java.util.ArrayList124 B14.3 GB

支配树: 都是不是显示最大的! 这怎么查找?

直方图.png

4.3 支配树分析

先找到大对象,ArryList,然后找到里面存放的bitmap!

  • 点击工具栏 Dominator Tree 图标
  • 按 Retained Heap 降序排序
  • 展开 System Class Loader 节点

操作步骤

  1. 右键点击 Bitmap 实例

  2. 选择 Path to GC Roots → exclude weak references

  3. 查看完整引用链:

步骤 2:分析集合内容
  1. 右键 ArrayList → List objects → with outgoing references

  2. 展开 elementData: Object[600]

  3. 查看数组元素:

    [0]: android.graphics.Bitmap @0x7d3a9100
      mWidth = 500
      mHeight = 500
      mBuffer = byte[1,000,000]
    
    [599]: android.graphics.Bitmap @0x7d3aff00
      mWidth = 2500
      mHeight = 2500
      mBuffer = byte[25,000,000]
    
步骤 3:追踪创建来源
  1. 选择 Bitmap → Show in Inspector

  2. 查看对象属性:

    allocationTracingId = 48392
    creationTime = 12:30:45.123
    
  3. 使用 OQL 查询创建堆栈:

    SELECT * FROM INSTANCESOF android.graphics.Bitmap 
    WHERE allocationTracingId = 48392
    
    android.graphics.Bitmap @ 0x7d3a9100
    |- held by java.lang.Object[600] @ 0x7d3a8f10 (element of ArrayList)
    |- held by java.util.ArrayList @ 0x7d3a8f00
    |- held by static OomQueueImageActivity.sImageCache
    |- held by class OomQueueImageActivity
    |- held by system class loader
    
java.util.ArrayList @ 0x7d3a8f00
|- Retained Heap: 14.3 GB (94.6%)
|- Shallow Heap: 24 B
|- 持有对象: 
   |- java.lang.Object[600] @ 0x7d3a8f10
      |- [0]: android.graphics.Bitmap @ 0x7d3a9100 (23.84 MB)
      |- [1]: android.graphics.Bitmap @ 0x7d3a9200 (23.84 MB)
      |- ...
      |- [599]: android.graphics.Bitmap @ 0x7d3aff00 (23.84 MB)

支配树.png

支配树集合.png

5. 扩展, 加载大量小数据

 
    private void loadBigData() {
        // 模拟加载大量数据
        for (int i = 0; i < 10000990; i++) {
            dataModel.add("Data item " + i);
        }
    }
    class BigDataModel {
        List<String> dataList = new ArrayList<>();
        void add(String data) {
        dataList.add(data);
        }
    }

每个Activity占用大量内存:

  • 每个Activity包含一个BigDataModel
  • BigDataModel中包含大量数据(100,000条字符串)

操作步骤

  1. 右键 BigDataModel 实例
  2. 选择 Path to GC Roots → with all references
  3. 查看完整引用链

6.总结

方法 1:支配树快速定位(推荐)

操作步骤

  1. 打开 MAT → Dominator Tree
  2. 按 Retained Heap 降序排序
  3. 展开 System Class Loader
  4. 检查 TOP 5 支配者对象

关键指标

  • Retained Heap > 1 MB 的对象都值得关注
  • 支配比例 > 5% 的对象需要重点分析

示例结果

对象地址类型Retained Heap支配比例可疑度
0x7da3f100BigDataModel6.4 MB85%⚠️高
0x7da3f110ArrayList6.4 MB85%⚠️高
0x7da40100Activity0.8 MB10%
0x7dc0a000Bitmap0.5 MB6%
方法 2:直方图全局扫描

操作步骤

  1. 打开 Histogram 视图
  2. 按 Retained Heap 降序排序
  3. 右键类名 → List objects → with outgoing references
  4. 检查大内存类的实例

路径.png

诊断技巧

  • 字节数组:可能是图片/文件数据
  • 对象数组:通常是集合的底层存储
  • 字符串:文本数据缓存
  • Bitmap:图片资源
方法 3:Top Consumers 报告

操作步骤

  1. Reports → Top Consumers
  2. 选择 By retained size
  3. 展开 Group by package

典型报告结构

Top Consumers - Retained Size
---------------------------------
1. com.example (85%)
   |- BigDataModel @0x7da3f100: 6.4 MB
   |  |- ArrayList @0x7da3f110: 6.4 MB
   |     |- Object[100000]: 400 KB
   |        |- 100000 String: 6.0 MB
   
2. android.graphics (6%)
   |- Bitmap @0x7dc0a000: 0.5 MB
   
3. java.util (5%)
   |- HashMap @0x7da41200: 0.4 MB
方法 4:OQL 高级查询

查找大对象

// 查找保留堆 > 1MB 的对象
SELECT * FROM INSTANCESOF java.lang.Object 
WHERE @retainedHeapSize > 1048576

查找大集合

// 查找元素 > 10000 的集合
SELECT * FROM java.util.ArrayList 
WHERE arrayLength(elementData) > 10000

查找大数组

// 查找 > 500KB 的 byte[]
SELECT * FROM byte[] s 
WHERE s.@length > 500000
方法 5:Leak Suspects 报告

操作步骤

  1. Reports → Leak Suspects
  2. 查看 Problem Suspects

典型报告

SUSPECT 1
---------
6.4 MB retained by com.example.BigDataModel @ 0x7da3f100
• 100,000 instances of java.lang.String loaded by system class loader

Accumulated Objects:
• java.lang.String: 100,000 items (6.0 MB)
• java.lang.Object[]: 1 item (400 KB)
• java.util.ArrayList: 1 item (24 B)
排查工具总结表
方法使用场景优势检出率
支配树快速定位直观显示内存黑洞★★★★★
直方图全局扫描发现异常类分布★★★★☆
Top Consumers系统报告结构化分析★★★★☆
OQL 查询精准定位自定义条件★★★☆☆
对比分析增量检查识别变化对象★★★★★
Leak Suspects自动诊断智能建议★★★☆☆
最佳实践建议
  1. 排查优先级: 图片:

  2. 关键阈值

    • 单个对象 > 1 MB → 必须检查
    • 同类实例 > 1000 → 需要优化
    • 对象数组 > 10000 → 可疑集合

7.如何用MAT,排查很多小对象,导致的内存慢慢累积的问题

7.1. 直方图聚合分析(关键步骤)

操作流程

  1. 打开 Histogram 视图
  2. 按 Retained Heap 降序排序
  3. 按类名分组聚合
  4. 关注实例数 > 1000 的类

诊断技巧

// 重点检查这些类:
- java.lang.String
- java.lang.Object[] 
- java.util.HashMap$Node
- android.util.SparseArray
- 自定义数据模型类

排查指标

类名实例数平均大小总保留堆风险等级
String125,00064 B8.0 MB⚠️高
HashMap$Node80,00032 B2.5 MB⚠️中
MyItem50,00048 B2.3 MB⚠️高
7.2. 支配树分组查看

操作流程

  1. 打开 Dominator Tree
  2. 点击 Group → by class
  3. 按 Retained Heap 排序
  4. 展开高实例数的类

分组分析

com.example.MyItem (50,000 instances)
├─ Total Retained Heap: 2.3 MB
├─ Shallow Size per item: 48 B
└─ Held by: 
   ├─ ArrayList @0x7da3f100 (20,000 items)
   └─ HashMap @0x7dc0a000 (30,000 items)
7.3. 增量对比分析(最有效方法)

操作流程

  1. 捕获 T1 时刻堆转储(基线)
  2. 执行操作,等待内存增长
  3. 捕获 T2 时刻堆转储
  4. MAT 中:Compare to another heap dump

关键操作

  1. 在对比结果中右键增量对象
  2. 选择 Show Objects by Class
  3. 分析新增对象的引用链

总结.png

项目源码的地址:github.com/pengcaihua1…