ภาพรวม
บทความนี้สาธิตวิธีตรวจสอบขนาด Heap Memory (หน่วยความจำที่จัดสรรให้แอป) ใน Android โดยใช้ ActivityManager.getMemoryClass() และ Debug.getNativeHeapAllocatedSize() พร้อมอธิบาย Dalvik/ART Heap, Memory Limit, และวิธีป้องกัน OutOfMemoryError
ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่
สร้างโปรเจกต์ Android ใหม่ใน Android Studio
ขั้นตอนที่ 2: Layout (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center"
tools:context=".MainActivity">
<Button
android:id="@+id/btnCheckHeap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🔍 ตรวจสอบ Heap Size" />
<TextView
android:id="@+id/tvHeapInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textSize="16sp"
android:lineSpacingExtra="8dp"
android:gravity="center"
android:text="กดปุ่มเพื่อตรวจสอบ" />
</LinearLayout>
ขั้นตอนที่ 3: MainActivity.java - ครอบคลุมทุก Metric
import androidx.appcompat.app.AppCompatActivity;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Debug;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.Formatter;
public class MainActivity extends AppCompatActivity {
private TextView tvHeapInfo;
private Button btnCheckHeap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvHeapInfo = findViewById(R.id.tvHeapInfo);
btnCheckHeap = findViewById(R.id.btnCheckHeap);
btnCheckHeap.setOnClickListener(v -> showHeapInfo());
}
private void showHeapInfo() {
StringBuilder info = new StringBuilder();
// 1. Dalvik/ART Heap Limit (จาก ActivityManager) - API 1+
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass(); // MB
int largeMemoryClass = activityManager.getLargeMemoryClass(); // MB (สำหรับ largeHeap=true)
info.append("📱 Heap Limits (System):\n");
info.append(String.format(" • Normal Heap Limit: %d MB\n", memoryClass));
info.append(String.format(" • Large Heap Limit: %d MB\n\n", largeMemoryClass));
// 2. Current Heap Usage (Runtime) - API 1+
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory(); // Heap Max (bytes)
long totalMemory = runtime.totalMemory(); // Heap Current Allocated (bytes)
long freeMemory = runtime.freeMemory(); // Free ใน Heap ปัจจุบัน (bytes)
long usedMemory = totalMemory - freeMemory; // Used จริง (bytes)
info.append("📊 Current Heap (Runtime):\n");
info.append(String.format(" • Max Heap: %s\n", formatBytes(maxMemory)));
info.append(String.format(" • Allocated: %s\n", formatBytes(totalMemory)));
info.append(String.format(" • Used: %s\n", formatBytes(usedMemory)));
info.append(String.format(" • Free: %s\n\n", formatBytes(freeMemory)));
// 3. Native Heap (C/C++ allocations, Bitmaps pre-API 8) - API 1+
long nativeAllocated = Debug.getNativeHeapAllocatedSize();
long nativeFree = Debug.getNativeHeapFreeSize();
long nativeSize = Debug.getNativeHeapSize();
info.append("🔧 Native Heap:\n");
info.append(String.format(" • Allocated: %s\n", formatBytes(nativeAllocated)));
info.append(String.format(" • Free: %s\n", formatBytes(nativeFree)));
info.append(String.format(" • Total: %s\n\n", formatBytes(nativeSize)));
// 4. Java Heap (MemoryMXBean) - API 9+
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage();
info.append("☕ Java Memory MXBean:\n");
info.append(String.format(" • Heap Used: %s / Max: %s\n",
formatBytes(heapUsage.getUsed()), formatBytes(heapUsage.getMax())));
info.append(String.format(" • Non-Heap Used: %s / Max: %s\n\n",
formatBytes(nonHeapUsage.getUsed()), formatBytes(nonHeapUsage.getMax())));
// 5. PSS (Proportional Set Size) - API 16+ (Accurate System View)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
android.os.Debug.MemoryInfo[] memInfo = activityManager.getProcessMemoryInfo(new int[]{android.os.Process.myPid()});
if (memInfo.length > 0) {
long pssKb = memInfo[0].getTotalPss(); // KB
long privateDirtyKb = memInfo[0].getTotalPrivateDirty(); // KB
info.append("📈 Process Memory (PSS):\n");
info.append(String.format(" • Total PSS: %s\n", formatBytes(pssKb * 1024)));
info.append(String.format(" • Private Dirty: %s\n", formatBytes(privateDirtyKb * 1024)));
}
}
// 6. App Memory Class (API 16+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
info.append("\n💾 System Memory:\n");
info.append(String.format(" • Available: %s\n", formatBytes(memoryInfo.availMem)));
info.append(String.format(" • Total: %s\n", formatBytes(memoryInfo.totalMem)));
info.append(String.format(" • Low Memory: %s\n", memoryInfo.lowMemory));
info.append(String.format(" • Threshold: %s\n", formatBytes(memoryInfo.threshold)));
}
tvHeapInfo.setText(info.toString());
}
private String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
String unit = "KMGTPE".charAt(exp - 1) + "B";
return String.format(Locale.getDefault(), "%.2f %s", bytes / Math.pow(1024, exp), unit);
}
}
ขั้นตอนที่ 4: AndroidManifest.xml - Large Heap Option
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="app.com.sample">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:largeHeap="true"> <!-- ขอ Heap ใหญ่กว่าปกติ (ใช้ด้วยความระมัดระวัง) -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
ความเข้าใจ Heap Memory ใน Android
1. Dalvik vs ART
- Dalvik (API < 21): JIT Compilation, Heap แยก Process, Garbage Collection แบบ Stop-the-world
- ART (API 21+): AOT Compilation, Heap แบบ Generational, GC แบบ Concurrent/Incremental, ใช้ Memory มากกว่า Dalvik
2. Memory Class (ต่อ Device)
| Device Category | Normal Heap | Large Heap |
|---|---|---|
| Low-end (Go Edition) | 16-32 MB | 64-128 MB |
| Mid-range | 64-128 MB | 256-512 MB |
| High-end / Flagship | 192-512 MB | 512 MB - 1.5 GB |
| Tablet / Foldable | 256-512 MB | 1-2 GB |
3. android:largeHeap="true"
- ขอ Heap Size =
getLargeMemoryClass()แทนgetMemoryClass() - ใช้เมื่อ: Image Processing, Large Bitmap, Video Editing, ML Models
- ข้อเสีย: GC ช้ากว่า, ใช้ System Memory มาก, อาจโดน Kill โดย LMK (Low Memory Killer) ก่อน
- Best Practice: หลีกเลี่ยงถ้าเป็นไปได้ ใช้ Bitmap Pooling, inSampleSize, LruCache แทน
การ Monitor Memory ใน Development
1. Android Studio Profiler (แนะนำ)
- View → Tool Windows → Profiler
- เลือก Process → Memory Tab
- ดู: Java Heap, Native Heap, Graphics, Stack, Code, Others
- Capture Heap Dump → Analyze Leak (LeakCanary Integration)
2. ADB Commands
# ดู Memory Info ของ Package adb shell dumpsys meminfo com.package.name # ดู Heap Dump (ต้อง Debuggable) adb shell am dumpheap com.package.name /data/local/tmp/heap.hprof # Pull มา analyze ด้วย MAT (Memory Analyzer Tool) หรือ Android Studio
3. LeakCanary (Library Detect Leak อัตโนมัติ)
dependencies {
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.12"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:2.12"
}
OutOfMemoryError - สาเหตุและการแก้
| สาเหตุ | อาการ | แก้ไข |
|---|---|---|
| Bitmap ใหญ่เกิน Heap | OOM บน decodeBitmap | inSampleSize, inBitmap, Glide/Coil, WebP |
| Memory Leak (Context, Listener, Static Ref) | Heap เพิ่มขึ้นเรื่อยๆ | LeakCanary, WeakReference, Lifecycle-Aware |
| Large Heap แต่อุปกรณ์ Low-end | Crash บนเครื่องเก่า | ไม่ใช้ largeHeap, Optimize Bitmap |
| Native Memory (Bitmap pre-API 8, OpenGL, NDK) | Java Heap OK แต่ Native เต็ม | recycle(), close(), explicit free |
Best Practices
- ใช้
LruCacheสำหรับ Cache Bitmap/Object - โหลดรูปด้วย
inSampleSizeหรือ Library (Glide, Coil, Picasso) - หลีกเลี่ยง Static Reference ของ Context/View/Bitmap
- Unregister Listener/Callback ใน onDestroy/onStop
- ใช้
WeakReferenceสำหรับ Callback/Listener - Profile เรียบร้อยก่อน Release