แอปพลิเคชัน Java และ Kotlin จัดการหน่วยความจำผ่านฮีปที่รวบรวมขยะ เมื่อเข้าถึงออบเจ็กต์ไม่ได้อีกต่อไป ตัวเก็บขยะ (GC) จะเรียกคืนพื้นที่ของออบเจ็กต์ในที่สุด หน่วยความจำรั่วเกิดขึ้นเมื่อออบเจ็กต์ที่ไม่จำเป็นอีกต่อไป ยังคงอยู่ใน "GC roots" ซึ่งทำให้ไม่สามารถเรียกคืนได้
แนวคิดหลัก
รูทของ GC
GC Root เป็นออบเจ็กต์ประเภทพิเศษที่ตัวเก็บขยะจะถือว่า เข้าถึงได้เสมอ ตัวอย่างเช่น
- เธรดที่ใช้งานอยู่ (และออบเจ็กต์ที่อ้างอิงจากเฟรมสแต็ก Java ที่กำลังดำเนินการอยู่)
- ชั้นเรียนที่มีวิธีการที่ใช้งานอยู่
- การอ้างอิง JNI (การอ้างอิงส่วนกลางหรือการอ้างอิงในเครื่องที่โค้ดแบบเนทีฟถือครอง)
เส้นทางไปยังรูท GC
ตราบใดที่มีการอ้างอิงแบบลูกโซ่จากรูทของ GC ไปยังออบเจ็กต์ ออบเจ็กต์นั้นจะ "เข้าถึงได้" และไม่สามารถเก็บขยะได้ ซึ่งเชนนี้เรียกว่าเส้นทางไปยังรูทของ GC หากต้องการแก้ไขปัญหาหน่วยความจำรั่ว คุณต้องระบุและทำลายห่วงโซ่นี้

ต้นไม้ที่โดดเด่น
แม้ว่าเส้นทางไปยังรูท GC จะบอกสาเหตุที่ออบเจ็กต์ยังคงอยู่ แต่ก็ไม่ได้บอกว่า ระบบจะเรียกคืนหน่วยความจำได้มากเพียงใดหากการอ้างอิงนั้นขาดหายไป เราใช้แผนภาพ Dominator สำหรับการดำเนินการนี้
ออบเจ็กต์ A จะครอบงำออบเจ็กต์ B หากทุกเส้นทางจากรูท GC ใดๆ ไปยัง B ต้องผ่าน A หาก A ครอบงำ B การเรียกคืน A จะรับประกันด้วยว่าสามารถเรียกคืน B ได้ เนื่องจากไม่มี เส้นทางอื่นๆ จากรูทไปยัง B
แผนภาพต่อไปนี้แสดงกราฟออบเจ็กต์และแผนภูมิต้นไม้ที่ครอบงำที่เกี่ยวข้อง โปรดสังเกตว่าออบเจ็กต์ D เข้าถึงได้ทั้งจาก A และ B ในกราฟ ดังนั้นทั้ง A และ B จึงไม่ได้ครอบงำ D แต่ GC Root ต่างหากที่เป็น ผู้ครอบงำที่ใกล้ที่สุด

การขอฮีปดัมป์ของ Java
ฮีปดัมป์คือสแนปชอตของออบเจ็กต์ทั้งหมดในฮีปของ Java ณ จุดหนึ่งๆ ในเวลา
การใช้ ADB
หากต้องการบันทึกฮีพดัมพ์จากกระบวนการที่กำลังทำงาน คุณสามารถส่งชื่อแพ็กเกจ
ไปยัง am dumpheap ได้โดยตรง หากต้องการเรียกใช้คำสั่งนี้ คุณต้องสร้างแอปด้วย
<profileable android:shell="true"/> หรือ <debuggable>
# 1. Trigger the dump (the command takes a moment to complete):
adb shell am dumpheap -g -b png com.android.memorylab /data/local/tmp/heap.hprof
# 2. Pull the file to your development machine:
adb pull /data/local/tmp/heap.hprof .
การใช้ Perfetto
นอกจากนี้ Perfetto ยังบันทึกการทิ้งฮีปของ Java เป็นส่วนหนึ่งของการติดตามทั้งระบบได้ด้วยการ
เปิดใช้แหล่งข้อมูล android.java_hprof ในการกำหนดค่า Perfetto ซึ่งมีประโยชน์ในการเชื่อมโยงสถานะฮีปกับเหตุการณ์อื่นๆ ของระบบ
หากต้องการบันทึกฮีปดัมป์สำหรับแอป MemoryLab โดยใช้ Perfetto คุณสามารถใช้คำสั่งต่อไปนี้
# Create a temp file for the configuration
cat > /tmp/java_heap.pbtx <<EOF
data_sources: {
config {
name: "android.java_hprof"
java_hprof_config {
process_cmdline: "com.android.memorylab"
}
}
}
EOF
# Run trace command referencing the file
external/perfetto/tools/record_android_trace -o java_heap.perfetto-trace \
-t 10s -c /tmp/java_heap.pbtx
ดูฮีปดัมป์ของ Java ในเอกสาร Perfetto
การวิเคราะห์ด้วย AHAT
AHAT (Android Heap Analysis Tool) เป็นเครื่องมือที่แนะนำสำหรับการดูไฟล์ .hprof
ในเว็บเบราว์เซอร์
เริ่มต้นใช้งาน AHAT
หากคุณติดตั้ง ahat ไว้ในเส้นทาง ให้เปิดใช้ด้วยคำสั่งต่อไปนี้
ahat heap.hprof
หรือเรียกใช้ไฟล์ JAR แบบสแตนด์อโลน
java -jar ahat.jar heap.hprof
จากนั้นเปิดเบราว์เซอร์ไปที่ http://localhost:7100
ดูรายละเอียดเกี่ยวกับการขอรับหรือสร้าง AHAT ได้ที่ ที่เก็บข้อมูลต้นฉบับของ AHAT
เวิร์กโฟลว์การวิเคราะห์ที่สำคัญ
การค้นหารอยรั่ว
ค้นหาคลาสกิจกรรม (MainActivity) ในมุมมองการจัดสรร

คลิกชั้นเรียนเพื่อดูอินสแตนซ์ทั้งหมด
คลิกอินสแตนซ์
MainActivityเพื่อตรวจสอบ

ในมุมมองอินสแตนซ์ คุณจะเห็นเส้นทางตัวอย่างจากรูท GC ซึ่งแสดง ห่วงโซ่ของการอ้างอิงที่ป้องกันไม่ให้ระบบเก็บขยะของออบเจ็กต์ และ ขนาดออบเจ็กต์ ซึ่งแสดงปริมาณหน่วยความจำที่อินสแตนซ์ ที่เฉพาะเจาะจงนี้เก็บไว้

กำลังวิเคราะห์บิตแมป
AHAT มีการรองรับพิเศษสำหรับการดูออบเจ็กต์ android.graphics.Bitmap ซึ่ง
มักใช้หน่วยความจำจำนวนมาก คลิกอินสแตนซ์บิตแมปเพื่อดูตัวอย่างเนื้อหาที่เรนเดอร์

หน้าการรั่วไหลของกิจกรรม
AHAT มีมุมมองเฉพาะสำหรับการระบุกิจกรรมที่รั่วไหล ซึ่งเป็นหนึ่งในปัญหาหน่วยความจำรั่วที่พบบ่อยและส่งผลกระทบมากที่สุดใน Android
- การดำเนินการ: ใน MemoryLab ให้แตะรั่วไหลกิจกรรม ซึ่งจะเปิดตัว
LeakedActivityซึ่งตั้งใจให้ข้อมูลรั่วไหล - ดัมพ์: สร้างฮีปดัมพ์
- วิเคราะห์: คลิกการรั่วไหลของกิจกรรมในแถบด้านข้างของ AHAT
- ยืนยัน: AHAT จะแสดง
com.android.memorylab.LeakedActivityเป็นหน่วยความจำรั่ว เนื่องจากฟิลด์mDestroyedเป็นจริง (แสดงว่าวงจรกิจกรรม สิ้นสุดแล้ว) แต่ยังคงเข้าถึงได้จากรูท GC

การเปรียบเทียบฮีปดัมป์
การเปรียบเทียบ Heap Dump 2 รายการเป็นวิธีที่มีประสิทธิภาพมากที่สุดวิธีหนึ่งในการระบุปัญหาเกี่ยวกับหน่วยความจำ การเปรียบเทียบการทิ้งข้อมูลพื้นฐานที่ "สะอาด" กับการทิ้งข้อมูลที่ดำเนินการหลังจากดำเนินการบางอย่าง จะช่วยให้คุณเห็นได้ทันทีว่าออบเจ็กต์ใดสะสมอยู่
แบบฝึกหัด: การระบุการรั่วไหลผ่านการเปรียบเทียบ
พื้นฐาน: เปิด MemoryLab แล้วทำการฮีปดัมป์พื้นฐาน
adb shell am dumpheap com.android.memorylab /data/local/tmp/base.hprof adb pull /data/local/tmp/base.hprof .การดำเนินการ: แตะจัดสรรหน่วยความจำ Java(10 MB) หลายครั้งในแอป
สุดท้าย: สร้างฮีปดัมป์ครั้งที่ 2
adb shell am dumpheap com.android.memorylab /data/local/tmp/leaked.hprof adb pull /data/local/tmp/leaked.hprof .เปรียบเทียบ: เริ่ม AHAT โดยใช้การทิ้งข้อมูลครั้งที่ 2 เป็นข้อมูลหลัก และครั้งแรกเป็น ข้อมูลพื้นฐาน
java -jar out/host/linux-x86/framework/ahat.jar leaked.hprof --baseline base.hprofภาพรวมการวิเคราะห์: ตอนนี้หน้าภาพรวมมีคอลัมน์ Δ (เดลต้า) แล้ว คุณจะเห็นเดลต้าบวกขนาดใหญ่สำหรับฮีป
appซึ่งบ่งบอกถึง การเติบโตของหน่วยความจำอย่างมาก

- เจาะลึก: คลิกรูทในเมนู หน้านี้แสดงออบเจ็กต์
ที่เข้าถึงได้จากรูท GC โดยจัดเรียงตามขนาดที่คงไว้ คุณจะเห็น
MainActivityที่ด้านบนซึ่งมีเดลต้าบวกขนาดใหญ่

การบันทึกสแต็กเทรซการจัดสรร
แม้ว่าเส้นทางตัวอย่างจากรูท GC จะบอกสาเหตุที่ออบเจ็กต์ยังคงใช้งานได้ แต่ก็ไม่ได้บอกวิธีการสร้างออบเจ็กต์ การติดตามสแต็กการจัดสรรจะระบุบรรทัดของโค้ดที่แน่นอนซึ่งจัดสรรออบเจ็กต์
แนวคิดและข้อแลกเปลี่ยน: การบันทึกสแต็กเทรซของการจัดสรรทุกรายการต้องใช้การคำนวณที่ซับซ้อนและใช้หน่วยความจำจำนวนมาก ในแอปเวอร์ชันที่ใช้งานจริงขนาดใหญ่ การดำเนินการนี้อาจทำให้แอปใช้งานไม่ได้เลย อย่างไรก็ตาม MemoryLab เป็นแอปพลิเคชันขนาดเล็ก เพียงพอที่เราจะเปิดใช้การติดตามนี้ได้อย่างปลอดภัยเพื่อระบุแหล่งที่มาของการจัดสรร
แบบฝึกหัด: การระบุแหล่งที่มาของอาร์เรย์ไบต์
เริ่มต้นด้วยการติดตาม: บังคับหยุด MemoryLab แล้วรีสตาร์ทด้วยแฟล็ก
--track-allocationเพิ่มความลึกของสแต็กเริ่มต้นเพื่อบันทึกบริบทเพิ่มเติม# Increase the allocation tracker's stack depth (requires a process restart) adb shell setprop dalvik.vm.allocTrackerMaxStack 16 adb shell am force-stop com.android.memorylab adb shell am start --track-allocation -n com.android.memorylab/.MainActivityการดำเนินการ: แตะจัดสรรหน่วยความจำ Java(10 MB) 2-3 ครั้ง
ดัมพ์: สร้างฮีปดัมพ์แล้วดึงออกมา
วิเคราะห์: เปิดการทิ้งข้อมูลใน AHAT ไปที่อินสแตนซ์
byte[]ขนาดใหญ่ (เช่น ตรวจสอบMainActivity→mJavaAllocations(ArrayList) →elementData(Object[]) → องค์ประกอบอาร์เรย์[0])ยืนยัน: ในมุมมองอินสแตนซ์ ให้ดูส่วนเว็บไซต์การจัดสรร โดยจะแสดงสแต็กเทรซแบบเต็มที่นำไปสู่
MainActivity.allocateJava

การค้นหาสตริงที่ซ้ำกันและการขยายขนาดการเติมไฮเดรต
แม้ว่าแอปจะไม่มีการรั่วไหลของ GC-root แบบคลาสสิก แต่ฮีป Java ที่ใช้งานอยู่ก็อาจมีขนาดใหญ่ขึ้น
เนื่องจากมีอินสแตนซ์ java.lang.String ที่ซ้ำกันหลายพันรายการซึ่งสร้างขึ้นระหว่าง
การแยกซีเรียลไลซ์ JSON, Protobuf, Cursor หรือฐานข้อมูล Room ระบบมักจะจัดสรรคีย์ที่ซ้ำกัน
สตริงสถานะ ป้ายกำกับหมวดหมู่ หรือ URL ใหม่ทุกครั้ง
ที่ได้รับการตอบกลับจากเครือข่ายหรือการค้นหาฐานข้อมูล ในแอปฟีด การรับส่งข้อความ และเนื้อหาขนาดใหญ่ สตริงที่ซ้ำกันมักคิดเป็น 30% ถึง 60% ของString
หน่วยความจำที่ใช้งานอยู่
วิธีตรวจสอบสตริงที่ซ้ำกันใน AHAT
- เปิดหน้าการจัดสรร แล้วกรองตาม
java.lang.String - เมื่อเปรียบเทียบ Heap Dump 2 รายการด้วย
--baselineให้ตรวจสอบว่าjava.lang.Stringจำนวนอินสแตนซ์และจำนวนไบต์ทั้งหมดเพิ่มขึ้นอย่างไม่สมส่วน หลังจากไฮเดรตฟีดหรือโหลดแคชในเครื่องหรือไม่ - เรียกดูตารางอินสแตนซ์
java.lang.String(จัดเรียงตามขนาดหรือค่า) เพื่อ ระบุค่าสตริงที่เหมือนกันซึ่งเก็บไว้ในออบเจ็กต์โมเดลในหน่วยความจำหลายรายการ
- วิธีแก้ไข: หลีกเลี่ยงการเรียกใช้
String.intern()โดยไม่เลือกในอินพุตของผู้ใช้หรือเครือข่ายที่กำหนดเอง เนื่องจากตารางภายในของรันไทม์เป็นแบบส่วนกลางและอาจทำให้เกิดการแย่งชิงล็อกหรือเก็บสตริงไว้นานกว่าที่จำเป็น แต่ให้หลีกเลี่ยงการทำซ้ำสตริงโดเมนที่มีความถี่สูงในระหว่างการยกเลิกการซีเรียลไลซ์โดยใช้ แคชการทำซ้ำที่มีขอบเขตและจำกัด (เช่นLruCache<String, String>ภายในตัวแยกวิเคราะห์หรืออะแดปเตอร์) หรือแสดงชุดค่าคงที่เป็น Enum หรือค่าคงที่จำนวนเต็ม
การวิเคราะห์ไดนามิกของหน่วยความจำ Java (โปรไฟล์รวม)
หากต้องการดูภาพรวมพฤติกรรมการใช้หน่วยความจำของแอปพลิเคชัน คุณสามารถรวม ตัวนับหน่วยความจำ กิจกรรมของเธรด และการจัดสรรตาม Callstack เข้าไว้ใน การติดตาม Perfetto รายการเดียว ซึ่งจะช่วยให้คุณเชื่อมโยงเมตริกหน่วยความจำทั่วทั้งระบบ (เช่น RSS และขนาดฮีป) กับการเรียกใช้โค้ดและไซต์การจัดสรรที่เฉพาะเจาะจงได้
เราจะใช้การกำหนดค่าแบบรวมที่เปิดใช้สิ่งต่อไปนี้
- ตัวนับหน่วยความจำ (
linux.process_stats): สำรวจ RSS และเมตริกหน่วยความจำอื่นๆ - ATrace (หมวดหมู่
dalvik,memory,sched): บันทึกสถานะของเธรด และเหตุการณ์ GC - Heapprofd (
android.heapprofd): กำหนดเป้าหมายทั้งฮีปcom.android.art(Java) และlibc.malloc(เนทีฟ) โดยมีการดัมป์อย่างต่อเนื่องทุกๆ 5 วินาที
แบบฝึกหัด: การวิเคราะห์หน่วยความจำแบบรวม
ในแบบฝึกหัดนี้ เราจะเรียกใช้แอป MemoryLab และดำเนินการตามลำดับของ การดำเนินการหน่วยความจำเพื่อสังเกตรูปแบบต่างๆ ในการติดตาม
- Baseline: สถานะไม่ได้ใช้งาน
- Java Churn: การจัดสรรชั่วคราวที่ถูกเก็บขยะทันที
- การจัดสรร Java แบบถาวร: การจัดสรรออบเจ็กต์ Java ที่ยังคงอยู่ใน หน่วยความจำ
- การจัดสรรบิตแมป: การจัดสรรชิ้นงานกราฟิกขนาดใหญ่ (ซึ่งอยู่ในฮีป/หน่วยความจำกราฟิกดั้งเดิม)
- การเรียกคืน: การปล่อยทรัพยากรที่จัดสรรทั้งหมด
1. เปิดตัวและเตรียมพร้อม
บังคับหยุดและรีสตาร์ทแอปเพื่อให้แน่ใจว่าสถานะเป็นปกติ
adb shell am force-stop com.android.memorylab adb shell am start -W -n com.android.memorylab/.MainActivity
2. เริ่มการติดตามและทริกเกอร์ลำดับ
เราจะเริ่มการติดตาม 40 วินาทีและทริกเกอร์เหตุการณ์หน่วยความจำโดยใช้คำสั่ง am
broadcast
เริ่มการติดตาม
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/java_memory.perfetto-trace <<EOF buffers: { size_kb: 131072 fill_policy: RING_BUFFER } data_sources: { config { name: "linux.process_stats" target_buffer: 0 process_stats_config { scan_all_processes_on_start: true proc_stats_poll_ms: 100 } } } data_sources: { config { name: "linux.ftrace" target_buffer: 0 ftrace_config { ftrace_events: "sched/sched_switch" ftrace_events: "task/task_newtask" ftrace_events: "task/task_rename" ftrace_events: "ftrace/print" atrace_categories: "dalvik" atrace_categories: "am" atrace_categories: "res" atrace_categories: "memory" atrace_categories: "sched" atrace_apps: "com.android.memorylab" } } } data_sources: { config { name: "android.heapprofd" target_buffer: 0 heapprofd_config { sampling_interval_bytes: 4096 process_cmdline: "com.android.memorylab" heaps: "libc.malloc" heaps: "com.android.art" shmem_size_bytes: 8388608 block_client: true continuous_dump_config { dump_phase_ms: 1000 dump_interval_ms: 5000 } } } } duration_ms: 40000 EOFทริกเกอร์ลำดับ (เรียกใช้คำสั่งเหล่านี้ในเทอร์มินัลโฮสต์ขณะที่ การติดตามทำงาน โดยให้เป็นไปตามเวลาที่แนะนำ)
# Wait ~5s for trace initialization, then start Java churn: adb shell am broadcast -a com.android.memorylab.CHURN_JAVA # Wait ~10s (at 15s mark), allocate 10MB of persistent Java memory: adb shell am broadcast -a com.android.memorylab.ALLOC_JAVA # Wait ~5s (at 20s mark), allocate 20MB of Bitmaps (native/graphics): adb shell am broadcast -a com.android.memorylab.LEAK_BITMAP # Wait ~10s (at 30s mark), free everything: adb shell am broadcast -a com.android.memorylab.FREE_ALLทางเลือก (เครื่องมือ CLI): คุณยังเริ่มโปรไฟล์ได้โดยใช้สคริปต์
heap_profileโดยตรง ซึ่งกำหนดเป้าหมายทั้งฮีป Java และฮีปแบบเนทีฟด้วย การดัมป์อย่างต่อเนื่องexternal/perfetto/tools/heap_profile -n com.android.memorylab \ --heaps com.android.art,libc.malloc \ -c 5000 \ -d 40000 \ -o java_memory_profile
3. การวิเคราะห์การติดตามที่รวมกัน
เปิด java_memory.perfetto-trace ที่รวบรวมไว้ใน Perfetto
UI
แทร็กที่สำคัญใน Perfetto
ก่อนวิเคราะห์ไทม์ไลน์ ให้ค้นหาแทร็กที่จำเป็นต่อไปนี้สำหรับcom.android.memorylabกระบวนการ
mem.rss.anon(RSS แบบไม่ระบุตัวตน): อยู่ในส่วนหน่วยความจำของกระบวนการ แทร็กนี้จะวัดหน่วยความจำจริง (RAM) ที่ระบบปฏิบัติการจัดสรรให้กับ กระบวนการ ซึ่งแสดงถึงหน่วยความจำที่ใช้จริงHeap size (KB): อยู่ในส่วนความทรงจำด้วย นี่คือตัวนับเฉพาะ Dalvik/ART ที่แสดงพื้นที่ที่อยู่เสมือนที่สงวนไว้สำหรับฮีป Java ซึ่งแสดงถึงขีดจำกัดฮีปภายในของ VM ซึ่ง จะผันผวนเมื่อมีการจัดสรรออบเจ็กต์และเรียกใช้ GCHeapTaskDaemon: อยู่ในรายการเธรดในกระบวนการ เธรดนี้ คือเธรดเบื้องหลังที่ตัวเก็บขยะของ ART ทำงานส่วนใหญ่ กิจกรรมที่นี่บ่งบอกถึงบัตร GC ที่ใช้งานอยู่- การทิ้งการจัดสรรอย่างต่อเนื่อง (heapprofd): แสดงเป็นชิ้นสีตาม ไทม์ไลน์ด้านบน แต่ละชิ้นแสดงถึงระยะเวลา การคลิกส่วนเดียวหรือการเลือกช่วงเวลาจะช่วยให้คุณตรวจสอบ Flamegraph (ในบานหน้าต่างด้านล่าง) สำหรับ com.android.art (การจัดสรร Java) หรือ libc.malloc (การจัดสรรเนทีฟ) เพื่อดูสิ่งที่ได้รับการจัดสรรในช่วงเวลานั้น
การวิเคราะห์ระยะเวลาตามลำดับ
มาตรวจสอบการติดตามตามลำดับเวลาเพื่อดูว่าแทร็กเหล่านี้โต้ตอบกันอย่างไรในระหว่าง แต่ละเฟสของการออกกำลังกาย
ระยะที่ 1: เกณฑ์พื้นฐาน (0-5 วินาที)
- สิ่งที่เกิดขึ้น: แอปไม่ได้ใช้งานและรอรับคำสั่ง
- สถานะการติดตาม
mem.rss.anon: เส้นตรงที่เส้นฐาน (โดยปกติจะอยู่ที่ประมาณ 60-80 MB ขึ้นอยู่กับอุปกรณ์)Heap size (KB): เส้นตรงที่ตรงกับการจัดสรรฮีปของ Java เริ่มต้นHeapTaskDaemon: ไม่ได้ใช้งาน (ไม่มีการแสดงการดำเนินการ)- การส่งออกการจัดสรร: แสดงการจัดสรรพื้นฐานขั้นต่ำ

ระยะที่ 2: การเปลี่ยนแปลงการจัดสรร Java (5-15 วินาที)
- เกิดอะไรขึ้น:
AllocationChurnThreadเริ่มทำงาน โดยจัดสรรอาร์เรย์ขนาด 1 MB ซ้ำๆ และทิ้งอาร์เรย์เหล่านั้น - สถานะการติดตาม
Heap size (KB): แสดงรูปแบบฟันเลื่อยที่เพิ่มขึ้นอย่างรวดเร็ว ขนาดฮีป จะเพิ่มขึ้นเมื่อมีการจัดสรรสะสม และลดลงอย่างรวดเร็วเมื่อ GC ทำงานHeapTaskDaemon: แสดงกิจกรรมที่เกิดขึ้นอย่างต่อเนื่อง โดยมีส่วนการดำเนินการ สอดคล้องกับส่วนที่ลดลงของHeap sizeฟันเลื่อยอย่างสมบูรณ์mem.rss.anon: ติดตามกิจกรรมฮีปของ Java- ฮีปดัมป์การจัดสรรของ Java: การเลือกชิ้นในแทร็กนี้จะแสดงการจัดสรรฮีปของ com.android.art
ตัวอย่างการจัดสรรแสดงให้เห็นว่า AllocationChurnThread เป็นผู้จัดสรรหลัก
โดยการจัดสรรทั้งหมดใช้ Callstack เดียวกันซึ่งชี้ไปยัง Lambda ภายใน
MainActivity.java

ระยะที่ 3: การจัดสรร Java แบบถาวร (15-20 วินาที)
- สิ่งที่เกิดขึ้น: เราจัดสรรออบเจ็กต์ Java ขนาด 10 MB และเก็บการอ้างอิงไว้ใน
mJavaAllocations - สถานะการติดตาม
Heap size (KB): บรรทัดฐานของขั้นบันไดฟันเลื่อยเพิ่มขึ้นประมาณ 10 MBmem.rss.anon: เพิ่มขึ้นประมาณ 10 MB เนื่องจากระบบปฏิบัติการต้องสำรองการจัดสรรแบบถาวรนี้ด้วยหน้าจริงใหม่- ฮีปดัมป์การจัดสรรของ Java: การเลือกชิ้นในแทร็กนี้จะแสดงการจัดสรรฮีปของ com.android.art
- การทิ้งข้อมูลการจัดสรร (Flamegraph): การตรวจสอบฮีป com.android.art
สำหรับการทิ้งข้อมูลที่ดำเนินการในหน้าต่างนี้จะแสดงเส้นทางการจัดสรรใหม่จาก
MainActivity.allocateJavaซึ่งมีส่วนทำให้ขนาดที่เก็บไว้
เลือกตัวอย่างการจัดสรรที่ครอบคลุมระยะเวลาที่ทับซ้อนกับการเพิ่มขึ้น 10 MB
สำหรับการจัดสรรแบบถาวร คุณควรเห็น Callstack การจัดสรรแยกออกเป็น 2 ไซต์ที่แตกต่างกัน โดยไซต์หนึ่งรับผิดชอบการจัดสรรที่มีอายุสั้นแบบเดิมที่เราเห็นก่อนหน้านี้ และอีกไซต์หนึ่งรับผิดชอบการจัดสรรใหม่ที่มีอายุยาว

ระยะที่ 4: การจัดสรรบิตแมป (20-30 วินาที)
- สิ่งที่จะเกิดขึ้น: เราจะจัดสรรบิตแมปขนาด 20 MB ด้วย
- สถานะการติดตาม
Heap size (KB): เหมือนเดิมmem.rss.anon: แสดงการเพิ่มขึ้นอย่างมากประมาณ 20 MB ซึ่งสอดคล้องกับการจัดสรรดั้งเดิมสำหรับข้อมูลพิกเซลของบิตแมป- การทิ้งข้อมูลการจัดสรร (Flamegraph): คราวนี้ให้โฟกัสที่สไลซ์สำหรับฮีปของ libc.malloc (เนทีฟ)
Callstack การจัดสรรแบบเนทีฟ
จะแสดงการจัดสรรบิตแมปที่มาจากไลบรารีกราฟิกแบบเนทีฟ
นี่เป็นกรณีการใช้งานที่ดีสำหรับการติดตามการจัดสรรหน่วยความจำของระบบ เนื่องจากคุณจะไม่เห็นการจัดสรรบิตแมปเหล่านี้ในฮีปของ Java

ระยะที่ 5: การฟื้นฟู (30-40 ปี)
- สิ่งที่เกิดขึ้น: เราจะทริกเกอร์
FREE_ALLโดยล้างการอ้างอิงถึงการจัดสรร Java และบิตแมปแบบถาวรทั้งหมด จากนั้นจะเรียกใช้System.gc()อย่างชัดเจน - สถานะการติดตาม
Heap size (KB): ลดลงกลับไปที่ระดับพื้นฐานmem.rss.anon: ลดลงอีกครั้ง ซึ่งแสดงให้เห็นว่าระบบปฏิบัติการกำลังเรียกคืนหน้าจริงHeapTaskDaemon: แสดงกิจกรรมที่เกิดขึ้นในช่วงสุดท้ายขณะที่ประมวลผลระบบจัดการหน่วยความจำที่ไม่ใช้แล้ว

การตรวจสอบ OOM ในอดีต (ApplicationExitInfo)
การจับ LMK ในขณะที่เกิดขึ้นเหมาะสำหรับการแก้ไขข้อบกพร่องที่ใช้งานอยู่ แต่สำหรับการวัดระยะทางในฟิลด์ คุณสามารถใช้ ApplicationExitInfo API ได้ ซึ่งจะช่วยให้แอปของคุณ
ค้นพบสาเหตุที่แอปถูกปิดในเซสชันก่อนหน้า
ActivityManager am = getSystemService(ActivityManager.class);
List<ApplicationExitInfo> exitReasons = am.getHistoricalProcessExitReasons(null, 0, 1);
if (!exitReasons.isEmpty()) {
ApplicationExitInfo info = exitReasons.get(0);
if (info.getReason() == ApplicationExitInfo.REASON_LOW_MEMORY) {
// App was killed by the system Low Memory Killer
}
}
แนวทางปฏิบัติแนะนำ
- Baseline First: ให้ใช้ ฮีปดัมป์ "Baseline" เสมอหลังจากที่แอป เริ่มต้นแล้ว แต่ก่อนที่จะดำเนินการที่คุณกำลังทดสอบ
- ใช้หน้าการรั่วไหลของกิจกรรมของ AHAT: AHAT มีหน้าการรั่วไหลของกิจกรรมโดยเฉพาะ ซึ่งจะระบุอินสแตนซ์ของกิจกรรมที่ถูกทำลายแล้วแต่ยังคงอยู่ในหน่วยความจำโดยอัตโนมัติ วิธีนี้มักเป็นวิธีที่เร็วที่สุดในการ ค้นหารอยรั่วที่พบบ่อย
- ตรวจสอบเส้นทางไปยังรูทของ GC: สำหรับออบเจ็กต์ที่รั่วไหล ให้ใช้มุมมองเส้นทางจาก รูทใน AHAT เพื่อทำความเข้าใจว่าการอ้างอิงใดที่ทำให้ออบเจ็กต์ยังคง ใช้งานได้ (เช่น ฟิลด์แบบคงที่ เธรดที่ทำงานเป็นเวลานาน หรือ Listener ที่ลงทะเบียน)
← เครื่องมือ | ↑ ขึ้น | บิตแมป →