大内存的场景
- 加载大型视频/音频文件
试图将整个文件(如 1GB 视频)读入byte[]
或ByteBuffer
- 一次性将整个文件读入内存
- 加载超大图片
本质分为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)**
在内存峰值时(崩溃前):
- 点击 Dump Java heap 图标 (堆栈图标)
- 或使用快捷键:Ctrl + D (Win/Linux) / Cmd + D (Mac)
3.4. 分析堆转储 - 定位大对象
在 Heap Dump 面板:
-
排序对象:
- 点击 Shallow Size 列标题(降序排列)
- 点击 Retained Size 列标题(降序排列)
-
过滤关键对象:
- 在搜索框输入:
byte[]
- 或过滤:
size:>100000000
(查找大于100MB的对象)
- 在搜索框输入:
-
识别问题对象:
luaClass Name | Shallow Size | Retained Size ---------------------------------------------- byte[] | 524,288,000 | 524,288,000 <-- 500MB对象 char[] | 1,234,567 | 1,234,567 ...
3.5. 查看对象引用链
-
右键点击 500MB 的
byte[]
对象 -
选择 Jump to Source:
arduino// 跳转到源代码位置 byte[] videoData = new byte[VIDEO_SIZE_BYTES]; // MainActivity.java:102
-
或查看引用树:
cssReferences 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[]对象
-
右键点击
byte[524288000]
对象 -
选择 "Path to GC Roots" > "exclude weak references"
-
查看完整引用链:
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. 视频加载案例实战对比
直方图分析流程:
- 按Retained Heap排序 → 发现byte[]类占524MB
- 展开byte[] → List objects with incoming references
- 找到524MB实例 → Path to GC Roots
- 追踪到DirectLoadTask → 耗时45秒
支配树分析流程:
- 打开支配树视图 → 直接看到524MB对象
- 右键 → Path to GC Roots → 立即显示完整引用链
- 定位到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 最佳实践:
- 先用直方图识别可疑类
- 对可疑类使用"Show in Dominator Tree"
- 在支配树中分析具体对象
- 使用"Merge Shortest Paths"简化视图

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