ابزارهای ارزیابی سریع

وقتی لازم است مصرف حافظه را بررسی کنید، با ابزارهای ارزیابی سطح بالا شروع کنید تا مشخص کنید کدام دسته‌های حافظه مهم‌تر هستند.

dumpsys meminfo

رایج‌ترین روش برای دریافت نمای کلی از میزان استفاده برنامه از حافظه dumpsys meminfo است. برای دیدن تفکیک دقیق شامل همه نگاشت‌های حافظه، اغلب مفید است که پرچم -a را اضافه کنید:

adb shell dumpsys meminfo -a <package_name_or_pid>

در اینجا یک نمونه خروجی از یک برنامه تحت فشار حافظه آورده شده است (به SwapPss بالا توجه کنید):

** MEMINFO in pid 4562 [com.android.memorylab] **
                   Pss  Private  Private  SwapPss      Rss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty    Total     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------   ------
  Native Heap      394      268       64    17440     4264    29416    18128     8040
  Dalvik Heap      165        8        0      672     8196    40949    32757     8192
 Dalvik Other       47       28        0      604     1132
        Stack      112      108        4      312      120
...
        TOTAL    33197      580      788    21920   167776    70365    50885    16232

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:       12                          32608
         Native Heap:      268                           4264
                Code:      724                         125672
               Stack:      108                            120
            Graphics:        0                              0
       Private Other:      256
              System:    31829
             Unknown:                                    5112

           TOTAL PSS:    33197            TOTAL RSS:   167776       TOTAL SWAP PSS:    21920

 Objects
               Views:       15         ViewRootImpl:        1
         AppContexts:        5           Activities:        1
...

تفسیر داده‌ها

  • Pss Total: اندازه مجموعه متناسب. این مجموع Private Dirty است
    • Private Clean + سهم عادلانه فرایند شما از حافظه هم‌رسانی‌شده با فرایندهای دیگر (مثل تصویر راه‌اندازی Zygote). این بهترین سنجه برای «این برنامه مسئول چه مقدار حافظه است» است.
  • Private Dirty: این حافظه دسترسی تصادفی است که فقط فرایند شما از آن استفاده می‌کند و اصلاح شده است (یا حافظه ناشناس است). این مهم‌ترین عدد برای پیدا کردن نشت است، زیرا این حافظه را نمی‌توان رها کرد.
  • پاک‌سازی خصوصی: RAM که فقط فرایند شما از آن استفاده می‌کند، اما نسخه اصلاح‌نشده‌ای از فایل در فضای ذخیره‌سازی است (مانند فایل DEX). اگر حافظه کم باشد، سیستم‌عامل می‌تواند آن را رها کند.
  • SwapPss: حافظه‌ای که فشرده شده و به ZRAM مبادله شده است. این اتفاق ممکن است به‌دلیل فشار حافظه بر سیستم یا به‌دلیل شناسایی فرایندهای پس‌زمینه‌ای توسط «زمان اجرای Android» برای فشرده‌سازی (انتقال صفحه‌های غیرفعال یا استفاده‌نشده به ZRAM) رخ دهد. در مثال dumpsys، تقریباً همه Native Heap و Dalvik Heap به ZRAM منتقل شده‌اند.
  • خلاصه برنامه: دسته‌بندی سطح بالاتری که ردیف‌های دقیق را در دسته‌های قابل‌فهم ترکیب می‌کند.
  • اشیاء: برای ردیابی نمونه‌های Activities و ViewRootImpl نشت‌کرده مفید است. اگر از برنامه‌تان خارج شوید و GC را اجباری کنید، Activities تعداد باید به ۰ برگردد.

بخش بودجه حافظه

در Android 18 و نسخه‌های بالاتر، dumpsys meminfo همچنین هنگام تخلیه یک برنامه، بخش بودجه حافظه را چاپ می‌کند. این بخش آمار memcg فرایند و کل برنامه (بسته) را می‌خواند، بنابراین نیازی نیست خودتان فایل‌های cgroup را پیدا کنید. در اینجا بخشی از برنامه نمونه MemoryLab در پیش‌زمینه، پس‌از سه بار تک‌ضرب زدن روی اختصاص دادن حافظه Java (‏۱۰ مگابایت) نشان داده شده است:

 Memory Budget
                        Process(KB)       Package(KB)
                        -----------       -----------
  Current Usage:              83820             87976
  Limit:                       none           4718592
  Limit Source:                none     MemoryLimiter
  Peak Usage:                 85136             89292
  Swap Used:                      0                 0

  Usage Breakdown
    Anonymous:                81700             81700
    File:                        96              4200
    File Mapped:                 16               616
    Shared Memory:               16                16
    Kernel:                    1580              1632
    Page Tables:                640               640
    Slab:                       439               458

  Pressure & Reclaim
    Throttle Events:              0                 0
    Refaults (Anon):              0                 0
    Refaults (File):              0                 0
  • استفاده کنونی: حافظه اختصاص‌داده‌شده به memcg (memory.current). این عددی است که بودجه حافظه محدود می‌کند. در نمونه، پس‌از سه تخصیص ۱۰ مگابایتی Java، حدود ۳۶٬۷۰۰ کیلوبایت افزایش یافت که با TOTAL PSS مطابقت دارد. مجموع بسته بیشتر از مجموع فرایند است، حتی برای برنامه تک‌فرایندی که صفحه‌های فایل ذخیره‌شده در حافظه نهان از اجرای قبلی هنوز بازیابی نشده‌اند.
  • حد: آستانه بازپس‌گیری (memory.high) که درحال‌حاضر اعمال می‌شود، یا none اگر آستانه‌ای وجود نداشته باشد. در نمونه، بسته دارای محدودیت حافظه سیستم است و فرایند محدودیت خاص خود را ندارد.
  • منبع محدودیت: محدودیت از کجا اعمال شده است. AndroidManifest یک <memory-budget> است که در مانیفست برنامه اعلام شده است، MemoryBudgetManager یک بودجه است که برنامه در زمان اجرا تنظیم کرده است، MemoryLimiter یک محدودیت حافظه سیستم است، و am memory-limiter یک لغو دستی است که با adb تنظیم شده است.
  • اوج استفاده: بالاترین شارژ از زمان ایجاد memcg (memory.peak). این مقدار N/A را در هسته‌هایی که از آن پشتیبانی نمی‌کنند نشان می‌دهد.
  • حافظه مجازی استفاده‌شده: حافظه فشرده‌شده در ZRAM (memory.swap.current).
  • تفکیک استفاده: فیلدهای اصلی memory.stat، مانند حافظه ناشناس و حافظه با پشتیبانی فایل.
  • فشار و بازیابی: رویدادهای محدودسازی تعداد دفعاتی را که memcg از حد memory.high خود فراتر رفته است می‌شمارد. خطاهای مجدد صفحاتی را می‌شمارد که جابه‌جا یا تخلیه شده‌اند و سپس دوباره موردنیاز بوده‌اند. اگر این اعداد هنگام استفاده از برنامه افزایش یابند، حد مجاز از مجموعه کاری برنامه تنگ‌تر است.

با پرچم -a یا -v، این بخش همچنین همه شمارنده‌های خام از memory.stat و memory.events را فهرست می‌کند.

تمرین عملی: تریاژ سریع با meminfo

این تمرین نشان می‌دهد که انواع مختلف تخصیص‌ها چگونه در meminfo نشان داده می‌شوند.

  1. راه‌اندازی برنامه:

    adb shell am start -W -n com.android.memorylab/.MainActivity
    
  2. بررسی حافظه پایه:

    adb shell dumpsys meminfo -s com.android.memorylab
    

    (پرچم -s خلاصه کوتاه و مختصری از دسته‌های حافظه ارائه می‌دهد).

  3. اختصاص دادن حافظه جاوا: روی دکمه اختصاص دادن حافظه جاوا (۱۰ مگابایت) ۳ بار در برنامه تک‌ضرب بزنید.

  4. مشاهده تغییر: meminfo -s را دوباره اجرا کنید. باید ببینید که مقدار Java Heap تقریباً ۳۰٬۰۰۰ کیلوبایت (۳۰ مگابایت) افزایش می‌یابد.

  5. اختصاص دادن حافظه بومی: روی دکمه اختصاص دادن حافظه بومی (۱ گیگابایت غیرقابل فشرده‌سازی) یک‌بار تک‌ضرب بزنید.

  6. مشاهده تغییر: meminfo -s را دوباره اجرا کنید. اکنون مقدار Native Heap باید افزایش قابل‌توجهی را نشان دهد (حدود ۱٬۰۰۰٬۰۰۰ کیلوبایت).

  7. مقایسه با memcg (‏Android 18 و بالاتر): به بخش بودجه حافظه در انتهای خروجی meminfo -s نگاه کنید. Current Usage grows along with TOTAL PSS after each allocation, because both the Java and the native allocations are charged to the app's memcg.

نمای جهانی

برای دیدن میزان استفاده از حافظه در کل سیستم، این فرمان را اجرا کنید:

adb shell dumpsys meminfo

این دستور خلاصه‌ای از استفاده از RAM، ازجمله کل RAM،‏ RAM آزاد، و فهرستی از فرایندها را که براساس PSS مرتب شده‌اند ارائه می‌دهد.

سنجه‌های کلیدی سیستم

  • ZRAM: مبادله فشرده‌شده در RAM.
  • DMA-BUF: میان‌گیرهای حافظه مشترک و گرافیک. نسخه‌های جدید Android به‌طور گسترده از DMA-BUF برای پردازش رابط کاربری استفاده می‌کنند. اگر مجموع DMA-BUF بسیار بالا است، فرایندهای سنگین گرافیکی را بااستفاده از /sys/kernel/debug/dma_buf/bufinfo بررسی کنید.
  • رم ازدست‌رفته: حافظه‌ای که توسط هسته یا هیچ فرایندی محاسبه نمی‌شود (مثلاً، مناطق رزروشده سخت‌افزار).

آمار memcg

هر فرایند برنامه گروه کنترل حافظه (memcg) خودش را دارد و هسته مجموع حافظه اختصاص‌داده‌شده به آن را به‌طور مداوم نگه می‌دارد (برای اطلاع از تفاوت این حسابداری با RSS و PSS، memcg را ببینید). خواندن این فایل‌ها ارزان است و برنامه شما را مختل نمی‌کند، بنابراین می‌توانید هر ثانیه که جریان کاربر را اجرا می‌کنید، از آن‌ها نمونه‌برداری کنید.

مسیر memcg فرایند را از خط 0:: در /proc/<PID>/cgroup جستجو کنید به‌جای اینکه خودتان مسیر را بسازید، زیرا چیدمان cgroup می‌تواند بین دستگاه‌ها متفاوت باشد. هر فرمان دستگاه را به‌عنوان یک رشته نقل‌قول‌شده تکی به adb shell ارسال کنید تا پوسته دستگاه نقل‌قول‌ها را ببیند:

PID=$(adb shell pidof -s <package_name>)

CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")

adb shell "cat $CG/memory.current $CG/memory.swap.current"

adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

فایل‌های کلیدی عبارت‌اند از:

  • memory.current: حافظه اختصاص‌داده‌شده به پردازش، به بایت. این عددی است که بودجه‌های حافظه محدود می‌کنند.
  • memory.swap.current: حافظه فشرده‌شده در ZRAM، برحسب بایت.
  • memory.stat: تفکیک هزینه، برحسب بایت برای اندازه‌ها. anon حافظه ناشناس است، مثل توده‌های جاوا و بومی. file حافظه نهان صفحه با پشتیبانی فایل است، مانند کد و منابع. workingset_refault_anon و workingset_refault_file صفحاتی را که جابه‌جا یا تخلیه شده‌اند و سپس دوباره موردنیاز بوده‌اند می‌شمارد. افزایش تعداد خطاهای ارجاع مجدد به این معنی است که فرایند با کمبود حافظه مواجه است.
  • memory.events: تعداد رویدادهای محدودیت حافظه. high تعداد دفعاتی را که فرایند به‌دلیل فراتر رفتن از حد مجاز memory.high محدود شده است می‌شمارد.

دایرکتوری والد memcg فرایند به کل برنامه (UID آن) تعلق دارد. فایل‌های آن شامل حافظه همه فرایندهای درحال اجرای برنامه، به‌علاوه صفحه‌های فایل ذخیره‌شده در حافظه نهان است که از فرایندهای قبلی برنامه که خارج شده‌اند و هنوز بازیابی نشده‌اند باقی مانده است:

adb shell "cat $CG/../memory.current"

اگر خطای اجازه دریافت کردید، adb root را اجرا کنید و دوباره امتحان کنید. برخی‌از دستگاه‌ها فقط به شما اجازه می‌دهند این فایل‌ها را به‌عنوان ریشه بخوانید.

برای راهنمای گام‌به‌گام این فایل‌ها با برنامه نمونه MemoryLab، به تمرین عملی: کاوش memcg مراجعه کنید.

نمایش نقشه

برای تفکیک بسیار دقیق‌تر نگاشت‌های حافظه فرایند، از showmap استفاده کنید. این ابزار از /proc/<pid>/smaps می‌خواند.

adb shell showmap <pid>

showmap همه نگاشت‌های حافظه را در فرایند فهرست می‌کند. نگاشت‌های کلید که باید به‌دنبالشان باشید عبارت‌اند از:

  • [anon:scudo:primary] یا [anon:libc_malloc]: پشته بومی. نسخه‌های مدرن Android از تخصیص‌دهنده Scudo استفاده می‌کنند.
  • *.art: تصاویر پشته جاوا (مثلاً تصویر راه‌اندازی).
  • [anon:dalvik-main space]: پشته اصلی جاوا.
  • *.so: کتابخانه‌های مشترک.
  • *.{oat,dex,odex,vdex}: کد DEX کامپایل‌شده.

می‌توانید خروجی را مرتب کنید تا بزرگ‌ترین مصرف‌کنندگان حافظه خصوصی را پیدا کنید:

# Sort by private clean + private dirty memory
adb shell showmap <pid> | awk '{print $6+$7, $0}' | sort -rn | head -n 10

نمونه برونداد:

 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
-------- -------- -------- -------- -------- -------- -------- -------- -------- --------- --------- --------- -------- -------- -------- ---- ------------------------------
2776    18176     5784     2823        0     3008      308     2468     4700     4700         0         0         0        0        0        0   47 [anon:scudo:primary]
560      952      876      657      316        0      560        0       12       12         0         0         0        0        0        0    8 /data/app/.../base.apk
448   393216      448      448        0        0        0      448        0        0         0         0         0        0        0        0    2 [anon:dalvik-main space]
...

تمرین عملی: کاوش عمیق با showmap

با نحوه شناسایی نگاشت‌های حافظه خاص و تمایز بین حافظه تمیز و کثیف آشنا شوید.

  1. Mmap Clean Memory (پاک کردن حافظه Mmap): روی دکمه Mmap Clean Anon Memory (10MB) (پاک کردن حافظه ناشناس Mmap‏ (۱۰ مگابایت)) در برنامه MemoryLab یک‌بار تک‌ضرب بزنید.
  2. دریافت شناسه پردازش: میانای کاربر برنامه شناسه پردازش آن را نشان می‌دهد. همچنین می‌توانید آن را با این فرمان پیدا کنید:

    adb shell pidof com.android.memorylab
    
  3. بازرسی با showmap:

    # Replace <PID> with the actual process ID from the previous step
    adb shell showmap <PID> | grep "memorylab-clean"
    
  4. شناسایی نگاشت: باید نگاشت ناشناسی را ببینید که با برچسب [anon:memorylab-clean] و اندازه مجازی دقیقاً ۱۰۲۴۰ کیلوبایت مشخص شده است. توجه داشته باشید که مقدار خصوصی کثیف برای این نقشه احتمالاً ۰ است. این حافظه ناشناس «تمیز» است زیرا از mmap برای رزرو فضای مجازی استفاده کردیم اما هنوز چیزی در آن ننوشته‌ایم (بنابراین هیچ RAM فیزیکی‌ای واقعاً مصرف نشده است).

  5. مقایسه پشته بومی: این را با نگاشت‌های [anon:scudo:primary] مقایسه کنید که اگر آن را بعداز مرحله ۵ تمرین meminfo اجرا کرده بودید، در showmap ظاهر می‌شد. چون آن ۱ گیگابایت تخصیص بومی درواقع در هر صفحه می‌نویسد، آن صفحه‌ها «کثیف» هستند و به‌شدت در ستون کثیف خصوصی نشان داده می‌شوند.

  6. نکته حرفه‌ای (اشیاء بزرگ): به تخصیص‌های Java Heap از تمرین meminfo توجه کنید. چون آرایه‌های بایت ۱۰ مگابایتی هستند، احتمالاً در showmap به‌صورت [anon:dalvik-free list large object space] نشان داده می‌شوند. زمان اجرای Android (ART) اشیای بزرگ‌تر از ۱۲ کیلوبایت را در فضای شیء بزرگ (LOS) جداگانه‌ای قرار می‌دهد تا از تکه‌تکه‌سازی در توده اصلی جلوگیری کند.

procstats

درحالی‌که meminfo عکس آنی را در زمان ارائه می‌دهد، procstats داده‌های سابقه را با نمونه‌برداری دوره‌ای از وضعیت و استفاده از حافظه همه برنامه‌های درحال اجرا ارائه می‌دهد. این ابزار اصلی مورداستفاده برای نظارت بر سلامت حافظه بلندمدت است و همان چیزی است که گزارش استفاده از حافظه را در «تنظیمات Android» هدایت می‌کند.

# View stats for the last 3 hours
adb shell dumpsys procstats --hours 3

آشنایی با وضعیت‌ها

برونداد procstats می‌تواند بسیار زیاد باشد. مهم‌ترین بخش، سنجه‌های داخل پرانتز در کنار هر وضعیت فرایند است:

Top: 100% (54MB-64MB-67MB / 49MB-58MB-61MB / 110MB-120MB-130MB over 4)

قالب: (% زمان در وضعیت) (minPss-avgPss-maxPss / minUss-avgUss-maxUss / minRss-avgRss-maxRss در نمونه‌ها)

  • Pss (اندازه مجموعه نسبی): حافظه مشترک به‌طور نسبی بین فرایندها تقسیم می‌شود.
  • Uss (اندازه مجموعه منحصربه‌فرد): حافظه‌ای که فقط توسط این فرایند استفاده می‌شود (معادل Private Clean + Private Dirty).
  • Rss (Resident Set Size): کل حافظه فیزیکی استفاده‌شده، شامل همه صفحه‌های هم‌رسانی‌شده (معمولاً بیش‌از حد واقعی محاسبه می‌شود).

چرا این موضوع اهمیت دارد:

  • برنامه پایدار: حداقل/ میانگین/ حداکثر به هم نزدیک هستند.
  • نشت حافظه: حداکثر به‌طور قابل‌توجهی بالاتر از حداقل است و با افزایش تعداد نمونه، میانگین به‌مرور زمان افزایش می‌یابد.
  • استفاده با جهش‌های ناگهانی: حداکثر بسیار بالاتر از میانگین است که نشان‌دهنده عملیات سنگین گذرا (مانند پردازش تصویر) است که اگر در زمان نامناسبی رخ دهند، ممکن است باعث کمبود حافظه شوند.

وضعیت‌های فرایند

procstats برنامه‌ها را در چندین ایالت (اغلب به‌صورت اختصاری) ردیابی می‌کند:

  • Persist: فرایندهای حیاتی سیستم که همیشه درحال اجرا هستند.
  • بالا: برنامه درحال‌حاضر در پیش‌زمینه است و برای کاربر قابل‌مشاهده است.
  • ImpFg (پیش‌زمینه مهم): فرایندهای پس‌زمینه حیاتی (مانند IME) یا برنامه‌های دارای سرویس پیش‌زمینه.
  • ImpBg (پس‌زمینه مهم): فرایندهای پس‌زمینه‌ای که برای سیستم مهم تلقی می‌شوند.
  • Fgs / BFgs: سرویس‌های پیش‌زمینه‌ای یا سرویس‌های پیش‌زمینه‌ای محدودشده.
  • سرویس / بازراه‌اندازی سرویس: سرویس‌های پس‌زمینه یا سرویس‌هایی که درحال‌حاضر بازراه‌اندازی می‌شوند.
  • ذخیره‌شده در حافظه نهان / LastAct: برنامه‌هایی که برای بازراه‌اندازی سریع در پس‌زمینه در حافظه نهان ذخیره شده‌اند. سیستم می‌تواند این برنامه‌ها را بدون مشکل متوقف کند.

وضعیت حافظه دستگاه و آمار زمان اجرا

procstats همچنین مدت‌زمانی را که کل دستگاه تحت فشار حافظه بوده است ردیابی می‌کند. به‌دنبال بخش آمار زمان اجرا بگردید:

Run time Stats:
  SOff/Norm: +1h10m15s
   SOn/Norm: +15m20s
        Low: +2m10s
       Crit: +45s
  • SOff / SOn: صفحه خاموش درمقابل صفحه روشن.
  • Norm / Mod / Low / Crit: سطوح فشار حافظه (عادی، متوسط، کم، بحرانی).

اگر دستگاه زمان قابل‌توجهی را در وضعیت کم یا بحرانی سپری کند، کند به‌نظر می‌رسد زیرا هسته به‌طور تهاجمی حافظه را بازیابی می‌کند و «بستن ناشی از کمبود حافظه» (LMK) شروع به خاتمه دادن به فرایندهای پس‌زمینه می‌کند. سپس می‌توانید آمار هر بسته را بررسی کنید تا ببینید کدام برنامه‌ها به‌طور خاص وقتی دستگاه در این وضعیت‌های تحت فشار قرار دارد، مصرف حافظه بالایی دارند.


← مفاهیم | ↑ بالا | جاوا →