برنامههای جاوا و کاتلین حافظه را از طریق یک هیپ جمعآوری زباله مدیریت میکنند. وقتی اشیاء دیگر قابل دسترسی نباشند، جمعآوری زباله (GC) در نهایت فضای آنها را پس میگیرد. نشت حافظه زمانی رخ میدهد که اشیاء که دیگر مورد نیاز نیستند هنوز توسط "ریشههای GC" نگهداری میشوند و از بازپسگیری آنها جلوگیری میشود.
مفاهیم اصلی
ریشههای GC
ریشه GC نوع خاصی از شیء است که زبالهروب آن را همیشه قابل دسترسی میداند. مثالها عبارتند از:
- نخهای فعال (و اشیایی که از فریمهای پشته جاوای در حال اجرای آنها ارجاع داده میشوند).
- کلاسهایی با متدهای اجرای فعال.
- ارجاعات JNI (ارجاعات سراسری یا محلی که توسط کد بومی نگهداری میشوند).
مسیر به ریشه GC
تا زمانی که زنجیرهای از ارجاعات از یک ریشه GC به یک شیء وجود داشته باشد، آن شیء "قابل دسترسی" است و نمیتواند جمعآوری زباله شود. این زنجیره، مسیر به ریشه GC نامیده میشود. برای رفع نشت حافظه، باید این زنجیره را شناسایی و بشکنید.

درختان سلطهگر
اگرچه مسیر ریشه GC به شما میگوید که چرا یک شیء زنده است، اما به شما نمیگوید که در صورت خراب شدن آن مرجع، چه مقدار از حافظه بازیابی میشود. برای این کار، ما از Dominator Trees استفاده میکنیم.
گفته میشود شیء A بر شیء B تسلط دارد اگر هر مسیری از هر ریشه GC به B باید از A عبور کند. اگر A بر B تسلط داشته باشد، پس بازپسگیری A تضمین میکند که B نیز میتواند بازپس گرفته شود، زیرا هیچ مسیر دیگری از هیچ ریشهای به B وجود ندارد.
نمودار زیر یک گراف شیء و درخت غالب متناظر با آن را نشان میدهد. توجه کنید که چگونه در این گراف، A و B به شیء D میرسند، بنابراین نه A و نه B بر D غالب نیستند؛ در عوض، ریشه GC نزدیکترین غالب به آن است.

به دست آوردن دادههای هیپ (heap dump) جاوا
یک heap dump تصویری از تمام اشیاء موجود در heap جاوا در یک نقطه زمانی خاص است.
استفاده از ADB
برای گرفتن یک فایل heap dump از یک فرآیند در حال اجرا، میتوانید نام بسته را مستقیماً به 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 همچنین میتواند با فعال کردن منبع داده 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
ببینید: دادههای هیپ جاوا در مستندات Perfetto .
تجزیه و تحلیل با AHAT
ابزار AHAT (ابزار تحلیل هیپ اندروید) ابزار پیشنهادی برای مشاهده فایلهای .hprof در مرورگر وب است.
شروع AHAT
اگر ahat را در مسیر خود نصب کردهاید، آن را با دستور زیر اجرا کنید:
ahat heap.hprof
یا فایل jar مستقل را اجرا کنید:
java -jar ahat.jar heap.hprof
سپس مرورگر خود را باز کنید و به آدرس http://localhost:7100 بروید .
برای جزئیات بیشتر در مورد به دست آوردن یا ساخت AHAT، به مخزن منبع AHAT مراجعه کنید.
گردشهای کاری تحلیل کلیدی
پیدا کردن نشتیها
در نمای Allocations، کلاس Activity خود ( MainActivity ) را جستجو کنید.

برای یافتن همه نمونهها ، روی کلاس کلیک کنید.
برای بررسی نمونهی MainActivity روی آن کلیک کنید.

در نمای نمونه، میتوانید مسیر نمونه از GC Root را پیدا کنید، که زنجیره ارجاعاتی را نشان میدهد که مانع از جمعآوری زباله توسط شیء میشود، و اندازه شیء ، که نشان میدهد این نمونه خاص چه مقدار حافظه را حفظ میکند.

تجزیه و تحلیل بیتمپها
AHAT پشتیبانی ویژهای برای مشاهده اشیاء android.graphics.Bitmap دارد که اغلب مصرفکنندگان حافظه زیادی هستند. برای دیدن پیشنمایش رندر شده از محتوای آن، روی یک نمونه Bitmap کلیک کنید.

صفحه نشت فعالیت
AHAT یک دیدگاه تخصصی برای شناسایی فعالیتهای نشتشده دارد که یکی از رایجترین و تأثیرگذارترین نشتهای حافظه در اندروید هستند.
- اقدام : در MemoryLab، روی گزینهی Leak an Activity (نشت یک فعالیت) ضربه بزنید. این کار
LeakedActivityرا اجرا میکند که عمداً خودش را نشت میدهد. - تخلیه : یک تخلیه هیپ انجام دهید.
- تحلیل : روی Activity Leaks در نوار کناری AHAT کلیک کنید.
- تأیید : AHAT،
com.android.memorylab.LeakedActivityبه عنوان نشتشده فهرست میکند، زیرا فیلدmDestroyedآن درست است (نشان میدهد که چرخه حیات Activity به پایان رسیده است) اما هنوز از یک ریشه GC قابل دسترسی است.

انواع مختلف هیپ دامپ
مقایسه دو هیپ دامپ یکی از قدرتمندترین راهها برای شناسایی مشکلات حافظه است. با مقایسه یک بیس لاین دامپ «تمیز» با دامپی که پس از انجام برخی اقدامات گرفته شده است، میتوانید بلافاصله ببینید کدام اشیاء انباشته شدهاند.
تمرین: شناسایی نشتیها از طریق تفاوتگذاری
خط پایه : MemoryLab را اجرا کنید و یک هیپ دامپ خط پایه بگیرید:
adb shell am dumpheap com.android.memorylab /data/local/tmp/base.hprof adb pull /data/local/tmp/base.hprof .اقدام : چندین بار در برنامه روی گزینه «اختصاص حافظه جاوا (10 مگابایت)» ضربه بزنید.
نهایی : یک دامپ هیپ دوم بگیرید:
adb shell am dumpheap com.android.memorylab /data/local/tmp/leaked.hprof adb pull /data/local/tmp/leaked.hprof .مقایسه : AHAT را با دومین dump به عنوان primary و اولین dump به عنوان baseline شروع کنید:
java -jar out/host/linux-x86/framework/ahat.jar leaked.hprof --baseline base.hprofتحلیل نمای کلی : صفحه نمای کلی اکنون شامل یک ستون Δ (دلتا) است. شما یک دلتای مثبت بزرگ برای پشته
appخواهید دید که نشان دهنده رشد قابل توجه حافظه است.

- بررسی دقیق : در منو روی root کلیک کنید. این صفحه اشیاء قابل دسترسی از ریشههای GC را نشان میدهد که بر اساس اندازه حفظ شدهشان مرتب شدهاند. در بالا
MainActivityبا یک دلتای مثبت بزرگ خواهید دید.

ضبط ردپاهای تخصیص پشته
در حالی که مسیر نمونه از GC Root به شما میگوید که چرا یک شیء هنوز زنده است، اما به شما نمیگوید که چگونه ایجاد شده است. ردیابیهای پشته تخصیص، خط دقیق کدی را که یک شیء را تخصیص داده است، ارائه میدهند.
مفهوم و بدهبستانها : ثبت رد پشته هر تخصیص از نظر محاسباتی پرهزینه است و حافظه قابل توجهی مصرف میکند. در یک برنامه تولیدی بزرگ، این میتواند برنامه را تقریباً غیرقابل استفاده کند. با این حال، 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اقدام : چند بار روی «اختصاص حافظه جاوا (10 مگابایت)» ضربه بزنید.
تخلیه : یک توده تخلیه بردارید و آن را بکشید.
تحلیل : فایل dump را در AHAT باز کنید. به یک نمونه
byte[]بزرگ بروید. (مثلاً، بررسی کنیدMainActivity→mJavaAllocations(ArrayList) →elementData(Object[]) → array element[0]).تأیید : در نمای نمونه، به بخش Allocation Site نگاه کنید. این بخش، ردیابی کامل پشته منتهی به
MainActivity.allocateJavaرا نشان میدهد.

تحلیل دینامیک حافظه جاوا (پروفایل ترکیبی)
برای به دست آوردن تصویر کاملی از رفتار حافظه یک برنامه، میتوانید شمارندههای حافظه، فعالیت نخها و پروفایل تخصیص مبتنی بر callstack را در یک ردیابی Perfetto واحد ترکیب کنید. این به شما امکان میدهد معیارهای حافظه در سطح سیستم (مانند RSS و اندازه پشته) را با سایتهای اجرای کد و تخصیص خاص مرتبط کنید.
ما از یک پیکربندی ترکیبی استفاده خواهیم کرد که موارد زیر را فعال میکند:
- شمارندههای حافظه (
linux.process_stats): نظرسنجیهای RSS و سایر معیارهای حافظه. - ATrace (
dalvik،memory،schedcategories): حالتهای نخ و رویدادهای GC را ثبت میکند. - Heapprofd (
android.heapprofd): هر دو هیپcom.android.art(جاوا) وlibc.malloc(بومی) را با تخلیه مداوم دادهها هر ۵ ثانیه یک بار هدف قرار میدهد.
تمرین: تحلیل حافظه ترکیبی
در این تمرین، برنامه MemoryLab را اجرا خواهیم کرد و یک توالی از عملیات حافظه را برای مشاهده الگوهای مختلف در ردیابی انجام خواهیم داد:
- حالت پایه : حالت بیکار.
- Java Churn : تخصیصهای موقتی که بلافاصله زبالهروب میشوند.
- تخصیص پایدار جاوا : تخصیص اشیاء جاوا که در حافظه باقی میمانند.
- تخصیص بیتمپ : تخصیص منابع گرافیکی بزرگ (که در حافظه هیپ/گرافیک بومی قرار دارند).
- بازپسگیری : آزاد کردن تمام منابع اختصاص داده شده.
۱. راهاندازی و آمادهسازی
برای اطمینان از پاک شدن برنامه، آن را متوقف کرده و مجدداً راهاندازی کنید:
adb shell am force-stop com.android.memorylab adb shell am start -W -n com.android.memorylab/.MainActivity
۲. شروع ردیابی و اجرای توالی
ما یک ردیابی ۴۰ ثانیهای را شروع میکنیم و رویدادهای حافظه را با استفاده از دستورات am broadcast فعال میکنیم.
ردیابی را شروع کنید :
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/java_memory.perfetto-trace <<EOF buffers: { size_kb: 65536 fill_policy: RING_BUFFER } data_sources: { config { name: "android.java_hprof" java_hprof_config { process_cmdline: "com.example.myapp" } } } duration_ms: 10000 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شروع کنید و هم هیپهای جاوا و هم هیپهای بومی را با دامپهای مداوم هدف قرار دهید:external/perfetto/tools/heap_profile -n com.android.memorylab \ --heaps com.android.art,libc.malloc \ -c 5000 \ -d 40000 \ -o java_memory_profile
۳. تحلیل رد ترکیبی
فایل java_memory.perfetto-trace جمعآوریشده را در رابط کاربری Perfetto باز کنید.
آهنگهای کلیدی در پرفتو
قبل از تجزیه و تحلیل جدول زمانی، این مسیرهای ضروری را برای فرآیند com.android.memorylab پیدا کنید:
-
mem.rss.anon(RSS ناشناس) : در بخش حافظه فرآیند یافت میشود. این مسیر، حافظه فیزیکی (RAM) اختصاص داده شده به فرآیند توسط سیستم عامل را اندازهگیری میکند. این مقدار، میزان واقعی اشغال حافظه را نشان میدهد. -
Heap size (KB): همچنین در بخش حافظه قرار دارد. این یک شمارنده مخصوص Dalvik/ART است که فضای آدرس مجازی رزرو شده برای هیپ جاوا را نشان میدهد. این شمارنده محدودیت هیپ داخلی ماشین مجازی را نشان میدهد که با اختصاص اشیاء و اجرای GC تغییر میکند. -
HeapTaskDaemon: در لیست نخهای زیر فرآیند یافت میشود. این نخ پسزمینهای است که زبالهروب ART بیشتر کار خود را در آن انجام میدهد. فعالیت در اینجا نشاندهندهی پاسهای فعال GC است. - تخلیههای تخصیص پیوسته (heapprofd) : به صورت برشهای رنگی در امتداد خط زمانی بالا نشان داده شده است. هر برش نشان دهنده یک مدت زمان است. کلیک بر روی یک برش یا انتخاب یک محدوده زمانی به شما امکان میدهد تا Flamegraph (در پنل پایین) را برای com.android.art (تخصیصهای جاوا) یا libc.malloc (تخصیصهای بومی) بررسی کنید تا ببینید در آن دوره چه چیزی اختصاص داده شده است.
تحلیل فاز زمانی
بیایید این رد را به ترتیب زمانی بررسی کنیم تا ببینیم این ردها چگونه در هر مرحله از تمرین با هم تعامل دارند.
مرحله ۱: خط پایه (۰ تا ۵ ثانیه)
- چه اتفاقی میافتد : برنامه غیرفعال است و منتظر دستورات است.
- وضعیت آهنگ :
-
mem.rss.anon: خط ثابت در خط پایه (معمولاً حدود ۶۰ تا ۸۰ مگابایت بسته به دستگاه). -
Heap size (KB): خط صاف، مطابق با تخصیص اولیه هیپ جاوا. -
HeapTaskDaemon: بیکار (هیچ برشی اجرا را نشان نمیدهد). - تخلیههای تخصیص : حداقل تخصیصهای پایه را نشان میدهد.
-

مرحله ۲: کاهش تخصیص جاوا (۵ تا ۱۵ ثانیه)
- چه اتفاقی میافتد :
AllocationChurnThreadشروع به کار میکند و به طور مکرر آرایههای ۱ مگابایتی را تخصیص داده و آنها را دور میریزد. - وضعیت آهنگ :
-
Heap size (KB): یک الگوی دندانه ارهای سریع را نشان میدهد. اندازه هیپ با تجمع تخصیصها افزایش مییابد و با اجرای GC به شدت کاهش مییابد. -
HeapTaskDaemon: فعالیت تقریباً ثابتی را نشان میدهد، به طوری که برشهای اجرایی کاملاً با قطرات در دندانه ارهایHeap sizeهمتراز میشوند. -
mem.rss.anon: فعالیت حافظه هیپ جاوا را ردیابی میکند. - تخلیههای تخصیص هیپ جاوا : انتخاب برشها در این مسیر، تخصیصهای هیپ com.android.art را نشان میدهد.
-
نمونههای تخصیص AllocationChurnThread به عنوان تخصیصدهنده اصلی نشان میدهند، و همه تخصیصها از یک callstack مشترک استفاده میکنند که به لامبدا درون MainActivity.java اشاره میکند.

مرحله ۳: تخصیص مداوم جاوا (۱۵ تا ۲۰ ثانیه)
- چه اتفاقی میافتد : ما 10 مگابایت از اشیاء جاوا را اختصاص میدهیم و در
mJavaAllocationsبه آنها ارجاع میدهیم. - وضعیت آهنگ :
-
Heap size (KB): اندازه پایه دندانه ارهای تقریباً 10 مگابایت افزایش مییابد. -
mem.rss.anon: تقریباً ۱۰ مگابایت افزایش مییابد، زیرا سیستمعامل باید این تخصیص پایدار را با صفحات فیزیکی جدید پشتیبانی کند. - تخلیههای تخصیص هیپ جاوا : انتخاب برشها در این مسیر، تخصیصهای هیپ com.android.art را نشان میدهد.
- تخلیههای تخصیص (Flamegraph) : بررسی هیپ com.android.art برای تخلیهای که در این پنجره انجام شده است، یک مسیر تخصیص جدید از
MainActivity.allocateJavaرا نشان میدهد که در اندازه حفظ شده نقش دارد.
-
یک نمونه تخصیص را انتخاب کنید که مدت زمانی را پوشش دهد که با افزایش ۱۰ مگابایتی برای تخصیص پایدار همپوشانی داشته باشد. باید ببینید که callstack های تخصیص به دو سایت مختلف واگرا میشوند، یکی مسئول همان ریزش تخصیص کوتاه مدت که قبلاً دیدیم و دیگری مسئول تخصیص طولانی مدت جدید است.

مرحله ۴: تخصیص بیتمپ (۲۰ تا ۳۰ ثانیه)
- چه اتفاقی میافتد : ما همچنین 20 مگابایت Bitmaps اختصاص میدهیم.
- وضعیت آهنگ :
-
Heap size (KB): مانند قبل. -
mem.rss.anon: افزایش قابل توجهی در حدود ۲۰ مگابایت را نشان میدهد که مربوط به تخصیصهای بومی برای دادههای پیکسلی Bitmap است. - تخلیههای تخصیص (Flamegraph) : این بار روی برشهای مربوط به هیپ libc.malloc (بومی) تمرکز کنید.
-
پشتههای فراخوانی تخصیص بومی، تخصیص Bitmap را که از کتابخانههای گرافیکی بومی سرچشمه میگیرد، آشکار میکنند. این یک مورد استفاده خوب برای ردیابی تخصیص بومی است، زیرا این تخصیصهای Bitmap را در پشته جاوا مشاهده نخواهید کرد.

مرحله ۵: احیا (۳۰ تا ۴۰ سال)
- چه اتفاقی میافتد : ما
FREE_ALLفعال میکنیم، که ارجاعات به تمام تخصیصها و بیتمپهای پایدار جاوا را پاک میکند، و به دنبال آن یکSystem.gc()صریح اجرا میشود. - وضعیت آهنگ :
-
Heap size (KB): به سطح پایه کاهش مییابد. -
mem.rss.anon: به پایین برمیگردد و نشان میدهد که سیستمعامل صفحات فیزیکی را بازیابی میکند. -
HeapTaskDaemon: آخرین فعالیتها را هنگام پردازش جمعآوری زباله نشان میدهد.
-

نظارت بر خروجیهای گذشته (ApplicationExitInfo)
دریافت LMK در حین وقوع، برای اشکالزدایی فعال عالی است، اما برای اندازهگیری از راه دور فیلد، میتوانید از API ApplicationExitInfo استفاده کنید. این به برنامه شما اجازه میدهد تا دلیل خاتمه یافتن آن در یک جلسه قبلی را کشف کند.
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
}
}
بهترین شیوهها
- ابتدا در سطح پایه : همیشه پس از مقداردهی اولیه برنامه و قبل از انجام عملی که در حال آزمایش آن هستید، یک هیپ دامپ «سطح پایه» تهیه کنید.
- از صفحه نشت فعالیت AHAT استفاده کنید : AHAT شامل یک صفحه اختصاصی نشت فعالیت است که به طور خودکار نمونههای فعالیتی را که از بین رفتهاند اما هنوز در حافظه نگهداری میشوند، شناسایی میکند. این اغلب سریعترین راه برای یافتن نشتهای رایج است.
- بررسی مسیر به ریشههای GC : برای هر شیء نشتشده، از نمای مسیر از ریشه در AHAT استفاده کنید تا دقیقاً بفهمید کدام مرجع آن را زنده نگه میدارد (مثلاً یک فیلد استاتیک، یک رشته طولانی مدت یا یک شنونده ثبتشده).
← ابزارها | ↑ بالا | بیتمپها →