مفاهیم اساسی

برای درک استفاده از حافظه در Android، باید سیستم را از چند دیدگاه مختلف بررسی کنید، از اشیاء Java سطح بالا گرفته تا صفحه‌های هسته سطح پایین.

افت ناگهانی عملکرد

استفاده از حافظه به‌خودی‌خود چیز خوب یا بدی نیست؛ آنچه اهمیت دارد این است که از حافظه برای چه کاری استفاده می‌کنید. بااین‌حال، وقتی به حد حافظه دردسترس در دستگاه نزدیک می‌شوید، درنهایت «عملکرد به‌شدت افت می‌کند».

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

وقتی از پرتگاه دور هستید، افزودن مقدار کمی استفاده از حافظه (مثلاً ۵۰ مگابایت) ممکن است تأثیر قابل‌توجهی بر عملکرد نداشته باشد. بااین‌حال، وقتی RAM دردسترس تمام می‌شود، به پرتگاه می‌رسیم. در این مرحله، سیستم‌عامل باید شروع به صفحه‌بندی حافظه و ازبین بردن فرایندها کند تا فضا آزاد شود. فراتر از این نقطه بحرانی، حتی افزایش کوچکی در استفاده از حافظه می‌تواند منجر به «کوبش» قابل‌توجهی شود، که در آن دستگاه غیرپاسخگو می‌شود یا به‌نظر می‌رسد بازراه‌اندازی می‌شود زیرا فرایندهای بحرانی متوقف می‌شوند.

پشته Android

هر لایه از پشته Android نمای منحصربه‌فرد خود را از حافظه دارد:

نمودار بلوکی پشته حافظه Android که برنامه‌های Java را در بالا، ART را در زیر آن‌ها، و هسته Linux را با صفحه‌های فیزیکی در پایین نشان می‌دهد

  1. برنامه‌ها (Java/Kotlin): توسعه‌دهندگان عمدتاً اشیای Java را می‌بینند که در پشته Java اختصاص داده شده‌اند.
  2. زمان اجرای Android‏ (ART): ART بااستفاده از صفحه‌های حافظه مجازی از هسته، پشته Java/Kotlin را مدیریت می‌کند. قبلاً با نام Dalvik شناخته می‌شد (این اصطلاحات گاهی به‌جای یکدیگر استفاده می‌شوند).
  3. هسته Linux: هسته حافظه را ازنظر صفحه‌های فیزیکی و مجازی می‌بیند. به‌طور سنتی، این اندازه ۴ کیلوبایت است، اما Android از اندازه صفحه ۱۶ کیلوبایتی نیز پشتیبانی می‌کند. هسته چیزی درباره «اشیاء Java» نمی‌داند.

اگر می‌خواهید حافظه را بهینه‌سازی کنید، باید یا در لایه خودتان بهینه‌سازی کنید (مثلاً بار کردن بیت‌مپ‌های کمتر) یا لایه‌های زیرین خودتان را درک کنید تا ببینید تخصیص‌های سطح بالای شما چگونه به استفاده از صفحه فیزیکی ترجمه می‌شوند.

انواع حافظه: ناشناس درمقابل پشتیبانی‌شده با فایل

قبل‌از اینکه به نحوه بازیابی حافظه توسط سیستم‌عامل بپردازیم، باید دو دسته اصلی صفحه‌های حافظه را در Linux درک کنید:

  1. حافظه ناشناس (anon): حافظه‌ای که توسط فایلی در فضای ذخیره‌سازی پشتیبانی نمی‌شود. این شامل حافظه اختصاص‌داده‌شده توسط C/C++‎ malloc (مثل Scudo allocator) و حافظه اختصاص‌داده‌شده برای اشیاء Java در پشته Java/Kotlin می‌شود. چون این حافظه فایل منبعی ندارد که به آن برگردد، سیستم‌عامل باید آن را در RAM نگه دارد یا درصورت تحت فشار بودن، آن را فشرده کند و به ZRAM منتقل کند.
  2. حافظه با پشتیبانی فایل (file): حافظه‌ای که مستقیماً از فایل در فضای ذخیره‌سازی نگاشت می‌شود. این شامل کد اجرایی (فایل‌های DEX، کتابخانه‌های بومی .so) و قلم‌ها می‌شود.

فایل‌های نگاشت‌شده حافظه (mmap)

‫Android از mmap برای نگاشت حافظه ناشناس و حافظه با پشتیبانی فایل در فضای نشانی پردازش استفاده می‌کند.

هنگام سروکار داشتن با حافظه با پشتیبانی فایل، صفحه‌ها بیشتر به موارد زیر طبقه‌بندی می‌شوند:

  • حافظه پاک: صفحه‌هایی که از فایلی که اصلاح نشده است نگاشت شده‌اند. اگر سیستم به حافظه بیشتری نیاز داشته باشد، هسته می‌تواند به‌راحتی این صفحه‌ها را رها کند، زیرا بعداً می‌توان آن‌ها را مستقیماً از فایل موجود در فضای ذخیره‌سازی مجدداً بار کرد.
  • حافظه کثیف: صفحه‌هایی که توسط فرایند اصلاح شده‌اند. این‌ها را نمی‌توان رها کرد؛ آن‌ها مانند حافظه ناشناس رفتار می‌کنند و باید در RAM نگهداری شوند یا به ZRAM منتقل شوند.

‫Android قالب‌های فایل (مانند DEX) را ترجیح می‌دهد که برای نگاشت حافظه مناسب هستند، و به سیستم اجازه می‌دهند تا به‌راحتی حافظه تمیز را تحت فشار بازیابی کند. ازآنجایی‌که حافظه ناشناس و کثیف را نمی‌توان به‌سادگی رها کرد، حافظه کثیف/ناشناس خصوصی بالا علت اصلی کوبش سیستم و مداخلات «بستن ناشی از کمبود حافظه» (LMK) است. اگر برنامه شما از حافظه کثیف خصوصی زیادی استفاده می‌کند، مستقیماً در افت عملکرد نقش دارید.

مدل فرایند Zygote

Android هزینه راه‌اندازی برنامه‌های جدید را بااستفاده از فرایندی به‌نام Zygote به‌حداقل می‌رساند.

  1. Zygote در زمان راه‌اندازی شروع می‌شود و کلاس‌ها و منابع چارچوب رایج را در حافظه خود پیش‌بارگیری می‌کند. از دیدگاه هسته، این به حافظه کثیف ناشناس تبدیل می‌شود، اما برای فرایند Zygote منحصربه‌فرد است.
  2. وقتی برنامه جدیدی شروع می‌شود، سیستم فرایند Zygote را منشعب می‌کند.
  3. فرایند فرزند جدید حافظه Zygote را بااستفاده از نگاشت مشترک با معناشناسی کپی-هنگام-نوشتن (COW) به ارث می‌برد.

نموداری که فرایند Zygote را نشان می‌دهد که صفحه‌های حافظه را با فرایندهای فرزند تازه منشعب‌شده بااستفاده از معناشناسی «کپی در زمان نوشتن» هم‌رسانی می‌کند

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

RSS،‏ PSS،‏ USS، و memcg

Because memory is shared heavily between processes—primarily through the Zygote model—there are several ways to account for a process's memory usage. RSS،‏ PSS، و USS صفحاتی را که در فضای نشانی یک فرایند نگاشت شده‌اند می‌شمارند:

نموداری که تفاوت بین «اندازه مجموعه مقیم» (RSS)، «اندازه مجموعه نسبی» (PSS)، و «اندازه مجموعه منحصربه‌فرد» (USS) را نشان می‌دهد و نحوه محاسبه صفحه‌های حافظه مشترک را نمایش می‌دهد

  • RSS (اندازه مجموعه مقیم): تعداد کل صفحه‌هایی که فرایند در RAM دارد. این مقدار مصرف را بیش‌از حد واقعی تخمین می‌زند زیرا صفحه‌های هم‌رسانی‌شده را چندین بار می‌شمارد (یک بار برای هر فرایندی که آن‌ها را هم‌رسانی می‌کند).
  • PSS (اندازه مجموعه تناسبی): مقدار کل حافظه منحصربه‌فرد برای فرایند، به‌علاوه سهم تناسبی آن از حافظه مشترک. اگر صفحه‌ای توسط ۵ فرایند هم‌رسانی شود، هر فرایند ۱/۵ آن صفحه را شارژ می‌کند.
  • USS (اندازه مجموعه منحصربه‌فرد): مقدار حافظه‌ای که مختص این پردازش است. این حافظه‌ای است که اگر پردازش متوقف شود به سیستم برگردانده می‌شود.

memcg: حسابداری خود هسته

هسته همچنین مجموع حافظه درحال اجرا را که هر فرایند استفاده می‌کند، چه آن حافظه نگاشت شده باشد یا نه، نگه می‌دارد. در Android، هر فرایند برنامه به گروه کنترل حافظه (memcg) خودش تعلق دارد که درون یک memcg برای کل برنامه تودرتو شده است (به سلسله‌مراتب memcg در Android مراجعه کنید). هرگاه صفحه‌ای از حافظه اختصاص داده شود، هسته آن را به memcg فرایندی که آن را اختصاص داده است منسوب می‌کند. کل شارژ در فایل memory.current memcg است.

محاسبه memcg از چند جهت مهم با RSS،‏ PSS، و USS متفاوت است:

  • هر صفحه دقیقاً یک‌بار به memcg فرایندی که اولین‌بار آن را به حافظه آورده است، شارژ می‌شود. این هزینه حتی اگر فرایندهای دیگر بعداً همان صفحه را نگاشت کنند، باقی می‌ماند. صفحه‌هایی که Zygote پیش‌بارگیری می‌کند به حساب Zygote گذاشته می‌شود، نه به حساب برنامه شما. وقتی برنامه شما در یکی از این صفحه‌ها می‌نویسد، نسخه خصوصی به حساب برنامه شما گذاشته می‌شود.
  • این حافظه شامل حافظه‌ای است که سنجه‌های جدول صفحه نمی‌توانند ببینند. memory.current صفحه‌های فایل را که برنامه‌تان بدون نگاشت کردن می‌خواند می‌شمارد (برای مثال، صفحه‌های پایگاه داده که با read یا pread خوانده می‌شوند).
  • این ویژگی مبادله را به‌طور جداگانه ردیابی می‌کند. حافظه‌ای که در ZRAM فشرده شده است از memory.current خارج می‌شود و به memory.swap.current می‌رود، بنابراین می‌توانید هر دو را بدون حدس زدن ببینید.
  • خواندن آن ارزان است. شمارنده‌ها همیشه به‌روز هستند و خواندن آن‌ها فضای نشانی برنامه‌تان را قفل یا اسکن نمی‌کند.
  • این چیزی است که سیستم محدود می‌کند. محدودیت‌های حافظه و بودجه‌های حافظه که Android برای برنامه‌ها اعمال می‌کند، بر memcg charge تأثیر می‌گذارد.

حافظه گرافیکی، مانند میان‌گیرهای پشت بیت‌مپ‌های سخت‌افزاری، معمولاً به memcg برنامه‌تان شارژ نمی‌شود.

از کدام سنجه‌ها و چه زمانی استفاده کنیم؟

انتخاب سنجه حافظه مناسب به آنچه می‌خواهید اندازه‌گیری کنید و محیطی که در آن اندازه‌گیری می‌کنید بستگی دارد. در دستگاه‌هایی که Android 17 QPR2 (سطح میانای برنامه‌سازی کاربردی ۳۷.۲) و بالاتر را اجرا می‌کنند، هنگام ردیابی برنامه خودتان، با memcg شروع کنید.

متریک بهترین برای قدرت کلید اشکال اصلی
memcg ردیابی برنامه تکی درطول زمان، مقایسه با بودجه‌ها هر صفحه را یک‌بار می‌شمارد، شامل حافظه پنهان صفحه نگاشت‌نشده است، فضای مبادله را به‌طور جداگانه ردیابی می‌کند. خواندن آن ارزان است. در همه دستگاه‌های دارای نسخه‌های قبل‌از Android 17 QPR2 دردسترس نیست. صفحه‌های هم‌رسانی‌شده به پردازشی که ابتدا آن‌ها را لمس کرده است نسبت داده می‌شود.
PSS چکیده‌نماهای حافظه در کل سیستم حافظه مشترک را به‌طور دقیق تقسیم می‌کند. مجموع PSS برابر است با کل حافظه استفاده‌شده. براساس سایر فرایندهای درحال اجرا نوسان دارد. برای مقایسه برنامه‌ای خاص درطول زمان مناسب نیست.
RSS ردگیری برنامه به‌صورت تکی درطول زمان، دورسنجی میدانی پایدار، مستقل از فرایندهای دیگر. جمع‌آوری سریع و ارزان. کل مصرف را بیش‌از حد واقعی تخمین می‌زند زیرا حافظه مشترک را چندین بار محاسبه می‌کند.
USS ارزیابی تأثیر توقف اجباری پردازش دقیقاً نشان می‌دهد که اگر برنامه بسته شود، چقدر حافظه آزاد می‌شود. حافظه مشترک را کاملاً نادیده می‌گیرد.

memcg (memory control group charge)

  • بهترین برای: ردیابی استفاده از حافظه یک برنامه خاص درطول زمان، در «مسیرهای قطعی کاربر» (CUJ)، یا بین نسخه‌های برنامه، در دستگاه‌های دارای Android 17 QPR2 یا بالاتر. همچنین این سنجه‌ای است که باید هنگام کالیبره کردن بودجه‌های حافظه تماشا کنید، زیرا این عددی است که بودجه‌ها محدود می‌کنند.
  • دلیل: مانند RSS، وقتی برنامه‌های دیگر هم‌رسانی صفحه‌ها را با برنامه شما شروع یا متوقف می‌کنند، memcg charge تغییر نمی‌کند. برخلاف RSS، هرگز صفحه‌ای را دو بار نمی‌شمارد، بنابراین می‌توانید مجموع memcg charge همه فرایندهای درحال اجرای برنامه‌تان را به‌دست آورید. memcg کل برنامه می‌تواند بیشتر از آن مجموع باشد، زیرا صفحه‌های فایل ذخیره‌شده در حافظه نهان را از فرایندهای خارج‌شده نگه می‌دارد تا زمانی که هسته آن‌ها را بازیابی کند. همچنین حافظه‌ای را که RSS،‏ PSS، و USS ازدست می‌دهند، مثل حافظه نهان فایل نگاشت‌نشده، محاسبه می‌کند. خواندن آن ارزان است، زیرا هسته شمارنده‌ها را هنگام شارژ کردن صفحه‌ها به‌روز می‌کند.
  • نحوه خواندن آن: memory.current، memory.swap.current، و memory.stat را در دایرکتوری memcg فرایند بخوانید (به آمار memcg مراجعه کنید). در Android 18 و بالاتر، dumpsys meminfo آن‌ها را چاپ هم می‌کند. از داخل برنامه‌تان، از MemoryBudgetManager استفاده کنید.
  • روال‌های مطلوب اندازه‌گیری میدانی: به همان دلایلی که در RSS توضیح داده شد، حافظه ناشناس + مبادله را ردیابی کنید: فیلد anon memory.stat به‌علاوه memory.swap.current. فیلد file از memory.stat شامل صفحه‌های فایلی است که برنامه‌تان یک‌بار خوانده است و هسته هنوز آن‌ها را بازیابی نکرده است، بنابراین memory.current می‌تواند برای مدتی پس‌از اینکه برنامه‌تان استفاده از آن حافظه را متوقف کرد بالا بماند.
  • چه زمانی باید مراقب بود: چون صفحات به فرایندی که اولین‌بار آن‌ها را لمس کرده است نسبت داده می‌شوند، برنامه شما برای صفحه فایل مشترکی (مانند قلم یا کتابخانه مشترک) که قبل‌از هر فرایند دیگری آن را می‌خواند، هزینه پرداخت می‌کند. برعکس، ممکن است فرایند دیگری برای صفحه‌هایی که برنامه شما نیز از آن‌ها استفاده می‌کند، هزینه دریافت کند. انتظار تفاوت‌های کوچکی بین دستگاه‌ها و اجراها برای حافظه با پشتیبانی فایل داشته باشید، و هنگام مقایسه به anon و swap تکیه کنید.

PSS (اندازه مجموعه متناسب)

  • بهترین کاربرد: گرفتن عکس آنی کامل از حافظه سیستم دستگاهی خاص در زمانی مشخص.
  • دلیل: PSS دارای ویژگی ریاضی مفیدی است که مجموع PSS برای همه فرایندهای درحال اجرا برابر با کل حافظه استفاده‌شده توسط فرایندها در سیستم است. این ویژگی حافظه مشترک را بدون دوبارشماری به‌طور کامل تخصیص می‌دهد.
  • چه زمانی باید از آن اجتناب کرد: نباید از PSS برای مقایسه فرایندی خاص در دو عکس آنی متفاوت، در زمان‌های متفاوت، یا در دستگاه‌های متفاوت استفاده کنید. ازآنجایی‌که PSS به تعداد فرایندهای دیگری بستگی دارد که درحال‌حاضر صفحه‌ها را با فرایند شما هم‌رسانی می‌کنند، PSS برنامه شما می‌تواند نوسان داشته باشد، حتی اگر رفتار واقعی برنامه شما کاملاً بدون تغییر باقی بماند.

RSS (Resident Set Size)

  • بهترین برای: ردیابی مصرف حافظه یک برنامه خاص درطول زمان، در «مسیرهای قطعی کاربر» (CUJ) مختلف، یا مقایسه بین نسخه‌های برنامه. همچنین کاربردی‌ترین انتخاب برای دورسنجی میدانی است.
  • دلیل: RSS پایدارتر از PSS است. این مقدار به شما می‌گوید چه مقدار حافظه به فرایند شما اختصاص داده شده است، صرف‌نظر از اینکه برنامه‌های دیگر چه کاری انجام می‌دهند. علاوه‌براین، جمع‌آوری RSS ارزان است. اندازه‌گیری PSS یا USS مستلزم اسکن ساختارهای پیچیده مدیریت حافظه هسته و دریافت قفل است که اگر بیش‌ازحد جمع‌آوری شود، می‌تواند باعث مشکلات عملکرد سیستم (مانند لرزش میانای کاربری) شود.
  • روال‌های مطلوب اندازه‌گیری میدانی: هنگام جمع‌آوری داده‌ها از دستگاه‌ها در محیط واقعی، معمولاً بهتر است RSS ناشناس + مبادله (ZRAM) را اندازه‌گیری کنید.
    • چرا فقط ناشناس؟ صفحه‌های پشتیبانی‌شده با فایل (مانند کد، منابع برنامه، قلم‌ها، یا دیگر فایل‌های نگاشت‌شده در حافظه) ممکن است در هر زمانی به‌دلیل فشار حافظه پنهان صفحه در سراسر سیستم توسط هسته تخلیه شوند. گنجاندن صفحه‌های فایل در سنجه‌ها برای ردیابی برنامه‌ای خاص باعث ایجاد اختلال ناشی از تصمیمات مدیریت حافظه سیستم‌عامل می‌شود که ممکن است به‌دلیل فشار حافظه ازسوی برنامه‌های دیگر یا سیستم باشد. برعکس، حافظه ناشناس (مانند Java و انبوهه‌های بومی) مستقیماً توسط برنامه شما کنترل می‌شود.
    • چرا باید فضای مبادله اضافه کرد؟ تحت فشار حافظه، سیستم‌عامل صفحه‌های ناشناس را فشرده می‌کند و آن‌ها را به ZRAM (مبادله) منتقل می‌کند. اگر فقط حافظه ناشناس مقیم را اندازه‌گیری کنید، ممکن است سنجه‌هایتان به‌اشتباه «کاهش» در استفاده از حافظه را نشان دهند، صرفاً به این دلیل که سیستم تحت فشار بوده و صفحه‌هایتان را جابه‌جا کرده است. افزودن حافظه مبادله‌شده تضمین می‌کند که همه حافظه ناشناسی را که برنامه‌تان اختصاص داده است، چه درحال‌حاضر در RAM باشد یا ZRAM، درنظر بگیرید.
    • هرجا ممکن است memcg را ترجیح دهید. در دستگاه‌های دارای Android 17 QPR2 و بالاتر، anon در memory.stat به‌علاوه memory.swap.current اندازه‌گیری مشابهی به شما می‌دهد که صفحات ناشناس هم‌رسانی‌شده با Zygote را حذف می‌کند و جمع‌آوری آن ارزان‌تر است (به memcg مراجعه کنید).

USS (اندازه مجموعه منحصربه‌فرد)

  • بهترین کاربرد: تعیین تأثیر فوری توقف فرایند بر سیستم.
  • دلیل: USS مقدار دقیق حافظه‌ای را که درصورت خاتمه یافتن فرایند (برای مثال، توسط دیمون Low Memory Killer،‏ lmkd) فوراً بازپس گرفته می‌شود و به سیستم برگردانده می‌شود به‌طور دقیق نشان می‌دهد.
  • نحوه استفاده: این سنجه هنگام ارزیابی فرایندهای پایدار یا سرویس‌های پس‌زمینه‌ای بسیار ارزشمند است. اگر درحال ارزیابی هزینه فرایند پس‌زمینه‌ای در سراسر سیستم هستید، USS دقیقاً به شما می‌گوید که سیستم چقدر حافظه را منحصراً برای زنده نگه داشتن آن فرایند خاص فدا می‌کند.

↑ بالا | ابزارها ←