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

大内存的场景

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

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

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

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

ini 复制代码
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日志:

php 复制代码
      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)

内存分配过大

arduino 复制代码
   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的报告

json 复制代码
{
  "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根因分析

json 复制代码
"oom_reason": {
  "main_cause": "SINGLE_LARGE_ALLOCATION",
  "requested_size_mb": 500,
  "available_size_mb": 26
}

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

2.2.2. 大对象追踪

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

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

2.2.3. 内存泄漏路径

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

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

3.Profiler分析大数据案例

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

3.2. 观察内存曲线:

diff 复制代码
-   会看到内存瞬间飙升到 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. 识别问题对象

    lua 复制代码
    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

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

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

4.MAT分析上面的案例

4.1. 概览仪表盘 (Overview Dashboard)

yaml 复制代码
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)

python 复制代码
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" 列头(降序排列)
python 复制代码
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. 查看完整引用链:

    csharp 复制代码
    ■ 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,重点)

4.4.1 按Shallow Heap降序:

  • 点击 "Retained Heap" 列头(降序排列)
  • 点击 "Shallow Heap" 列头(降序排列)
vbnet 复制代码
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 过滤关键类

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

4.4.3 byte[] 类详细分析

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

ini 复制代码
byte[524288000] @ 0x7d3c5a000
  <- [videoData] MainActivity$DirectLoadTask @ 0x7d1f4b300
less 复制代码
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"

r 复制代码
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"

css 复制代码
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[]

less 复制代码
SELECT SUM(@retainedHeapSize)/1048576 as total_mb
FROM byte[] 
WHERE @retainedHeapSize > 104857600

6. 线程分析 (Thread Overview)

展开 "AsyncTask #1" 线程:

php 复制代码
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.修复方案:

分块,分片加载

ini 复制代码
// 分块加载任务
    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对比

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

6.1. 分析视角差异

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

6.2. 数据组织方式

直方图数据结构

vbnet 复制代码
| Class Name      | Objects | Shallow | Retained |
|-----------------|---------|---------|----------|
| byte[]          | 12,458  | 524 MB  | 524 MB   |
| java.lang.String| 84,567  | 5.4 MB  | 8.7 MB   |
perl 复制代码
■ 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"简化视图

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

相关推荐
卑微前端在线挨打5 分钟前
2025数字马力一面面经(社)
前端
OpenTiny社区19 分钟前
一文解读“Performance面板”前端性能优化工具基础用法!
前端·性能优化·opentiny
拾光拾趣录41 分钟前
🔥FormData+Ajax组合拳,居然现在还用这种原始方式?💥
前端·面试
不会笑的卡哇伊1 小时前
新手必看!帮你踩坑h5的微信生态~
前端·javascript
bysking1 小时前
【28 - 记住上一个页面tab】实现一个记住用户上次点击的tab,上次搜索过的数据 bysking
前端·javascript
Dream耀1 小时前
跨域问题解析:从同源策略到JSONP与CORS
前端·javascript
前端布鲁伊1 小时前
【前端高频面试题】面试官: localhost 和 127.0.0.1有什么区别
前端
HANK1 小时前
Electron + Vue3 桌面应用开发实战指南
前端·vue.js
極光未晚1 小时前
Vue 前端高效分包指南:从 “卡成 PPT” 到 “丝滑如德芙” 的蜕变
前端·vue.js·性能优化
郝亚军1 小时前
炫酷圆形按钮调色器
前端·javascript·css