מגביל זיכרון

ב-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). תהליכים עם מזהה משתמש (UID) נמוך מ-10,000 לא מקבלים מגבלות פלטפורמה, וגם לא החבילה של שירות ההסקה שמוגדר כברירת מחדל בארגז החול במכשיר (config_defaultOnDeviceSandboxedInferenceService). אפליקציות ושירותים של המערכת עדיין יכולים להצהיר על תקציבי זיכרון מצומצמים יותר, כפי שמתואר בקטע תקציבי זיכרון של המערכת.

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

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

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

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

מצב התהליךמגבלת הזיכרון
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מטמון

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

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

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

קטגוריות של תקציב הזיכרון

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

כשלתהליך יש תקציב, מגביל הזיכרון ממפה את מצב התהליך לאחת מארבע קטגוריות. הערכים android:state,‏ foreground,‏ perceptible ו-background במניפסט בוחרים את התקציב של הקטגוריה התואמת. לתהליכים בקטגוריה שנשמרה במטמון אין תקציב פעיל.

מצב התהליךטווח תקציב
PERSISTENTחזית
PERSISTENT_UIחזית
TOPחזית
BOUND_TOPחזית
TOP_SLEEPINGחזית
FOREGROUND_SERVICEניתן לתפיסה
BOUND_FOREGROUND_SERVICEניתן לתפיסה
IMPORTANT_FOREGROUNDניתן לתפיסה
IMPORTANT_BACKGROUNDניתן לתפיסה
TRANSIENT_BACKGROUNDרקע
BACKUPרקע
SERVICEרקע
RECEIVERרקע
HEAVY_WEIGHTרקע
HOMEרקע
LAST_ACTIVITYרקע
CACHED_ACTIVITYמטמון
CACHED_ACTIVITY_CLIENTמטמון
CACHED_RECENTמטמון
CACHED_EMPTYמטמון

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

ב-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 של מצלמה, סביבות ביצוע מאובטחות ומנהלי מכונות וירטואליות, שבדרך כלל צורכים בין 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)
Not visible swap limit
(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)

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

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

  • עקביות במערכת האקולוגית ותאימות של האפליקציות: מגבלות זיכרון עקביות במכשירים שונים מבטיחות ביצועי אפליקציה צפויים בכל המערכת האקולוגית של 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

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

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