การวิเคราะห์หน่วยความจำ Java

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

แนวคิดหลัก

รูทของ GC

GC Root เป็นออบเจ็กต์ประเภทพิเศษที่ตัวเก็บขยะจะถือว่า เข้าถึงได้เสมอ ตัวอย่างเช่น

  • เธรดที่ใช้งานอยู่ (และออบเจ็กต์ที่อ้างอิงจากเฟรมสแต็ก Java ที่กำลังดำเนินการอยู่)
  • ชั้นเรียนที่มีวิธีการที่ใช้งานอยู่
  • การอ้างอิง JNI (การอ้างอิงส่วนกลางหรือการอ้างอิงในเครื่องที่โค้ดแบบเนทีฟถือครอง)

เส้นทางไปยังรูท GC

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

เส้นทางไปยังรูท GC

ต้นไม้ที่โดดเด่น

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

ออบเจ็กต์ A จะครอบงำออบเจ็กต์ B หากทุกเส้นทางจากรูท GC ใดๆ ไปยัง B ต้องผ่าน A หาก A ครอบงำ B การเรียกคืน A จะรับประกันด้วยว่าสามารถเรียกคืน B ได้ เนื่องจากไม่มี เส้นทางอื่นๆ จากรูทไปยัง B

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

แผนผัง Dominator

การขอฮีปดัมป์ของ 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) ในมุมมองการจัดสรร

มุมมอง AHAT ที่แสดงอินสแตนซ์

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

มุมมอง AHAT ที่แสดงอินสแตนซ์ MainActivity
คลิกอินสแตนซ์ MainActivityเพื่อตรวจสอบ

AHAT แสดงรายละเอียดอินสแตนซ์

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

เส้นทางตัวอย่าง AHAT จากรูท GC และขนาดออบเจ็กต์

กำลังวิเคราะห์บิตแมป

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

ตัวอย่างบิตแมป AHAT

หน้าการรั่วไหลของกิจกรรม

AHAT มีมุมมองเฉพาะสำหรับการระบุกิจกรรมที่รั่วไหล ซึ่งเป็นหนึ่งในปัญหาหน่วยความจำรั่วที่พบบ่อยและส่งผลกระทบมากที่สุดใน Android

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

หน้าการรั่วไหลของกิจกรรม AHAT

การเปรียบเทียบฮีปดัมป์

การเปรียบเทียบ Heap Dump 2 รายการเป็นวิธีที่มีประสิทธิภาพมากที่สุดวิธีหนึ่งในการระบุปัญหาเกี่ยวกับหน่วยความจำ การเปรียบเทียบการทิ้งข้อมูลพื้นฐานที่ "สะอาด" กับการทิ้งข้อมูลที่ดำเนินการหลังจากดำเนินการบางอย่าง จะช่วยให้คุณเห็นได้ทันทีว่าออบเจ็กต์ใดสะสมอยู่

แบบฝึกหัด: การระบุการรั่วไหลผ่านการเปรียบเทียบ

  1. พื้นฐาน: เปิด MemoryLab แล้วทำการฮีปดัมป์พื้นฐาน

    adb shell am dumpheap com.android.memorylab /data/local/tmp/base.hprof
    adb pull /data/local/tmp/base.hprof .
    
  2. การดำเนินการ: แตะจัดสรรหน่วยความจำ Java(10 MB) หลายครั้งในแอป

  3. สุดท้าย: สร้างฮีปดัมป์ครั้งที่ 2

    adb shell am dumpheap com.android.memorylab /data/local/tmp/leaked.hprof
    adb pull /data/local/tmp/leaked.hprof .
    
  4. เปรียบเทียบ: เริ่ม AHAT โดยใช้การทิ้งข้อมูลครั้งที่ 2 เป็นข้อมูลหลัก และครั้งแรกเป็น ข้อมูลพื้นฐาน

    java -jar out/host/linux-x86/framework/ahat.jar leaked.hprof --baseline base.hprof
    
  5. ภาพรวมการวิเคราะห์: ตอนนี้หน้าภาพรวมมีคอลัมน์ Δ (เดลต้า) แล้ว คุณจะเห็นเดลต้าบวกขนาดใหญ่สำหรับฮีป app ซึ่งบ่งบอกถึง การเติบโตของหน่วยความจำอย่างมาก

ภาพรวม AHAT พร้อมส่วนต่าง

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

มุมมอง AHAT ที่รูทพร้อมส่วนต่าง

การบันทึกสแต็กเทรซการจัดสรร

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

แนวคิดและข้อแลกเปลี่ยน: การบันทึกสแต็กเทรซของการจัดสรรทุกรายการต้องใช้การคำนวณที่ซับซ้อนและใช้หน่วยความจำจำนวนมาก ในแอปเวอร์ชันที่ใช้งานจริงขนาดใหญ่ การดำเนินการนี้อาจทำให้แอปใช้งานไม่ได้เลย อย่างไรก็ตาม MemoryLab เป็นแอปพลิเคชันขนาดเล็ก เพียงพอที่เราจะเปิดใช้การติดตามนี้ได้อย่างปลอดภัยเพื่อระบุแหล่งที่มาของการจัดสรร

แบบฝึกหัด: การระบุแหล่งที่มาของอาร์เรย์ไบต์

  1. เริ่มต้นด้วยการติดตาม: บังคับหยุด 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
    
  2. การดำเนินการ: แตะจัดสรรหน่วยความจำ Java(10 MB) 2-3 ครั้ง

  3. ดัมพ์: สร้างฮีปดัมพ์แล้วดึงออกมา

  4. วิเคราะห์: เปิดการทิ้งข้อมูลใน AHAT ไปที่อินสแตนซ์ byte[] ขนาดใหญ่ (เช่น ตรวจสอบ MainActivity → mJavaAllocations (ArrayList) → elementData (Object[]) → องค์ประกอบอาร์เรย์ [0])

  5. ยืนยัน: ในมุมมองอินสแตนซ์ ให้ดูส่วนเว็บไซต์การจัดสรร โดยจะแสดงสแต็กเทรซแบบเต็มที่นำไปสู่ MainActivity.allocateJava

เว็บไซต์การจัดสรร AHAT

การค้นหาสตริงที่ซ้ำกันและการขยายขนาดการเติมไฮเดรต

แม้ว่าแอปจะไม่มีการรั่วไหลของ GC-root แบบคลาสสิก แต่ฮีป Java ที่ใช้งานอยู่ก็อาจมีขนาดใหญ่ขึ้น เนื่องจากมีอินสแตนซ์ java.lang.String ที่ซ้ำกันหลายพันรายการซึ่งสร้างขึ้นระหว่าง การแยกซีเรียลไลซ์ JSON, Protobuf, Cursor หรือฐานข้อมูล Room ระบบมักจะจัดสรรคีย์ที่ซ้ำกัน สตริงสถานะ ป้ายกำกับหมวดหมู่ หรือ URL ใหม่ทุกครั้ง ที่ได้รับการตอบกลับจากเครือข่ายหรือการค้นหาฐานข้อมูล ในแอปฟีด การรับส่งข้อความ และเนื้อหาขนาดใหญ่ สตริงที่ซ้ำกันมักคิดเป็น 30% ถึง 60% ของString หน่วยความจำที่ใช้งานอยู่

วิธีตรวจสอบสตริงที่ซ้ำกันใน AHAT

  1. เปิดหน้าการจัดสรร แล้วกรองตาม java.lang.String
  2. เมื่อเปรียบเทียบ Heap Dump 2 รายการด้วย --baseline ให้ตรวจสอบว่า java.lang.Stringจำนวนอินสแตนซ์และจำนวนไบต์ทั้งหมดเพิ่มขึ้นอย่างไม่สมส่วน หลังจากไฮเดรตฟีดหรือโหลดแคชในเครื่องหรือไม่
  3. เรียกดูตารางอินสแตนซ์ 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 และดำเนินการตามลำดับของ การดำเนินการหน่วยความจำเพื่อสังเกตรูปแบบต่างๆ ในการติดตาม

  1. Baseline: สถานะไม่ได้ใช้งาน
  2. Java Churn: การจัดสรรชั่วคราวที่ถูกเก็บขยะทันที
  3. การจัดสรร Java แบบถาวร: การจัดสรรออบเจ็กต์ Java ที่ยังคงอยู่ใน หน่วยความจำ
  4. การจัดสรรบิตแมป: การจัดสรรชิ้นงานกราฟิกขนาดใหญ่ (ซึ่งอยู่ในฮีป/หน่วยความจำกราฟิกดั้งเดิม)
  5. การเรียกคืน: การปล่อยทรัพยากรที่จัดสรรทั้งหมด

1. เปิดตัวและเตรียมพร้อม

  1. บังคับหยุดและรีสตาร์ทแอปเพื่อให้แน่ใจว่าสถานะเป็นปกติ

    adb shell am force-stop com.android.memorylab
    adb shell am start -W -n com.android.memorylab/.MainActivity
    

2. เริ่มการติดตามและทริกเกอร์ลำดับ

เราจะเริ่มการติดตาม 40 วินาทีและทริกเกอร์เหตุการณ์หน่วยความจำโดยใช้คำสั่ง am broadcast

  1. เริ่มการติดตาม

    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
    
  2. ทริกเกอร์ลำดับ (เรียกใช้คำสั่งเหล่านี้ในเทอร์มินัลโฮสต์ขณะที่ การติดตามทำงาน โดยให้เป็นไปตามเวลาที่แนะนำ)

    # 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
    
  3. ทางเลือก (เครื่องมือ 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กระบวนการ

  1. mem.rss.anon (RSS แบบไม่ระบุตัวตน): อยู่ในส่วนหน่วยความจำของกระบวนการ แทร็กนี้จะวัดหน่วยความจำจริง (RAM) ที่ระบบปฏิบัติการจัดสรรให้กับ กระบวนการ ซึ่งแสดงถึงหน่วยความจำที่ใช้จริง
  2. Heap size (KB): อยู่ในส่วนความทรงจำด้วย นี่คือตัวนับเฉพาะ Dalvik/ART ที่แสดงพื้นที่ที่อยู่เสมือนที่สงวนไว้สำหรับฮีป Java ซึ่งแสดงถึงขีดจำกัดฮีปภายในของ VM ซึ่ง จะผันผวนเมื่อมีการจัดสรรออบเจ็กต์และเรียกใช้ GC
  3. HeapTaskDaemon: อยู่ในรายการเธรดในกระบวนการ เธรดนี้ คือเธรดเบื้องหลังที่ตัวเก็บขยะของ ART ทำงานส่วนใหญ่ กิจกรรมที่นี่บ่งบอกถึงบัตร GC ที่ใช้งานอยู่
  4. การทิ้งการจัดสรรอย่างต่อเนื่อง (heapprofd): แสดงเป็นชิ้นสีตาม ไทม์ไลน์ด้านบน แต่ละชิ้นแสดงถึงระยะเวลา การคลิกส่วนเดียวหรือการเลือกช่วงเวลาจะช่วยให้คุณตรวจสอบ Flamegraph (ในบานหน้าต่างด้านล่าง) สำหรับ com.android.art (การจัดสรร Java) หรือ libc.malloc (การจัดสรรเนทีฟ) เพื่อดูสิ่งที่ได้รับการจัดสรรในช่วงเวลานั้น

การวิเคราะห์ระยะเวลาตามลำดับ

มาตรวจสอบการติดตามตามลำดับเวลาเพื่อดูว่าแทร็กเหล่านี้โต้ตอบกันอย่างไรในระหว่าง แต่ละเฟสของการออกกำลังกาย

ระยะที่ 1: เกณฑ์พื้นฐาน (0-5 วินาที)
  • สิ่งที่เกิดขึ้น: แอปไม่ได้ใช้งานและรอรับคำสั่ง
  • สถานะการติดตาม
    • mem.rss.anon: เส้นตรงที่เส้นฐาน (โดยปกติจะอยู่ที่ประมาณ 60-80 MB ขึ้นอยู่กับอุปกรณ์)
    • Heap size (KB): เส้นตรงที่ตรงกับการจัดสรรฮีปของ Java เริ่มต้น
    • HeapTaskDaemon: ไม่ได้ใช้งาน (ไม่มีการแสดงการดำเนินการ)
    • การส่งออกการจัดสรร: แสดงการจัดสรรพื้นฐานขั้นต่ำ

UI ของ Perfetto แสดงข้อมูลพื้นฐานของระยะที่ 1

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

UI ของ Perfetto แสดงการเลิกใช้งานในระยะที่ 2 ตัวอย่างการจัดสรรแสดงให้เห็นว่า AllocationChurnThread เป็นผู้จัดสรรหลัก โดยการจัดสรรทั้งหมดใช้ Callstack เดียวกันซึ่งชี้ไปยัง Lambda ภายใน MainActivity.java

UI ของ Perfetto แสดงการจัดสรร Java ในระยะที่ 2

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

UI ของ Perfetto แสดงการจัดสรร Java แบบถาวรในระยะที่ 3
เลือกตัวอย่างการจัดสรรที่ครอบคลุมระยะเวลาที่ทับซ้อนกับการเพิ่มขึ้น 10 MB สำหรับการจัดสรรแบบถาวร คุณควรเห็น Callstack การจัดสรรแยกออกเป็น 2 ไซต์ที่แตกต่างกัน โดยไซต์หนึ่งรับผิดชอบการจัดสรรที่มีอายุสั้นแบบเดิมที่เราเห็นก่อนหน้านี้ และอีกไซต์หนึ่งรับผิดชอบการจัดสรรใหม่ที่มีอายุยาว

UI ของ Perfetto แสดงการจัดสรร Java ในระยะที่ 3

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

UI ของ Perfetto แสดงการจัดสรรบิตแมปในเฟส 4
Callstack การจัดสรรแบบเนทีฟ จะแสดงการจัดสรรบิตแมปที่มาจากไลบรารีกราฟิกแบบเนทีฟ นี่เป็นกรณีการใช้งานที่ดีสำหรับการติดตามการจัดสรรหน่วยความจำของระบบ เนื่องจากคุณจะไม่เห็นการจัดสรรบิตแมปเหล่านี้ในฮีปของ Java

UI ของ Perfetto แสดงการจัดสรรเนทีฟในเฟส 4

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

UI ของ Perfetto แสดงการเรียกคืนเฟส 5

การตรวจสอบ 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
    }
}

แนวทางปฏิบัติแนะนำ

  1. Baseline First: ให้ใช้ ฮีปดัมป์ "Baseline" เสมอหลังจากที่แอป เริ่มต้นแล้ว แต่ก่อนที่จะดำเนินการที่คุณกำลังทดสอบ
  2. ใช้หน้าการรั่วไหลของกิจกรรมของ AHAT: AHAT มีหน้าการรั่วไหลของกิจกรรมโดยเฉพาะ ซึ่งจะระบุอินสแตนซ์ของกิจกรรมที่ถูกทำลายแล้วแต่ยังคงอยู่ในหน่วยความจำโดยอัตโนมัติ วิธีนี้มักเป็นวิธีที่เร็วที่สุดในการ ค้นหารอยรั่วที่พบบ่อย
  3. ตรวจสอบเส้นทางไปยังรูทของ GC: สำหรับออบเจ็กต์ที่รั่วไหล ให้ใช้มุมมองเส้นทางจาก รูทใน AHAT เพื่อทำความเข้าใจว่าการอ้างอิงใดที่ทำให้ออบเจ็กต์ยังคง ใช้งานได้ (เช่น ฟิลด์แบบคงที่ เธรดที่ทำงานเป็นเวลานาน หรือ Listener ที่ลงทะเบียน)

← เครื่องมือ | ↑ ขึ้น | บิตแมป →