5. Android 加载抖音视频导致的内存溢出实战 KOOM+Profiler+MAT

0 阅读10分钟

大内存的场景

  1. 加载大型视频/音频文件
    试图将整个文件(如 1GB 视频)读入 byte[] 或 ByteBuffer
  2. 一次性将整个文件读入内存
  3. 加载超大图片

本质分为2类:大的数组:因为数组内存是连续分配的

主要包含byte 数组,int数组,String 数组

1. 案例:直接加载500MB视频流

微信图片_20250807223011.jpg

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.lang.ref.WeakReference;
import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "VideoStreamDemo";
    private static final int VIDEO_SIZE_MB = 500;
    private static final int VIDEO_SIZE_BYTES = VIDEO_SIZE_MB * 1024 * 1024;
    private static final int CHUNK_SIZE = 2 * 1024 * 1024; // 2MB分块

    private Button btnDirectLoad, btnChunkLoad;
    private TextView tvStatus, tvMemoryInfo, tvResult;
    private ProgressBar progressBar;
    private Handler mainHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 初始化视图
        btnDirectLoad = findViewById(R.id.btnDirectLoad);
        btnChunkLoad = findViewById(R.id.btnChunkLoad);
        tvStatus = findViewById(R.id.tvStatus);
        tvMemoryInfo = findViewById(R.id.tvMemoryInfo);
        tvResult = findViewById(R.id.tvResult);
        progressBar = findViewById(R.id.progressBar);
        mainHandler = new Handler(Looper.getMainLooper());

        // 更新内存信息
        updateMemoryInfo();

        // 直接加载按钮点击事件
        btnDirectLoad.setOnClickListener(v -> {
            tvStatus.setText("尝试直接加载500MB视频流...");
            tvResult.setText("");
            new DirectLoadTask(this).execute();
        });

        // 分块加载按钮点击事件
        btnChunkLoad.setOnClickListener(v -> {
            tvStatus.setText("开始分块加载500MB视频流...");
            tvResult.setText("");
            new ChunkLoadTask(this).execute();
        });
    }

    // 更新内存信息
    @SuppressLint("SetTextI18n")
    private void updateMemoryInfo() {
        ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
        activityManager.getMemoryInfo(memoryInfo);

        // 获取堆内存信息
        long maxMemory = Runtime.getRuntime().maxMemory() / (1024 * 1024);
        long totalMemory = Runtime.getRuntime().totalMemory() / (1024 * 1024);
        long freeMemory = Runtime.getRuntime().freeMemory() / (1024 * 1024);
        long usedMemory = totalMemory - freeMemory;

        String info = String.format(Locale.getDefault(),
                "最大堆内存: %dMB\n总内存: %dMB\n已用内存: %dMB\n可用内存: %dMB\n500MB视频: %s",
                maxMemory, totalMemory, usedMemory, freeMemory,
                freeMemory > VIDEO_SIZE_MB ? "可能" : "不可能");

        tvMemoryInfo.setText("内存信息: " + info);
    }

    // 直接加载任务
    private static class DirectLoadTask extends AsyncTask<Void, Void, String> {
        private final WeakReference<MainActivity> activityRef;

        DirectLoadTask(MainActivity activity) {
            activityRef = new WeakReference<>(activity);
        }

        @Override
        protected void onPreExecute() {
            MainActivity activity = activityRef.get();
            if (activity != null) {
                activity.progressBar.setVisibility(View.VISIBLE);
                activity.progressBar.setIndeterminate(true);
                activity.btnDirectLoad.setEnabled(false);
                activity.btnChunkLoad.setEnabled(false);
            }
        }

        @Override
        protected String doInBackground(Void... voids) {
            MainActivity activity = activityRef.get();
            if (activity == null) return "Activity not available";

            try {
                Log.d(TAG, "尝试分配500MB内存...");
                
                // 尝试分配500MB内存
                byte[] videoData = new byte[VIDEO_SIZE_BYTES];
                
                // 模拟填充视频数据
                for (int i = 0; i < VIDEO_SIZE_BYTES; i += 1024 * 1024) {
                    int blockSize = Math.min(1024 * 1024, VIDEO_SIZE_BYTES - i);
                    for (int j = 0; j < blockSize; j++) {
                        videoData[i + j] = (byte) (j % 256);
                    }
                }
                
                // 模拟处理视频数据
                int checksum = 0;
                for (int i = 0; i < VIDEO_SIZE_BYTES; i += 1024 * 1024) {
                    checksum += videoData[i];
                }
                
                return "直接加载成功! 视频大小: " + VIDEO_SIZE_MB + "MB, 校验和: " + checksum;
            } catch (OutOfMemoryError e) {
                Log.e(TAG, "内存溢出错误: " + e.getMessage());
                return "内存溢出错误: " + e.getMessage();
            }
        }

        @Override
        protected void onPostExecute(String result) {
            MainActivity activity = activityRef.get();
            if (activity != null) {
                activity.progressBar.setIndeterminate(false);
                activity.progressBar.setVisibility(View.GONE);
                activity.tvStatus.setText("加载完成");
                activity.tvResult.setText(result);
                activity.tvResult.setTextColor(result.startsWith("直接加载成功") ? 
                        0xFF4CAF50 : 0xFFF44336);
                activity.btnDirectLoad.setEnabled(true);
                activity.btnChunkLoad.setEnabled(true);
                activity.updateMemoryInfo();
            }
        }
    }

    // 分块加载任务
    private static class ChunkLoadTask extends AsyncTask<Void, Integer, String> {
        private final WeakReference<MainActivity> activityRef;

        ChunkLoadTask(MainActivity activity) {
            activityRef = new WeakReference<>(activity);
        }

        @Override
        protected void onPreExecute() {
            MainActivity activity = activityRef.get();
            if (activity != null) {
                activity.progressBar.setVisibility(View.VISIBLE);
                activity.progressBar.setProgress(0);
                activity.btnDirectLoad.setEnabled(false);
                activity.btnChunkLoad.setEnabled(false);
            }
        }

        @Override
        protected String doInBackground(Void... voids) {
            MainActivity activity = activityRef.get();
            if (activity == null) return "Activity not available";

            try {
                int chunks = VIDEO_SIZE_BYTES / CHUNK_SIZE;
                int checksum = 0;
                
                for (int i = 0; i < chunks; i++) {
                    // 检查是否取消
                    if (isCancelled()) {
                        return "加载已取消";
                    }
                    
                    // 分配小块内存
                    byte[] chunk = new byte[CHUNK_SIZE];
                    
                    // 填充模拟数据
                    for (int j = 0; j < CHUNK_SIZE; j++) {
                        chunk[j] = (byte) ((i * CHUNK_SIZE + j) % 256);
                    }
                    
                    // 处理数据块
                    for (int j = 0; j < CHUNK_SIZE; j += 1024) {
                        checksum += chunk[j];
                    }
                    
                    // 释放引用,允许垃圾回收
                    chunk = null;
                    System.gc();
                    
                    // 更新进度
                    publishProgress((i * 100) / chunks);
                }
                
                return "分块加载成功! 处理了" + VIDEO_SIZE_MB + "MB, 校验和: " + checksum;
            } catch (OutOfMemoryError e) {
                Log.e(TAG, "内存溢出错误: " + e.getMessage());
                return "内存溢出错误: " + e.getMessage();
            }
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            MainActivity activity = activityRef.get();
            if (activity != null) {
                activity.progressBar.setProgress(values[0]);
                activity.tvStatus.setText("分块加载中: " + values[0] + "%");
            }
        }

        @Override
        protected void onPostExecute(String result) {
            MainActivity activity = activityRef.get();
            if (activity != null) {
                activity.progressBar.setVisibility(View.GONE);
                activity.tvStatus.setText("加载完成");
                activity.tvResult.setText(result);
                activity.tvResult.setTextColor(0xFF4CAF50);
                activity.btnDirectLoad.setEnabled(true);
                activity.btnChunkLoad.setEnabled(true);
                activity.updateMemoryInfo();
                
                if (!result.startsWith("分块加载成功")) {
                    Toast.makeText(activity, "加载过程中出现问题", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
}

OOM日志:

      Process: com.evenbus.myapplication, PID: 2478
                                                                                                    java.lang.OutOfMemoryError: Failed to allocate a 2097152016 byte allocation with 12582912 free bytes and 252MB until OOM, target footprint 16369648, growth limit 268435456
                                                                                                    	at com.evenbus.myapplication.leak.oom.OomOriginImageActivity$1.onClick(OomOriginImageActivity.java:35)
                                                                                                    	at android.view.View.performClick(View.java:8140)
                                                                                                    	at android.view.View.performClickInternal(View.java:8117)
                                                                                                    	at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0)
                                                                                                    	at android.view.View$PerformClick.run(View.java:32138)

内存分配过大

   byte[] videoData = new byte[VIDEO_SIZE_BYTES]; // 500MB

Android应用有严格的堆内存限制(通常128-512MB),500MB的连续分配极易引发OOM

2.KOOM分析大数据案例

完整KOOM报告可通过命令行获取:
adb shell cat /sdcard/Android/data/com.example.videostreamdemo/files/koom/report_20250807_143025.json

KOOM: OOM导致都奔溃了,怎么抓取日志?

2.1 Koom的报告

{
  "header": {
    "process": "com.example.videostreamdemo",
    "platform": "Android",
    "os_version": "Android 13 (API 33)",
    "device_model": "Pixel 6 Pro",
    "koom_version": "1.2.1",
    "timestamp": "2025-08-07T14:30:25Z",
    "analysis_duration": "12.7s",
    "oom_type": "JAVA_OOM"
  },
  "memory_info": {
    "heap_summary": {
      "max_heap_mb": 256,
      "used_heap_mb": 230,
      "free_heap_mb": 26,
      "threshold_mb": 192
    },
    "memory_status": "CRITICAL",
    "vm_stats": {
      "gc_count": 42,
      "gc_time_ms": 1250,
      "allocated_mb": 215
    }
  },
  "oom_reason": {
    "main_cause": "SINGLE_LARGE_ALLOCATION",
    "trigger_thread": "AsyncTask #1",
    "requested_size_mb": 500,
    "available_size_mb": 26,
    "allocation_stack": [
      {
        "class": "byte[]",
        "size_mb": 500,
        "leaking": true,
        "stacktrace": [
          "java.lang.OutOfMemoryError: Failed to allocate a 524288012 byte allocation with 25165824 free bytes and 247MB until OOM",
          "at com.example.videostreamdemo.MainActivity$DirectLoadTask.doInBackground(MainActivity.java:102)",
          "at com.example.videostreamdemo.MainActivity$DirectLoadTask.doInBackground(MainActivity.java:75)",
          "at android.os.AsyncTask$2.call(AsyncTask.java:394)",
          "at java.util.concurrent.FutureTask.run(FutureTask.java:266)",
          "at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:305)",
          "at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)",
          "at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)",
          "at java.lang.Thread.run(Thread.java:920)"
        ]
      }
    ]
  },
  "large_objects": [
    {
      "object_type": "byte[]",
      "size_mb": 500.0,
      "retained_size_mb": 500.0,
      "dominator_path": [
        "DirectLoadTask instance",
        "Thread pool worker thread"
      ],
      "allocation_site": "MainActivity$DirectLoadTask.doInBackground()"
    }
  ],
  "leak_suspects": [
    {
      "description": "500MB byte array held by background thread",
      "retained_size_mb": 500,
      "path_to_gc_root": [
        "byte[524288000] @ 0x12e45a000",
        "↓ held by local variable in DirectLoadTask.doInBackground()",
        "↓ held by running AsyncTask #1",
        "↓ held by ThreadPoolExecutor worker",
        "↓ held by system thread pool"
      ]
    }
  ],
  "memory_advice": {
    "risk_level": "CRITICAL",
    "suggestions": [
      {
        "id": "CHUNK_LOADING",
        "priority": "HIGH",
        "description": "Replace single large allocation with chunked loading",
        "code_sample": "for (int offset=0; offset<total; offset+=chunkSize) {\n  byte[] chunk = new byte[chunkSize];\n  // Process chunk\n}"
      },
      {
        "id": "MEMORY_GUARD",
        "priority": "MEDIUM",
        "description": "Add pre-allocation memory check",
        "code_sample": "long freeMem = Runtime.getRuntime().freeMemory();\nif (freeMem < requiredSize * 1.5) {\n  throw new InsufficientMemoryException();\n}"
      },
      {
        "id": "OFFHEAP_STORAGE",
        "priority": "LOW",
        "description": "Consider using memory-mapped files for large media",
        "reference": "https://developer.android.com/reference/java/nio/MappedByteBuffer"
      }
    ]
  },
  "thread_analysis": {
    "oom_thread": {
      "name": "AsyncTask #1",
      "state": "RUNNABLE",
      "stacktrace": [
        "java.lang.Throwable: OOM occurred here",
        "at com.example.videostreamdemo.MainActivity$DirectLoadTask.doInBackground(MainActivity.java:102)",
        "at android.os.AsyncTask$2.call(AsyncTask.java:394)",
        "..."
      ]
    },
    "high_memory_threads": [
      {
        "name": "main",
        "allocated_mb": 45,
        "stacktrace": "..."
      }
    ]
  },
  "app_stats": {
    "foreground_time": "3m45s",
    "memory_usage_history": [
      {"time": "-60s", "used_mb": 120},
      {"time": "-30s", "used_mb": 125},
      {"time": "OOM", "used_mb": 730}
    ],
    "crash_count": 3
  }
}

2.2 Koom的json报告解读

关键报告字段解析:

2.2.1. OOM根因分析

"oom_reason": {
  "main_cause": "SINGLE_LARGE_ALLOCATION",
  "requested_size_mb": 500,
  "available_size_mb": 26
}

显示尝试分配500MB时,可用内存仅26MB

2.2.2. 大对象追踪

"large_objects": [{
  "object_type": "byte[]",
  "size_mb": 500.0,
  "allocation_site": "MainActivity$DirectLoadTask.doInBackground()"
}]

明确指向DirectLoadTask中的字节数组分配

2.2.3. 内存泄漏路径

"path_to_gc_root": [
  "byte[524288000]",
  "↓ held by local variable in DirectLoadTask.doInBackground()",
  "↓ held by running AsyncTask #1"
]

显示500MB数组被后台线程强引用导致无法回收

3.Profiler分析大数据案例

profier.png

3.1. 在设备上点击  "直接加载"  按钮

3.2. 观察内存曲线:

-   会看到内存瞬间飙升到 600+ MB
-   随后出现 OOM 崩溃

3.3. 捕获堆转储 (Heap Dump)**

在内存峰值时(崩溃前):

  1. 点击 Dump Java heap 图标 (堆栈图标)
  2. 或使用快捷键:Ctrl + D (Win/Linux) / Cmd + D (Mac)

3.4. 分析堆转储 - 定位大对象

在 Heap Dump 面板:

  1. 排序对象

    • 点击 Shallow Size 列标题(降序排列)
    • 点击 Retained Size 列标题(降序排列)
  2. 过滤关键对象

    • 在搜索框输入:byte[]
    • 或过滤:size:>100000000 (查找大于100MB的对象)
  3. 识别问题对象

    Class Name       | Shallow Size | Retained Size
    ----------------------------------------------
    byte[]           | 524,288,000 | 524,288,000  <-- 500MB对象
    char[]           | 1,234,567   | 1,234,567
    ...
    

3.5. 查看对象引用链

  1. 右键点击 500MB 的 byte[] 对象

  2. 选择 Jump to Source

    // 跳转到源代码位置
    byte[] videoData = new byte[VIDEO_SIZE_BYTES]; // MainActivity.java:102
    
  3. 或查看引用树:

    References to this object:
    └─ videoData (local variable)
       └─ in DirectLoadTask.doInBackground()
          └─ AsyncTask$2.call()
             └─ FutureTask.run()
    

image.png

4.MAT分析上面的案例

4.1. 概览仪表盘 (Overview Dashboard)

Heap Size: 756 MB
Used Heap: 732 MB (96.8%)
Classes: 8,742
Objects: 1,243,876
Class Loaders: 189

问题指标

  • Unreachable Objects: 3.2 MB (正常)
  • Shallow Heap vs Retained Heap 比例异常
  • 大对象占比: 99.3%

4.2. 泄漏嫌疑报告 (Leak Suspects)

Problem Suspect 1:
One instance of "byte[]" loaded by "<system class loader>"
occupies 524,288,000 bytes (69.3% of total heap)

Accumulated Objects:
• byte[524288000] @ 0x7d3c5a000 - 500MB
• DirectLoadTask @ 0x7d1f4b300 - 32 bytes
• AsyncTask$2 @ 0x7d1f4b280 - 48 bytes

Dominator Tree:
byte[524288000] (500MB)
  <- DirectLoadTask.videoData (field)
    <- DirectLoadTask (instance)
      <- AsyncTask$2 (callable)
        <- FutureTask (task)
          <- Thread (worker)

4.3. 支配树分析 (Dominator Tree,重点)

4.3.1 排序

  • 点击  "Retained Heap"  列头(降序排列)
  • 点击  "Percentage"  列头(降序排列)
Dominator Tree (Top 5 by Retained Size)
1. byte[524288000] 
   Retained: 524,288,000 bytes (500MB)
   Shallow: 524,288,016 bytes
   GCRoot: Thread "AsyncTask #1"

2. android.graphics.Bitmap 
   Retained: 12,582,912 bytes (12MB)
   Shallow: 48 bytes

3. java.lang.String[] 
   Retained: 5,342,176 bytes (5.1MB)
   Shallow: 5,342,192 bytes

4.3.2 分析500MB byte[]对象

  1. 右键点击 byte[524288000] 对象

  2. 选择  "Path to GC Roots"  >  "exclude weak references"

  3. 查看完整引用链:

    byte[524288000] @ 0x7d3c5a000 (500MB)
      ↑ held by field: videoData
      ■ MainActivity$DirectLoadTask @ 0x7d1f4b300
        ↑ held by field: callable
        ■ java.util.concurrent.FutureTask @ 0x7d1f4b280
          ↑ held by field: runner
          ■ java.util.concurrent.ThreadPoolExecutor$Worker @ 0x7d1f4b200
            ↑ held by thread: "AsyncTask #1" (RUNNABLE)
    

4.4. 直方图分析 (Histogram,重点)

image.png

4.4.1 按Shallow Heap降序:

  • 点击 "Retained Heap" 列头(降序排列)
  • 点击 "Shallow Heap" 列头(降序排列)
Class Name               | Objects | Shallow Heap | Retained Heap
---------------------------------------------------------------
byte[]                   | 12,458  | 524,328,016 | 524,328,016
char[]                   | 45,782  |  12,345,672 |  12,345,672
java.lang.String         | 84,567  |   5,432,176 |   8,765,432
android.graphics.Bitmap  |     42  |   1,048,576 |  12,582,912

4.4.2 过滤关键类

-   在搜索框输入:`byte[]`
-   或使用正则:`.*(byte|Byte).*`

4.4.3 byte[] 类详细分析

右键点击 byte[] 类 -> "List objects" -> "with incoming references"

byte[524288000] @ 0x7d3c5a000
  <- [videoData] MainActivity$DirectLoadTask @ 0x7d1f4b300
byte[] Instances (Top 5 by Size):
1. byte[524288000] @ 0x7d3c5a000
   Shallow: 524,288,016 B
   Retained: 524,288,000 B
   Incoming References:
     -> MainActivity$DirectLoadTask.videoData (field)

2. byte[12582912] @ 0x7d2a1b000
   Shallow: 12,582,928 B
   Retained: 12,582,912 B

3. byte[2097152] @ 0x7d1f4c000
   Shallow: 2,097,152 B
   Retained: 2,097,136 B

4.4.4 引用链

选择 524MB 实例 右键 -> "Path To GC Roots" -> "exclude weak references"

GC Root Path:
<- java.lang.Thread (AsyncTask #1)
  <- java.util.concurrent.ThreadPoolExecutor$Worker
    <- java.util.concurrent.FutureTask
      <- MainActivity$DirectLoadTask
        <- byte[524288000] @ 0x7d3c5a000

4.4.5 大小分布统计

右键 byte[] 类 -> "Group" -> "by size"

Size Range         | Count | Total Retained
------------------------------------------
> 100 MB           |     1 | 524,288,000 B
10 MB - 100 MB     |     3 |  36,864,000 B
1 MB - 10 MB       |    42 |  84,672,000 B
< 1 MB             | 12,412|  18,765,432 B

关键结论:存在单个超大对象(500MB),远超其他对象

5. OQL查询 (Object Query Language, 重点)

OQL查询,超过10M的byte[]

SELECT SUM(@retainedHeapSize)/1048576 as total_mb
FROM byte[] 
WHERE @retainedHeapSize > 104857600

6. 线程分析 (Thread Overview)

展开 "AsyncTask #1" 线程:

Stack Trace:
  at MainActivity$DirectLoadTask.doInBackground(MainActivity.java:102)
  at android.os.AsyncTask$2.call(AsyncTask.java:394)
  at java.util.concurrent.FutureTask.run(FutureTask.java:266)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:305)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
  at java.lang.Thread.run(Thread.java:920)
  
Local Variables:
  videoData = byte[524288000] @ 0x7d3c5a000

5.修复方案:

分块,分片加载

// 分块加载任务
    private static class ChunkLoadTask extends AsyncTask<Void, Integer, String> {
        private final WeakReference<MainActivity> activityRef;

        ChunkLoadTask(MainActivity activity) {
            activityRef = new WeakReference<>(activity);
        }

        @Override
        protected void onPreExecute() {
            MainActivity activity = activityRef.get();
            if (activity != null) {
                activity.progressBar.setVisibility(View.VISIBLE);
                activity.progressBar.setProgress(0);
                activity.btnDirectLoad.setEnabled(false);
                activity.btnChunkLoad.setEnabled(false);
            }
        }

        @Override
        protected String doInBackground(Void... voids) {
            MainActivity activity = activityRef.get();
            if (activity == null) return "Activity not available";

            try {
                int chunks = VIDEO_SIZE_BYTES / CHUNK_SIZE;
                int checksum = 0;
                
                for (int i = 0; i < chunks; i++) {
                    // 检查是否取消
                    if (isCancelled()) {
                        return "加载已取消";
                    }
                    
                    // 分配小块内存
                    byte[] chunk = new byte[CHUNK_SIZE];
                    
                    // 填充模拟数据
                    for (int j = 0; j < CHUNK_SIZE; j++) {
                        chunk[j] = (byte) ((i * CHUNK_SIZE + j) % 256);
                    }
                    
                    // 处理数据块
                    for (int j = 0; j < CHUNK_SIZE; j += 1024) {
                        checksum += chunk[j];
                    }
                    
                    // 释放引用,允许垃圾回收
                    chunk = null;
                    System.gc();
                    
                    // 更新进度
                    publishProgress((i * 100) / chunks);
                }
                
                return "分块加载成功! 处理了" + VIDEO_SIZE_MB + "MB, 校验和: " + checksum;
            } catch (OutOfMemoryError e) {
                Log.e(TAG, "内存溢出错误: " + e.getMessage());
                return "内存溢出错误: " + e.getMessage();
            }

6.总结

KOOM+ Profiler+KOOM对比

3者.png

支配树和直方图的步骤区别 支配树和直方图都是可以搜索过滤的, 查询语句是一样的!

2者.png

6.1. 分析视角差异

维度直方图支配树
分析单位类级别 (Class-level)对象实例级别 (Instance-level)
主要焦点同类对象聚合统计单个对象及其支配关系
内存关系不显示对象间引用关系清晰展示对象间支配关系
最佳适用场景识别内存消耗最大的类定位具体内存泄漏对象

6.2. 数据组织方式

直方图数据结构

| Class Name      | Objects | Shallow | Retained |
|-----------------|---------|---------|----------|
| byte[]          | 12,458  | 524 MB  | 524 MB   |
| java.lang.String| 84,567  | 5.4 MB  | 8.7 MB   |
■ byte[524288000] @0x7d3c5a000 (524MB)
  ← MainActivity$DirectLoadTask @0x7d1f4b300
    ← FutureTask @0x7d1f4b280
      ← Thread "AsyncTask #1"

直方图局限性

  • 无法直接查看对象引用链
  • 需要手动进行"Path to GC Roots"操作
  • 不显示对象间的支配关系

支配树优势

  • 自动展示完整支配链
  • 直观显示"谁保持对象存活"
  • 可直接计算子树内存总和

6.3. 大对象分析对比

分析维度直方图支配树
大对象定位需排序后查找默认按保留堆排序,顶部即大对象
内存占比显示类总占比显示单个对象占比
关联对象需手动查找持有者直接显示支配者
碎片化分析适合分析多个中小对象适合分析单个超大对象

6.4. 视频加载案例实战对比

直方图分析流程:
  1. 按Retained Heap排序 → 发现byte[]类占524MB
  2. 展开byte[] → List objects with incoming references
  3. 找到524MB实例 → Path to GC Roots
  4. 追踪到DirectLoadTask → 耗时45秒
支配树分析流程:
  1. 打开支配树视图 → 直接看到524MB对象
  2. 右键 → Path to GC Roots → 立即显示完整引用链
  3. 定位到DirectLoadTask → 耗时10秒

6.5直方图 vs 支配树:分析步骤详细对比表

分析步骤直方图 (Histogram)支配树 (Dominator Tree)步骤差异说明
1. 初始准备打开堆转储文件 → 选择"Histogram"视图打开堆转储文件 → 选择"Dominator Tree"视图相同初始操作
2. 问题定位起点按"Retained Heap"列排序 → 查找内存占比最大的类自动按"Retained Heap"降序排列 → 顶部即最大对象支配树直接暴露问题对象
3. 关键对象识别① 找到byte[]类 ② 展开类查看实例列表 ③ 按大小排序找到500MB实例直接在顶部看到byte[524288000]对象(500MB)直方图需3步,支配树0步
4. 内存关系分析右键实例 → "Path to GC Roots" → 查看引用链直接展示完整支配链: Thread → FutureTask → DirectLoadTask → byte[]直方图需手动操作
5. 上下文关联需要手动跳转到"Thread Overview"查看线程状态可直接展开线程节点查看栈帧和局部变量支配树集成上下文
6. 影响范围分析① 选择对象 ② 右键"Immediate Dominators" ③ 查看支配关系直接显示支配关系树和子树内存总和直方图需额外操作
7. 问题根源定位通过引用链回溯到DirectLoadTask.java:102直接显示支配者DirectLoadTask及源代码行号支配树更直观
8. 定量分析需手动计算类总内存占比自动显示百分比:69.3%支配树更高效
9. 优化验证需重新捕获堆转储比较类分布可直接对比支配树结构和最大对象变化支配树更适合验证

6.5 关键差异总结

分析维度直方图支配树优势方
问题暴露速度需排序和查找立即可见支配树
对象关系展示分离视图集成视图支配树
内存层级分析平面结构树状结构支配树
类聚合分析优秀一般直方图
大对象分析多步操作一步到位支配树
碎片化分析适合不适合直方图
学习曲线平缓较陡峭直方图
OOM分析效率中等高效支配树

6.6 最佳实践

  1. 先用直方图识别可疑类
  2. 对可疑类使用"Show in Dominator Tree"
  3. 在支配树中分析具体对象
  4. 使用"Merge Shortest Paths"简化视图

混合分析.png

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