מגביל זיכרון

ב-Android מגרסה 17 ואילך יש את Memory Limiter, שירות מערכת שעוקב אחרי השימוש בזיכרון של תהליכי אפליקציות ומגביל אותו באמצעות Linux cgroup v2. התכונה "מגביל הזיכרון" מונעת מאפליקציות בודדות לצרוך כמות לא פרופורציונלית של זיכרון המערכת. כך היא מפחיתה את העומס הכולל על הזיכרון ומונעת סגירה אגרסיבית של תהליכים קריטיים בחזית ובמטמון בגלל מחסור בזיכרון.

מנגנון

התכונה "מגביל הזיכרון" משתלבת עם Activity Manager Service‏ (AMS) כדי לעקוב אחרי אירועים במחזור החיים של התהליך ושינויים בסטטוס. התכונה "מגבלת הזיכרון" אוכפת את מגבלות הזיכרון באמצעות היררכיית cgroup v2 של ליבת Linux.

כדי לתמוך במגביל הזיכרון, הליבה של המכשיר צריכה להפעיל את cgroup v2 ואת הבקר memory. השירות מסתמך באופן ספציפי על המאפיינים הבאים:

memory.high
מגבלה גמישה. כשהמגבלה נחצית, התהליך מוגבל והליבה מנסה להחזיר דפים באופן יזום מהתהליך הזה.
memory.swap.max
‫
מגבילה את הכמות המקסימלית של שטח ההחלפה (כמו ZRAM) שהתהליך יכול להשתמש בו.

ההשפעה על האפליקציות

אפליקציות שפועלות במסגרת מגבלות הזיכרון שלהן לא מושפעות מהמגבלה.

כשאפליקציה חורגת ממגבלת ה-memory.high, הליבה מפנה את הזיכרון הנקי של האפליקציה שמגובה בקבצים, ומחליפה את הזיכרון האנונימי הלא פעיל שלה כדי שהאפליקציה לא תחרוג מהמגבלה. כתוצאה מפעילות הפינוי וההחלפה של הדפים, יכול להיות שהאפליקציה תהיה כפופה זמנית לוויסות קצב הביצוע, אבל היא תמשיך לפעול.

במצבים קיצוניים, כמו דליפת זיכרון לא מבוקרת שבה האפליקציה ממשיכה להקצות זיכרון אנונימי אחרי ששטח ההחלפה נגמר, האפליקציה לא מצליחה להקצות זיכרון ונסגרת.

מעקב אחרי תהליכים

כברירת מחדל, מגביל הזיכרון עוקב אחרי תהליכי האפליקציה (UID >= 10000). תהליכי מערכת חיוניים פטורים מההגבלה כדי לשמור על יציבות המערכת.

התכונה "מגביל הזיכרון" מקצה מגבלות זיכרון על סמך מצב החשיפה של התהליך:

  • תהליכים גלויים: תהליכים שמארחים ממשק משתמש שגלוי למשתמש (כמו הפעילות שנמצאת כרגע במוקד או אינטראקציה פעילה). כדי להציג ממשק משתמש נדרשת קבוצת עבודה גדולה יותר לעיבוד ולצינורות עיבוד גרפיים, ולכן תהליכים גלויים מקבלים מגבלת זיכרון נדיבה יותר.

  • תהליכים לא גלויים: תהליכים שפועלים ברקע בלי להציג ממשק משתמש (כמו שירותים שפועלים ברקע או מקלטי שידור). מכיוון שהם לא מעבדים רכיבי ממשק משתמש, הם פועלים תחת מגבלה מחמירה יותר.

בטבלה הבאה מפורט המיפוי של מצבי תהליך ספציפיים לסיווגים של מגבלות זיכרון:

מצב התהליךמגבלת הזיכרון
PERSISTENTבלתי מוגבלת
PERSISTENT_UIבלתי מוגבלת
TOPגלוי
BOUND_TOPגלוי
FOREGROUND_SERVICEלא מוצג
BOUND_FOREGROUND_SERVICEלא מוצג
IMPORTANT_FOREGROUNDגלוי
IMPORTANT_BACKGROUNDלא מוצג
TRANSIENT_BACKGROUNDלא מוצג
BACKUPלא מוצג
SERVICEלא מוצג
RECEIVERלא מוצג
TOP_SLEEPINGגלוי
HEAVY_WEIGHTלא מוצג
HOMEלא מוצג
LAST_ACTIVITYלא מוצג
CACHED_ACTIVITYמטמון
CACHED_ACTIVITY_CLIENTמטמון
CACHED_RECENTמטמון
CACHED_EMPTYמטמון

במצב השמירה במטמון, התהליכים מוקפאים ואז המערכת מחזירה אותם למצב הקודם ככל האפשר.

כשתהליך חורג מהמגבלה שהוקצתה לו ב-memory.high, מגביל הזיכרון מזהה את האירוע ויכול להפעיל פעולות אבחון, כמו תיעוד פרופיל הזיכרון או רישום חריגה ב-statsd.

הגדרות אישיות

ב-Android מגרסה 17 ואילך, מגדירים את מגביל הזיכרון באמצעות קובץ התצורה הרגיל של הפלטפורמה במחיצת המערכת ב-/system/etc/memory-limiter-config.xml.

פורמט XML

קובץ התצורה פועל לפי הסכימה שמוגדרת ב-memory-limiter-config.xsd. הקובץ מגדיר כמה קבוצות של מגבלות לפי הזיכרון הזמין. השירות בוחר את קבוצת המגבלות התואמת הגבוהה ביותר על סמך ה-RAM הזמין במכשיר. כל ערכי הזיכרון מצוינים במגה-בייטים (MiB).

<MemoryLimiterConfig>
  <version>1</version>
  <configList>
    <!-- RAM boundaries account for MemTotal excluding carve-outs. -->
    <limitSet>
      <!-- Limits for 16GB RAM device [14336, 18432): 10G/5G/5G/5G -->
      <minimumRequiredMemTotal>14336</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>18432</maximumAllowedMemTotal>
      <memVisible>10240</memVisible>
      <memNotVisible>5120</memNotVisible>
      <swapVisible>5120</swapVisible>
      <swapNotVisible>5120</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 12GB RAM device [9216, 14336): 8G/4G/4G/4G -->
      <minimumRequiredMemTotal>9216</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>14336</maximumAllowedMemTotal>
      <memVisible>8192</memVisible>
      <memNotVisible>4096</memNotVisible>
      <swapVisible>4096</swapVisible>
      <swapNotVisible>4096</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 8GB RAM device [6800, 9216): 5G/3G/3G/3G -->
      <minimumRequiredMemTotal>6800</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>9216</maximumAllowedMemTotal>
      <memVisible>5120</memVisible>
      <memNotVisible>3072</memNotVisible>
      <swapVisible>3072</swapVisible>
      <swapNotVisible>3072</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 6GB RAM device [4800, 6800): 4G/2G/2G/2G -->
      <minimumRequiredMemTotal>4800</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>6800</maximumAllowedMemTotal>
      <memVisible>4096</memVisible>
      <memNotVisible>2048</memNotVisible>
      <swapVisible>2048</swapVisible>
      <swapNotVisible>2048</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 4GB RAM device [3200, 4800): 2G/1G/1G/1G -->
      <minimumRequiredMemTotal>3200</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>4800</maximumAllowedMemTotal>
      <memVisible>2048</memVisible>
      <memNotVisible>1024</memNotVisible>
      <swapVisible>1024</swapVisible>
      <swapNotVisible>1024</swapNotVisible>
    </limitSet>
  </configList>
</MemoryLimiterConfig>
version
‫
מספר שלם חיובי שמזהה את גרסת ההגדרה. הערך חייב להיות 1.
minimumRequiredMemTotal

כמות הזיכרון המינימלית במערכת MemTotal (ב-MiB, כולל ערך תחתון) שנדרשת כדי להחיל את קבוצת המגבלות הזו. השירות משווה את זה לזיכרון הכולל הזמין שמדווח על ידי הליבה ב-/proc/meminfo.

הבנת הגבולות וההקצאות של הזיכרון: הערך של הליבה MemTotal משקף את ה-DRAM הפיזי שנגיש למערכת ההפעלה אחרי ניכוי הקצאות הזיכרון של החומרה והקושחה. הקצאות הזיכרון שומרות זיכרון RAM לרכיבי חומרה מיוחדים כמו GPU, מודם פס בסיס, ISP של מצלמה, סביבות ביצוע מאובטחות ומנהלי מכונות וירטואליות (hypervisors), שבדרך כלל צורכים בין 500 MiB ליותר מ-2 GiB של זיכרון פיזי.

מכיוון שהערך של MemTotal נמוך מה-DRAM הפיזי של המכשיר, ערכי הסף של minimumRequiredMemTotal כוללים מרווח לנתונים האלה. לדוגמה, בהגדרה שמטרגטת מכשירים עם 16GB, הסף התחתון הוא 14336 (14GiB) במקום 16384 (16GiB), וברמה של מכשירים עם 12GB הסף התחתון הוא 9216 (9GiB). כך אפשר לוודא שכל מכשיר תואם לרמה המיועדת שלו, בלי קשר לשינויים ברמת הלוח.

maximumAllowedMemTotal

הערך המקסימלי של MemTotal במערכת (ב-MiB, תקרה בלעדית) שעליו חלה המגבלה הזו. אם הערך של MemTotal גדול מהתקרה הזו או שווה לה, המגבלה שהוגדרה לא חלה. במכשירים עם זיכרון של 18,432MiB‎ או יותר, או במכשירים עם זיכרון של פחות מ-3,200MiB‎, מגביל הזיכרון לא מחיל מגבלות, והתהליכים לא מוגבלים.

memVisible

מגבלת הזיכרון הגמישה (memory.high) ב-MiB שחלה על תהליכים גלויים.

memNotVisible

מגבלת הזיכרון הגמישה (memory.high) ב-MiB שחלה על תהליכים לא גלויים.

swapVisible

מגבלת שטח ההחלפה (memory.swap.max) ב-MiB שחלה על תהליכים גלויים.

swapNotVisible

המגבלה על שטח ההחלפה (memory.swap.max) ב-MiB שחלה על תהליכים לא גלויים.

מגבלות רגילות של הפלטפורמה

ב-Android מגרסה 17 ואילך, הפלטפורמה מציינת מגבלות סטנדרטיות לזיכרון ולהחלפה ב-/system/etc/memory-limiter-config.xml ברמות הפיזיות הנפוצות של זיכרון RAM, באמצעות מרווחי טווח של [floor, ceiling):

זיכרון RAM פיזי של היעד טווח MemTotal [מינימום, מקסימום)
(minimumRequiredMemTotal,
maximumAllowedMemTotal)
מגבלת הזיכרון הגלוי
(memVisible)
מגבלת הזיכרון הלא גלוי
(memNotVisible)
מגבלת ההחלפה הגלויה
(swapVisible)
מגבלת ההחלפה לא גלויה
(swapNotVisible)
‫16GB [14336, 18432) MiB ‫10,240 MiB‏ (10 GiB) ‫5,120 MiB‏ (5 GiB) ‫5,120 MiB‏ (5 GiB) ‫5,120 MiB‏ (5 GiB)
‫12GB [9216, 14336) MiB ‫8,192 MiB‏ (8 GiB) ‫4,096 MiB‏ (4 GiB) ‫4,096 MiB‏ (4 GiB) ‫4,096 MiB‏ (4 GiB)
8GB [6800, 9216) MiB ‫5,120 MiB‏ (5 GiB) ‫3,072 MiB‏ (3 GiB) ‫3,072 MiB‏ (3 GiB) ‫3,072 MiB‏ (3 GiB)
6GB [4800, 6800) MiB ‫4,096 MiB‏ (4 GiB) ‫2,048 MiB‏ (2 GiB) ‫2,048 MiB‏ (2 GiB) ‫2,048 MiB‏ (2 GiB)
4GB [3200, 4800) MiB ‫2,048 MiB‏ (2 GiB) ‫1,024 MiB‏ (1 GiB) ‫1,024 MiB‏ (1 GiB) ‫1,024 MiB‏ (1 GiB)

עקרונות של מגבלת הזיכרון

ההגדרה של מגביל הזיכרון מבוססת על העקרונות הבאים של הפלטפורמה:

  • עקביות במערכת האקולוגית ותאימות של האפליקציה: מגבלות זיכרון עקביות במכשירים שונים מבטיחות ביצועי האפליקציה צפויים בכל המערכת האקולוגית של Android. האפליקציות מפותחות ונבדקות בהתאם לציפיות הסטנדרטיות לגבי הזיכרון. שמירה על מגבלות אחידות בפלטפורמה מונעת הגבלת קצב בלתי צפויה או סגירה מוקדמת.

  • הקצאת משאבים פרופורציונלית: המגבלות של הפלטפורמה מותאמות לפי הקיבולת הפיזית של זיכרון ה-RAM:

    • תהליכים גלויים: מוקצים בערך חצי עד שני שלישים מכלל זיכרון ה-RAM הפיזי כדי לתמוך בעומסי עבודה פעילים של ממשק המשתמש, הקומפוזיציה והרינדור.
    • תהליכים לא גלויים: מוקצה בערך רבע עד שליש מסך כל זיכרון ה-RAM הפיזי למשימות ברקע.
  • תחולה אוניברסלית: המגבלות חלות באופן אחיד על כל תהליכי האפליקציות במכשיר (UID >= 10000), כולל אפליקציות מערכת ואפליקציות OEM שמותקנות מראש. התכונה "הגבלת הזיכרון" לא תומכת ברשימות היתרים כדי להחריג אפליקציות ספציפיות, וכך מבטיחה ניהול הוגן של הזיכרון בכל האפליקציות. כדי להגדיר רמות הגנה מינימליות של memory.min בכוונון עצמי על תהליכי רקע אינטראקטיביים קריטיים, ורמות מקסימליות הדוקות יותר של memory.high שמכוילות לעומסי עבודה באפליקציות ובשירותים ספציפיים של המערכת, קראו את המאמר תקציבי זיכרון המערכת.

  • היעדר ממשקי API לשאילתות בזמן ריצה: ב-Android מגרסה 17 ואילך, אפליקציות לא יכולות לשלוח שאילתות באופן פרוגרמטי לגבי מגבלות הזיכרון שהוקצו להן בזמן הריצה. המגבלות הרגילות של הפלטפורמה מוגדרות עם מרווחים נדיבים, כך שאפליקציות שפועלות בצורה תקינה פועלות ללא מגבלות במהלך שימוש רגיל.

  • החזרה ודינמיקה של החלפה בקרנל: כשמגיעים למגבלת ה-memory.high של תהליך באפליקציה, הקרנל של Linux מפנה את החזרת הזיכרון באופן ספציפי ל-cgroup של התהליך הזה. זה כולל הסרה של דפים לא פעילים שמגובים בקבצים והחלפה של דפים אנונימיים לא פעילים ל-ZRAM, כדי לשמור על פעולה חלקה של המערכת בלי להפעיל עומס על הזיכרון הגלובלי.

פקודות מעטפת

הפקודה am memory-limiter מאפשרת למפתחים ולמפעילי מערכות ליצור אינטראקציה עם השירות בזמן הריצה לצורך פיתוח, בדיקה וניפוי באגים:

am memory-limiter <SUB-COMMAND>

status

פקודת המשנה status מדווחת על סטטוס הפעילות ועל המדדים הפעילים של מגביל הזיכרון:

adb shell am memory-limiter status

הפלט לדוגמה:

Memory limiter
  enabled                  monitoring=true          ignored=none
  visibleMem=1948MB        visibleSwap=974MB
  notVisibleMem=974MB      notVisibleSwap=487MB
  started=36               watched=36               watch-failed=0
  events=0                 processes=36             process-hwm=36

שדות המפתח בפלט כוללים:

monitoring
מציין אם מגביל הזיכרון עוקב באופן פעיל אחרי תהליכים.
visibleMem וגם notVisibleMem
מגבלות הזיכרון המוחלטות המחושבות (memory.high) שנאכפות כרגע לכל מצב חשיפה.
visibleSwap וגם notVisibleSwap
מגבלות ההחלפה המוחלטות המחושבות (memory.swap.max) שנאכפות כרגע לכל מצב חשיפה.
events
מספר הפעמים שתהליך חרג מהמגבלה שהוקצתה לו.
processes
מספר התהליכים הנוכחיים שנמצאים במעקב.

התעלמות

פקודת המשנה ignore מחריגה באופן זמני מזהה משתמש ספציפי או את כל התהליכים מהגבלת הזיכרון. האפשרות הזו שימושית להשוואת ביצועים, לבדיקת עומסים או לאבחון התנהגות הזיכרון:

# Ignore a specific UID
adb shell am memory-limiter ignore 10087
# Ignore all processes (temporarily disables limiting)
adb shell am memory-limiter ignore all
# Resume normal limiting operation
adb shell am memory-limiter ignore none

ידני

פקודת המשנה manual מבטלת את המגבלות המחושבות לתהליך ספציפי לפי מזהה תהליך (PID) עם ערך מוחלט מותאם אישית בבייטים. הערך חייב להיות מספר שלם, אבל יכול לכלול את הסיומת MB כדי לציין MiB או את הסיומת GB כדי לציין GiB:

# Set a 1GiB limit for PID 1234
adb shell am memory-limiter manual 1234 1073741824
# Set a 1GiB limit for PID 1234
adb shell am memory-limiter manual 1234 1024MB
# Set a 1GiB limit for PID 1234
adb shell am memory-limiter manual 1234 1GB
# Remove the manual override for PID 1234
adb shell am memory-limiter manual 1234 none

שינויים ידניים חלים רק על משך החיים של המופע הספציפי של התהליך. אם התהליך מתחיל מחדש, הוא חוזר למגבלות הרגילות בהתאם למצב שלו.

אי אפשר להגדיר ידנית ערך שחורג מהמגבלות הפיזיות של המערכת.