Arbeitsspeicherbudgets des Systems

In Android 17 und höher wird der Arbeitsspeicher für Systemprozesse über zwei sich ergänzende Linux-cgroup-v2-Mechanismen verwaltet, die zusammen mit dem allgemeinen Memory Limiter für Anwendungen funktionieren:

  • System-Daemon-Schutzuntergrenzen (SystemMemoryBudgetManager mit memory.min): Selbstoptimierende cgroup v2-memory.min-Reservierungen (festgelegt auf 90 % des festgelegten residenten Arbeitssatzes jedes Daemons und global auf 10% des gesamten physischen RAM begrenzt) schützen kritische interaktive Daemons und Shell-Prozesse (system_server, surfaceflinger, die Grafik-composer-HAL, audioserver, cameraserver, netd, SystemUI und den Home Launcher) bei Speicherdruck und ersetzen das statische Anpinnen von PinnerService-mlock-Dateien.
  • Speicherobergrenzen für System-Apps (<memory-budget> mit memory.high): Die auf die Arbeitslast abgestimmten weichen Limits von cgroup v2 memory.high (normalerweise 30 MB bis 190 MB), die in Manifestdateien für persistente und im Hintergrund ausgeführte System-Apps (Bluetooth, Nfc, CalendarProvider, Traceur, MediaProvider, :PhotoPicker, NetworkStack, AppSearch und TeleService) deklariert sind, geben proaktiv den Cache für kalte Dateiseiten und den anonymen Heap nach einem Burst frei, bevor globaler Speicherdruck auftritt.

In der folgenden Tabelle werden die beiden Mechanismen verglichen:

Dimension Schutzebenen für System-Daemons Speicherobergrenzen für System-Apps
Plattformdienst SystemMemoryBudgetManager MemoryLimiter
Cgroup v2-Steuerung memory.min harter Schutzboden memory.high Obergrenze für die teilweise Rückforderung
Zielprozesse Latenzkritische interaktive Daemons und Shell-UI (system_server, surfaceflinger, composer HAL, audioserver, cameraserver, netd, SystemUI und die Home App) Persistente und monitorlose System-Apps, Contentanbieter und Hintergrunddienste (z. B. Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch und TeleService)
Modell zur Größenanpassung Selbstoptimierung zur Laufzeit (90% des festgelegten Arbeitssatzes, global auf 10% des gesamten RAM begrenzt) ohne gerätespezifische Optimierungstabellen Deklarative XML-Attribute im <memory-budget>-Manifest, die nach RAM-Stufe, Prozessstatus und Displaypixelanzahl kalibriert sind
Hauptvorteil Verhindert Ruckeln der interaktiven Benutzeroberfläche und Blockierungen beim Aufwachen bei Speichermangel, während statische PinnerService-mlock-Dateilisten ersetzt werden Entfernt kontinuierlich kalten Einmal-Dateicache und Post-Burst-Heap, um mehr physischen RAM für Vordergrund- und im Cache gespeicherte Apps freizugeben

Weitere Informationen zur <memory-budget>-Syntax, zu XML-Attributen, Laufzeit-APIs und zur Arbeitsspeicherabrechnung des Betriebssystems finden Sie unter Arbeitsspeicherbudgets für Apps festlegen und Android-Arbeitsspeicher verstehen und Fehler beheben.

Kritische interaktive Daemons mit SystemMemoryBudgetManager schützen

SystemMemoryBudgetManager sorgt dafür, dass die Prozesse, die sich direkt im interaktiven Pfad des Nutzers befinden, bei Speicherdruck im physischen RAM verbleiben. Anstatt eine statische Liste von APK-, ODEX- und VDEX-Dateien mit mlock im Arbeitsspeicher zu fixieren (wie es PinnerService für config_defaultPinnerServiceFiles und config_pinnerHomePinBytes tut), weist SystemMemoryBudgetManager den folgenden Prozessen dynamische cgroup v2-memory.min-Untergrenzen zu:

  • Core-System-Slice (/sys/fs/cgroup/system): system_server, surfaceflinger, die Grafik-composer-HAL, audioserver, cameraserver und netd sowie alle formfaktorspezifischen nativen System- oder Anbieter-Daemons, die in config_systemMemoryBudgetAdditionalSystemProcesses aufgeführt sind (z. B. android.hardware.perceptionhost und auracamerahal auf XR-Geräten).
  • Interaktiver App-Slice (/sys/fs/cgroup/apps): Der Hauptprozess des konfigurierten SystemUI-Pakets (config_systemUi) und der Hauptprozess des aktuellen Nutzers, der die Rolle „Home“ innehat.

Wenn du neben config_systemUi auch den Inhaber der Rolle „Zuhause“ schützt, sind alle Formfaktoren abgedeckt. Bei Formfaktoren, bei denen config_systemUi ein einfaches Kompatibilitätspaket ist (z. B. Wear OS, wo das Zifferblatt, die Benachrichtigungen und die Kacheln in der Home App ausgeführt werden), erhält die für Nutzer sichtbare Shell weiterhin den vollen Schutz des Working Sets. Wenn der Home-Intent zum Paket config_systemUi aufgelöst wird, wird der Prozess einmal geschützt. Nur der Hauptprozess jedes Pakets (processName == packageName) erhält eine Schutzuntergrenze, sodass Hilfssubprozesse (z. B. :screenshot) kein Schutzbudget verbrauchen.

Richtlinie zur Selbstoptimierung und dreistufige cgroup-Delegation

Da der Arbeitssatz eines interaktiven Daemons je nach Formfaktor und RAM-Kapazität stark variiert (z. B. verwendet SystemUI etwa 45 MB auf einer Smartwatch, 60 MB auf einem Smartphone mit 6 GB und 180 MB auf einem Smartphone mit 16 GB), sind statische gerätespezifische Suchtabellen unzuverlässig. Stattdessen leitet SystemMemoryBudgetManager die Untergrenzen direkt aus dem eigenen Resident-Footprint jedes Prozesses mithilfe von drei Konstanten ab:

Richtlinienparameter Standardwert Verhalten
Mindestpreis pro Prozess 90% des abgerechneten memory.current abzüglich des nicht zugeordneten Datei-Caches Schützt anonymen Heap, Slab und Hot-Mapped-Code, während die kältesten 10% des Tails wiederherstellbar bleiben, sodass wirklich kalte Seiten weiterhin entfernt werden können. Der nicht zugeordnete Dateicache (max(0, file - shmem - file_mapped) von memory.stat) wird abgezogen, bevor das Verhältnis von 90% angewendet wird, damit der Cache für den Boot-Paket-Scan oder einmalige Asset-Lesevorgänge die Mindestwerte nicht künstlich erhöhen.
Globale RAM-Obergrenze 10% des gesamten RAM (config_systemMemoryBudgetLowKb) Entspricht der Schutzmaßnahme PinnerService config_pinnerMaxPinnedMemoryPercentage. Wenn die Summe der 90-%-Mindestpreise 10% von MemTotal überschreitet, wird jeder Mindestpreis proportional um denselben Faktor herabgesetzt.
Aktualisierungsrhythmus 1, 4, 16 und 64 Sekunden nach dem Start, dann alle 30 Minuten Eine geometrische Stabilisierungsreihe konvergiert auf dem Arbeitssatz jedes Daemons nach dem Start, ohne dass die Aufwärmverzögerungen pro Gerät hartcodiert werden müssen. Eine 30-minütige Aktualisierung im stabilen Zustand verfolgt das allmähliche Wachstum und die Verkleinerung des Arbeitssatzes über die Lebensdauer des Geräts.

Im Linux-cgroup v2-Speichercontroller (mm/memcontrol.c:effective_protection) ist der effektive memory.min-Schutz einer Blatt-cgroup durch den memory.min-Wert begrenzt, der für die übergeordneten und übergeordneten cgroups konfiguriert ist. Wenn Sie memory.min nur für ein Blattverzeichnis pid_<pid> festlegen, während uid_<uid>/memory.min auf 0 gesetzt ist, führt dies zu einem effektiven Schutz von null. Wenn Sie dagegen einen pauschalen memory.min nur für das oberste /sys/fs/cgroup/system-Segment festlegen, wird der nicht beanspruchte Schutz proportional auf alle untergeordneten UIDs und Prozesse verteilt (memory_recursiveprot). Dadurch können Diagnose- oder sekundäre Prozesse, die uid_1000 (android.uid.system) gemeinsam nutzen, Schutz von surfaceflinger und audioserver abziehen.

Um eine genaue Isolation pro Daemon zu ermöglichen, schreibt SystemMemoryBudgetManager bei jeder Aktualisierung Schutzmaßnahmen auf allen drei Ebenen der cgroup v2-Hierarchie, wie in Abbildung 1 dargestellt:

  1. Blattprozessknoten (pid_<pid>/memory.min): Auf die (skalierte) 90-%-Untergrenze des Arbeitssatzes des Prozesses festgelegt.
  2. UID-Knoten (uid_<uid>/memory.min): Wird auf die genaue Summe der verwalteten pid_*-Blatt-Floors festgelegt, sodass nicht verwaltete Prozesse, die diese UID gemeinsam nutzen (z. B. andere AID_SYSTEM-Daemons oder sharedUserId="android.uid.system"-Pakete), memory.min = 0 haben und keinen Schutz beanspruchen können.
  3. Slice auf oberster Ebene (/sys/fs/cgroup/system/memory.min und /sys/fs/cgroup/apps/memory.min): Auf die genaue Summe der untergeordneten uid_*-Elemente festgelegt. Alle veralteten memory.low-Werte auf verwalteten Knoten werden auf 0 gelöscht und alle uid_*-Verzeichnisse, die bei einer vorherigen Aktualisierung verwaltet wurden (z. B. ein vorheriger Home-Launcher, nachdem der Nutzer den Launcher gewechselt hat), werden auf 0 zurückgesetzt.

Dreistufige cgroup v2-memory.min-Schutzhierarchie über /sys/fs/cgroup/system und /sys/fs/cgroup/apps, wobei die Blattprozess-Mindestwerte in UID- und Slice-Knoten summiert werden, während ungeschützte gleichgeordnete Prozesse bei null bleiben

Abbildung 1. Dreistufige Delegierung des Schutzlevels über cgroup-Slices hinweg.

Warum „memory.min“ anstelle von „memory.low“ auf MGLRU-Kerneln verwendet wird

Unter Multi-Generational LRU (MGLRU, die Standard-Engine zur Seitenrückgewinnung auf Android 6.6 und neueren Kernels) verhalten sich memory.low und memory.min in mm/vmscan.c:shrink_one unterschiedlich:

  • memory.low (Zurückstellung mit einem Durchlauf): Eine cgroup, deren Nutzung unter memory.low liegt, wird an das Ende der memcg-LRU (MEMCG_LRU_TAIL) verschoben und für einen einzelnen Freigabedurchlauf übersprungen. Beim nächsten Durchlauf unter anhaltendem Arbeitsspeicherdruck wird die cgroup vollständig freigegeben und ein MEMCG_LOW-Ereignis protokolliert. In Stresstests auf physischen Geräten mit einem anonymen Speicherfresser, der auf 85% des physischen RAM anwuchs, ermöglichte memory.low, dass SystemUI auf einer Smartwatch von ca. 45 MB auf ca. 5 bis 10 MB und audioserver auf einem faltbaren Gerät mit 16 GB von 15 MB auf 2 MB bei einem Mindestwert von ca. 14 MB sanken.
  • memory.min (harte Schutzuntergrenze): Eine cgroup, deren Nutzung unter memory.min liegt, wird von der globalen Freigabe übersprungen. Wie in Abbildung 2 dargestellt, werden bei derselben Arbeitslast mit 85% RAM-Auslastung memory.min nur die kältesten 10% der einzelnen Daemons entfernt. SystemUI, surfaceflinger und audioserver werden bei 90% gehalten, ohne dass anonyme Refaults auftreten und ohne dass die Anzahl der lmkd -Prozessbeendigungen steigt.

memory.min bietet eine feste physische Reservierung, die dem Schutz des PinnerService-mlock-Pinnings entspricht, das es ersetzt. Der Vorteil ist, dass die Kernel-LRU (und nicht eine statische Dateiliste) auswählt, welche Hot-Code-, Heap- und Grafikseiten resident bleiben.

Vergleich des residenten Speichers des Daemons bei steigendem Systemarbeitsspeicherdruck von 40% auf 95% des physischen RAMs. „memory.min“ bleibt bei 90 %, während „memory.low“ nach einem einzelnen MGLRU-Verzögerungsdurchlauf einbricht.

Abbildung 2: Aufbewahrung des Daemon-Arbeitssatzes bei steigender Speicherauslastung.

Vorwärmen beim Booten und Freigabe des nicht zugeordneten Datei-Caches nach dem Booten

Wenn Sie die permanente mlock von config_defaultPinnerServiceFiles durch PinnerService ersetzen, müssen Sie zwei Verhaltensweisen beim Booten koordinieren:

  • Einmaliges Vorwärmen des Hintergrundstarts (PinnerService): Wenn PinnerService mlock übersprungen wird, ohne den Seitencache vorzuwärmen, werden beim Start von SystemUI (OnBootPhase_550) etwa 95 MB an SystemUIAPK-, ODEX- und VDEX-Laufwerkslesevorgängen auf den synchronen Start pfad verschoben, was die Startzeit um 300 bis 450 ms verlängert. Der Aufruf von madvise(MADV_WILLNEED) reicht nicht aus, da force_page_cache_ra im Kernel einen einzelnen MADV_WILLNEED-Aufruf auf ein Readahead-Fenster (normalerweise 128 KB bis 2 MB) beschränkt. Stattdessen ordnet PinnerService, wenn SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning aktiv ist, jede Datei in config_defaultPinnerServiceFiles während onStart einem Hintergrundthread zu, füllt die Datei synchron mit Os.mlock in den Seitencache und ruft sofort Os.munlock, munmap und close auf. Dadurch wird die Schnellstartzeit beibehalten, während keine Bytes dauerhaft gesperrt werden, sodass Seiten für den Kaltstart später entfernt werden können.
  • Ausschluss und Rückforderung des system_server-Boot-Paket-Scan-Cache: Während des Bootvorgangs scannt PackageManagerService installierte APKs und hinterlässt 1,5 bis 2,5 GB nicht zugeordneten Dateiseiten-Cache, der der system_server-Cgroup zugerechnet wird. Wenn der Kaltstart-Cache in die memory.min-Berechnung von system_server einbezogen wird, füllt er die globale RAM-Obergrenze von 10% aus und skaliert die Schutzuntergrenzen von surfaceflinger, SystemUI und audioserver proportional herunter. Um eine Obergrenzen-Unterversorgung zu verhindern, liest SystemMemoryBudgetManager memory.stat für system_server, berechnet den nicht zugeordneten Dateicache als max(0, file - shmem - file_mapped) und subtrahiert diesen nicht zugeordneten Cache von memory.current, bevor die 90-%-Untergrenze berechnet wird, wie in Abbildung 3 dargestellt. Außerdem führt der Manager 10 Sekunden nach PHASE_BOOT_COMPLETED einen einmaligen Schreibvorgang von <unmapped_file> swappiness=0 in den memory.reclaim-Knoten von system_server auf Linux 6.2 und neueren Kerneln aus, um den kalten Paketscan-Cache proaktiv zu entfernen, ohne anonymen Heap auszulagern oder zugeordnete DEX- und Codeseiten zu entfernen. Bei älteren Kernels (z. B. android14-6.1), die swappiness=0 mit EINVAL ablehnen, überspringt der Manager den Schreibvorgang memory.reclaim, anstatt auf einen unqualifizierten Schreibvorgang für die Rückgewinnung zurückzugreifen, der den anonymen Heap während des Bootvorgangs auslagern würde.

Zeitachse des system_server-Arbeitsspeichers nach dem Booten mit den geometrischen Settle-Aktualisierungen nach 1, 4, 16 und 64 Sekunden, Ausschluss des nicht zugeordneten Boot-Paket-Scan-Cache aus dem 90 %-memory.min-Floor und einmaligem memory.reclaim mit swappiness=0

Abbildung 3: Konvergenz der Schutzuntergrenze und Cache-Rückgewinnung nach dem Booten.

Kernel-Anforderungen und Overlays für die Gerätekonfiguration

SystemMemoryBudgetManager basiert auf mem_cgroup_calculate_protection im Linux-Kernel, um memory.min über verschachtelte cgroups zu verteilen. Die Android Common Kernels android15-6.6 und android16-6.12 bieten standardmäßig vollständige Unterstützung für den verschachtelten MGLRU-Schutz. Geräte mit android14-6.1 und aktiviertem MGLRU erfordern drei Upstream-Backports in mm/vmscan.c und mm/memcontrol.c, damit leere Zwischen-Slice- und UID-cgroups den Schutz auf Blattprozess-cgroups übertragen:

  • 30d77b7eef01 (mm/mglru: fix ineffective protection calculation)
  • 8aa420617918 (mm/mglru: respect min_ttl_ms with memcgs)
  • adb8213014b2 (mm: memcg: fix stale protection of reclaim target memcg)

OEMs können SystemMemoryBudgetManager mithilfe der folgenden Framework-Ressourcen-Overlays anpassen oder deaktivieren:

  • config_systemMemoryBudgetLowKb (Ganzzahl, KB): 0 (Standard) begrenzt die Summe aller memory.min-Untergrenzen auf 10% des gesamten physischen RAM. Ein positiver Wert legt eine explizite Obergrenze in KB fest und ein negativer Wert deaktiviert SystemMemoryBudgetManager auf dem Gerät (wobei das alte statische mlock-Pinning von PinnerService beibehalten wird).
  • config_systemMemoryBudgetHighKb (Ganzzahl, KB): 0 (Standard) belässt /sys/fs/cgroup/system/memory.high bei max. Ein positiver Wert legt eine Obergrenze für das Top-Slice-memory.high fest (muss größer oder gleich der Untergrenze sein).
  • config_systemMemoryBudgetAdditionalSystemProcesses (String-Array): Listet zusätzliche latenzkritische native System- oder Anbieter-Daemon-Prozessnamen in /sys/fs/cgroup/system auf (abgeglichen mit /proc/<pid>/comm oder dem Basisnamen von argv[0] in /proc/<pid>/cmdline für System- und Anbieter-UIDs unter 10000), die neben den sechs Standard-System-Daemons mit 90% memory.min-Untergrenzen geschützt werden sollen. Verwenden Sie dieses Overlay, anstatt memory.min in init.rc festzulegen, da SystemMemoryBudgetManager uid_<uid>/memory.min als Summe der verwalteten Blattprozesse neu berechnet und nicht verwaltete uid_*-Knoten bei jeder Aktualisierung löscht. Ein XR-Gerät kann beispielsweise seine Spatial-Tracking- und Kamera-HAL-Daemons mit dem folgenden Overlay schützen:

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (String): Paketname der SystemUI-App der Plattform, aufgelöst mit MATCH_SYSTEM_ONLY.

Führen Sie den folgenden Befehl aus, um aktive SystemMemoryBudgetManager-Untergrenzen und cgroup-Knoten auf einem Gerät zu prüfen:

# Inspect policy state, resolved PIDs, and applied floors in dumpsys meminfo
adb shell dumpsys meminfo | sed -n '/System Memory Budgets/,$p'
# Inspect raw cgroup v2 memory.min nodes across the system and apps slices
adb shell head -n 1 \
  /sys/fs/cgroup/system/memory.min \
  /sys/fs/cgroup/system/uid_*/memory.min \
  /sys/fs/cgroup/system/uid_*/pid_*/memory.min \
  /sys/fs/cgroup/apps/memory.min \
  /sys/fs/cgroup/apps/uid_*/memory.min \
  /sys/fs/cgroup/apps/uid_*/pid_*/memory.min

Persistente und im Hintergrund ausgeführte System-Apps mit Arbeitsspeicherbudgets begrenzen

SystemMemoryBudgetManager schützt interaktive Daemons mithilfe von memory.min-Untergrenzen vor der Rückforderung. Viele andere System-Apps und privilegierte Plattformdienste werden jedoch über längere Zeiträume im Hintergrund als PERSISTENT-Prozesse, gebundene Dienste im Vordergrund oder häufig abgefragte Contentanbieter ausgeführt. Ohne explizite Arbeitsspeicher-Obergrenzen werden diese Prozesse erst dann eingeschränkt, wenn sie die Memory Limiter-Limits von mehreren Gigabyte erreichen (z. B. 4 GB bis 10 GB für sichtbare Apps und 2 GB bis 5 GB für Hintergrund-Apps) oder wenn die globale Arbeitsspeicherauslastung systemweite kswapd- und Low-Memory-Killer-Aktivitäten (lmkd) auslöst.

Wenn sie nicht eingeschränkt werden, sammeln sich in Hintergrundsystemprozessen oft große Mengen an Cold Memory an, der nicht Teil ihres aktiven Arbeitssatzes ist:

  • Unbegrenzter Dateiseitencache: Dienste, die Medien indexieren, Datenbanken durchsuchen oder Kalender- und Anbietertabellen synchronisieren (z. B. AppSearch, MediaProvider und CalendarProvider), ordnen große Dateien auf dem Laufwerk zu oder streamen sie. Ohne eine memory.high-Obergrenze für die cgroup werden bereinigte dateibasierte Seiten aus einmaligen Lesevorgängen auf unbestimmte Zeit der Prozess-cgroup in Rechnung gestellt, was Hunderte von Megabyte physischen RAM verbraucht.
  • Anonyme Heap-Aufbewahrung nach Aktivitätsspitzen:Dienste mit vorübergehenden Aktivitätsspitzen, z. B. Bluetooth-Audio- und BLE-Erkennungsspitzen, NetworkStack-TLS-Validierung für Captive Portale oder UI-Hilfsprogramme, die in den Hintergrund wechseln, können kalte anonyme Heap-Seiten noch lange nach Abschluss der Aktivitätsspitze aufbewahren.

Die Anwendung kalibrierter <memory-budget>-Deklarationen (memory.high-Soft-Limits, in der Regel 30 MB bis 190 MB) auf System-Apps bietet zwei Hauptvorteile:

  • Proaktive, lokalisierte Seitenrückgewinnung: Wenn ein Systemprozess das konfigurierte memory.high-Budget überschreitet, weist der Linux-Kernel die Seitenrückgewinnung speziell dieser cgroup zu. Dabei werden zuerst kalte dateibasierte Seiten entfernt und inaktive anonyme Seiten in ZRAM komprimiert, ohne auf globalen Gerätespeicherdruck zu warten.
  • Mehr verfügbarer RAM für Apps im Vordergrund und den Cache-App-Pool:Durch die Begrenzung von Cold-File-Caches in Always-On-Systemdiensten werden auf dem Gerät Dutzende bis Hunderte Megabyte physischer RAM freigegeben. Dadurch werden UI-Ruckler reduziert, die Kamera wird schneller gestartet und mehr Nutzer-Apps bleiben im Cache-Status aktiv.

Speicherbudgets für System-Apps konfigurieren

System-Apps verwenden dieselbe <memory-budget>-Manifestsyntax, Prozessstatus-Kaskadierung, Skalierung der Anzeigedichte und Auflösungsregel für die letzte übereinstimmende Klausel wie allgemeine Apps. Eine vollständige Liste der Attribute finden Sie in der XML-Attributreferenz.

OEMs können <memory-budget>-Elemente direkt in der AndroidManifest.xml einer System-App deklarieren oder sie mithilfe des Attributs additional_manifests in Android.bp aus einer separaten XML-Datei zusammenführen. Gemäß der Konvention speichert das AOSP die Budgets für Referenzsystem-Apps in eigenständigen *-budget.xml-Dateien (entweder in frameworks/base/core/memory_budgets/ für Kernplattformpakete oder zusammen mit einzelnen Modulen wie packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/ und packages/services/Telephony/) und hängt sie mit additional_manifests an:

android_app {
    name: "TeleService",
    manifest: "AndroidManifest.xml",
    additional_manifests: ["Telephony-budget.xml"],
    // ...
}

Paketebene (UID) im Vergleich zur Prozessebene (PID)

Ob du ein Baseline-Budget auf Paketebene (<application>) oder ein Budget für mehrere Prozesse (<processes><process>) deklarierst, bestimmt, ob der Kernel memory.high auf der UID-cgroup-Ebene (/sys/fs/cgroup/uid_<uid>/memory.high) oder auf der PID-cgroup-Ebene (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high) erzwingt, wie in Abbildung 4 dargestellt:

  • Budget auf Paketebene (<application>):Verwende diese Option, wenn das APK einen einzelnen Prozess unter seiner eigenen dedizierten UID ausführt (z. B. Bluetooth, Nfc, NetworkStack, CalendarProvider, Traceur oder AppSearch).
  • Budget auf Prozessebene (<processes><process>):Verwende diese Option, wenn das APK android:sharedUserId (z. B. android.uid.phone mit UID 1001 oder android.uid.system mit UID 1000) mit anderen Systempaketen teilt, damit andere Prozesse, die diese UID teilen, nicht zusammen gedrosselt werden, oder wenn ein einzelnes APK mehrere Prozesse mit unterschiedlichen Speicherrollen ausführt (z. B. den Hintergrund-Daemon MediaProvider und den UI-Unterprozess :PhotoPicker).

Cgroup v2-Hierarchie, die die Durchsetzung von „memory.high“ auf UID-Ebene für dedizierte UIDs mit der Durchsetzung von „memory.high“ pro PID für freigegebene System-UIDs und APKs mit mehreren Prozessen vergleicht

Abbildung 4: Speicherobergrenzen auf UID-Ebene im Vergleich zu PID-Ebene.

Im folgenden Beispiel wird MediaProvider-budget.xml gezeigt, wodurch RAM-gestaffelte Budgets auf den Standardprozess MediaProvider (<process>) und ein display-scaled budget auf den UI-Prozess :PhotoPicker beschränkt werden. Jedes android:maxDeviceRamMb-Attribut entspricht Geräten, deren physischer RAM (MemTotal) insgesamt dem angegebenen MB-Grenzwert entspricht oder darunter liegt. Wenn kein Grenzwert angegeben ist, gilt das Attribut für alle RAM-Kapazitäten. Die Klauseln sind von der uneingeschränkten Standardeinstellung bis zum kleinsten maxDeviceRamMb-Schwellenwert sortiert, sodass die Auflösungsregel für die letzte übereinstimmende Klausel die engste anwendbare Stufe auswählt:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.android.providers.media.module">
    <uses-sdk android:targetSdkVersion="10000" />
    <application>
        <processes>
            <process>
                <memory-budget
                    android:maxMb="85" />
                <memory-budget
                    android:maxMb="75"
                    android:maxDeviceRamMb="12288" />
                <memory-budget
                    android:maxMb="60"
                    android:maxDeviceRamMb="8192" />
            </process>
            <process android:process=":PhotoPicker">
                <memory-budget
                    android:maxMb="85"
                    android:additionalBytesPerDisplayPixel="25" />
            </process>
        </processes>
    </application>
</manifest>

Wie AOSP-Referenzbudgets auf Google Pixel-Geräten kalibriert wurden

Die Referenzspeicherbudgets für System-Apps im AOSP wurden auf Google Pixel-Geräten mit 6 GB bis 16 GB physischem RAM (einschließlich Smartphones, faltbaren Geräten und Tablets mit 6 GB, 8 GB, 12 GB und 16 GB) und Displayauflösungen von ca. 1, 2 MP (720 × 1.600) bis ca. 4, 5 MP (2.076 × 2.152) kalibriert.

Für die Kalibrierung wurden zwei sich ergänzende Datenquellen kombiniert: flottenweite cgroup v2-Feldtelemetrie und kontrollierte memory.high-Sweeps auf physischen Geräten für wichtige Nutzerverhalten (Critical User Journey, CUJ).

Flottenweite cgroup v2-Telemetrieanalyse

Es wurden regelmäßig cgroup v2-Snapshots auf allen Geräten erfasst und nach physischer RAM-Stufe (≤ 8 GB, 12 GB und ≥ 16 GB) und Prozessstatus-Bucket (foreground, perceptible, background und cached) aufgeteilt. Die statistische Analyse wurde anhand von sechs Prinzipien durchgeführt:

  • memory.current in Arbeitssatz und wiederherstellbaren Dateicache zerlegen: Da memory.high die residente cgroup-Gebühr (memory.current) regelt, wurde jeder Snapshot mit memory.stat in anonymen Heap (anon), ausgelagerte anonyme Seiten (swap) und dateigestützten Seitencache (file) zerlegt, wie in Abbildung 5 dargestellt. Der gesamte inkomprimierbare anonyme Bedarf (anon + swap bei P99) legt die Mindestgröße des Arbeitssatzes für das Budget fest, damit der Prozess nicht Gefahr läuft, dass der ZRAM erschöpft wird. Eine große Lücke zwischen memory.current P99 und anon + swap P99 weist auf einen Dateiseitencache hin, den memory.high zurückfordern kann, sofern der Cache nicht zugeordnet und kalt ist.
  • Speicher berücksichtigen, den memory.high nicht freigeben kann: Nicht alle Dateien und freigegebenen Speicher sind Cold Cache. Gesperrte Seiten (unevictable in memory.stat, z. B. mit mlock gesperrter gemeinsam genutzter Arbeitsspeicher oder der gesperrte Gast-RAM einer virtuellen Maschine, deren Hostprozesse der cgroup der App zugerechnet werden) legen eine feste Untergrenze für das Budget fest. Zugeordnete Seiten (file_mapped) sind oft Teil des aktiven Arbeitssatzes. Prüfen Sie unevictable, shmem und file_mapped, bevor Sie Dateispeicher als freigebbar behandeln. Beispiel: Etwa 100 MB des AppSearch-Speichers sind gesperrt und können nicht entfernt werden. Das Budget deckt also diese Untergrenze sowie den aktiven Host-Arbeitssatz ab, wobei Spielraum für das Wachstum der gesperrten Region vorhanden ist.
  • Verwässerung im inaktiven Zustand herausfiltern:Contentanbieter (CalendarProvider) und On-Demand-Entwicklertools (Traceur) verbringen 70% bis 99,9% der Feld-Snapshots im inaktiven Zustand mit etwa 1 MB bis 3 MB. Die Berechnung von Rohperzentilen über alle Snapshots hinweg verwässert die P95- und P99-Schätzungen erheblich. Stattdessen wurden Perzentile und Konfidenzintervalle über unverdünnte aktive Snapshots berechnet (wobei CACHED-Snapshots und inaktive Baseline-Snapshots herausgefiltert wurden).
  • Gerätebezogene Konfidenzintervalle verwenden:Da ein einzelnes Gerät im Laufe der Zeit viele regelmäßige Snapshots liefert, wird die Unsicherheit bei der Behandlung von Snapshots als unabhängige Stichproben unterschätzt. Alle Schätzungen der Arbeitssätze für das 95. und 99. Perzentil wurden mit konservativen, gerätebasierten Konfidenzintervallen von 95 % bewertet. Dabei wurde die Anzahl der einzelnen Geräte (N_dev) als effektive Stichprobengröße verwendet.
  • Berücksichtigen Sie vorübergehende Spitzen mit memory.peak: Bei regelmäßigen Stichproben können kurzlebige Spitzen mit hoher Zuweisung übersehen werden (z. B. TLS-Validierung des Captive Portals von NetworkStack und gleichzeitige DNS-Prüfungen oder Initialisierung von Bluetooth A2DP und BLE). Bei Diensten mit Lastspitzen wurden die Budgets so dimensioniert, dass sie das 90. bis 95. Perzentil der Höchstwerte für die Kernel-memory.peak-Lebensdauer über alle RAM-Stufen hinweg abdecken.
  • Prüfen, ob eine RAM-Stufe den RAM und nicht die Gerätegeneration widerspiegelt:Neuere Geräte haben in der Regel mehr RAM und mehr Funktionen. Ein Working Set, das mit der RAM-Stufe wächst, kann also tatsächlich mit den Funktionen wachsen. Bevor Sie eine maxDeviceRamMb-Stufe hinzufügen, sollten Sie die Daten nach Gerätemodell aufteilen. Beispiel: com.android.phone erreicht auf Smartphones mit 12 GB und 16 GB derselben Generation denselben P99-anon + swap (~85 bis 90 MB), während ein älteres faltbares Gerät mit 12 GB 40 MB verwendet. Das Budget verwendet daher einen einzelnen Wert ohne RAM-Stufe, damit ein Gerät mit weniger RAM, das die neueren Telefoniefunktionen bietet, nicht unterbudgetiert ist.

Aufschlüsselung des cgroup-Arbeitsspeichers im Zeitverlauf in nicht komprimierbaren anonymen Arbeitssatz, aktives Dateicache-Fenster und kalten einmaligen Dateicache, der über memory.high hinaus freigegeben wird

Abbildung 5: Arbeitssatz im Vergleich zu wiederherstellbarem Datei-Cache.

CUJ für physische Geräte und Sweeps für die Displayskalierung

Um die aus der Telemetrie abgeleiteten Budgets zu validieren und die displayproportionalen Budgets (z. B. :PhotoPicker) und die zustandsspezifischen Hintergrundbudgets (z. B. Traceur) zu kalibrieren, wurden Live-memory.high-Sweeps auf physischen Geräten mit 6 GB bis 16 GB RAM ausgeführt:

  • Null-Refault-Knie finden:Während der Ausführung von End-to-End-CUJs (z. B. schnelles Scrollen im Fotoraster, Durchsuchen von Alben, Wischen durch hochauflösende Foto- und Videovorschauen im Vollbildmodus sowie Aufzeichnen und Speichern von Traces) wurde memory.high schrittweise verringert, während die cgroup-Zähler memory.stat (workingset_refault_anon, workingset_refault_file, pgscan und pgsteal) und memory.events (high) gemessen wurden. Wie in Abbildung 6 dargestellt, liegt das optimale Budget knapp über dem Null-Refault-Knie. Der Zero-Refault-Knick ist der Schwellenwert, bei dem der Kernel den kalten Einmal-Dateicache (pgsteal > 0) zurückfordert, ohne dass es zu einem aktiven Working-Set-Thrashing (workingset_refault == 0) kommt.
  • Validierung der Skalierung von Displaypixeln bei verschiedenen Auflösungen: Tests von :PhotoPicker mit vier physischen und emulierten Displayauflösungen von ca. 1,2 MP (720 × 1.600) bis ca. 4,5 MP (2.076 × 2.152, aufgeklapptes Innendisplay) haben bestätigt, dass die aktive Bitmap-Arbeitsgruppe von Glide und ViewPager2 linear mit der Anzahl der Displaypixel skaliert. Die Baseline ohne Display variiert ebenfalls je nach Gerät: Bei fast gleicher Pixelanzahl lag der Null-Refault-Knick bei einem faltbaren Gerät mit 16 GB RAM und einem äußeren Display bei etwa 140 MB, während er bei einem Smartphone mit 6 GB RAM bei 110 bis 125 MB lag. Eine Baseline von 85 MB plus 25 Byte pro Displaypixel (android:maxMb="85" und android:additionalBytesPerDisplayPixel="25") bleibt auf jedem getesteten Display über dem gemessenen Knick (~110 MB auf HD+, ~145 MB auf FHD+ und ~190 MB auf aufgeklappten Displays).
  • ART-Garbage-Collection im Hintergrund prüfen:In cgroup v2 werden von Zygote freigegebene boot.art- und Framework-Image-Seiten weiterhin der cgroup von Zygote zugerechnet und während der ART-Garbage-Collection mit gleichzeitiger Markierung und Komprimierung (Concurrent Mark-Compact, CMC) als immuner Bereich behandelt. Stresstests haben bestätigt, dass beim Übergang einer System-UI-App zu background durch die Verschärfung von memory.high (z. B. auf 30 MB bei Traceur) der private Heap und der Seitencache sauber begrenzt werden, ohne dass es zu GC-Thrashing oder Swap-Aktivitäten kommt.

Diagramm der Rückgewinnung von kaltem Cache (pgsteal) und des Thrashings des Arbeitssatzes (workingset_refault), während memory.high während einer CUJ schrittweise verringert wird, wobei der Null-Refault-Knick und das Zielbudget hervorgehoben werden

Abbildung 6. Den Punkt finden, an dem die Anzahl der Fehler während einer kritischen User Journey auf null sinkt.

Zusammenfassung der Budgets für AOSP-Referenzsystem-Apps

In der folgenden Tabelle sind die kalibrierten Referenzbudgets im AOSP für die verschiedenen RAM-Stufen und Prozessstatus zusammengefasst:

Systempaket oder -prozess Umfang ≤ 8 GB RAM 12 GB RAM ≥16 GB RAM Primärer Kalibrierungstreiber
com.android.bluetooth UID (<application>) 80 MB 95 MB 110 MB Deckt P90-Lebensdauer-memory.peak-Bursts (~75 bis 110 MB) und >2x P99-aktive memory.current ab.
com.android.nfc UID (<application>) 50 MB 60 MB 60 MB Deckt die aktive Host Card Emulation P99 (bis zu ~50 MB) und P90 bis P95 memory.peak ab, während der Cold-File-Cache gekürzt wird.
com.android.providers.calendar UID (<application>) 70 MB Standard (Vordergrund)
50 MB perceptible und background
Der aktive Arbeitssatz ist über alle RAM-Klassen hinweg unveränderlich. Das Hintergrundbudget hält anonymen Spielraum für Erinnerungsalarme bereit, während der SQLite-Seiten-Cache während der Synchronisierung gekürzt wird.
com.android.traceur UID (<application>) 85 MB Standard (Vordergrund und wahrnehmbar)
30 MB background
Behält während der aktiven Trace-Aufzeichnung und des Exports (TraceService) die vollen 85 MB Headroom bei, während der Ruhe- und Broadcast-Status auf 30 MB begrenzt wird.
com.android.providers.media.module (Hauptschlaf) PID (<process>) 60 MB 75 MB 85 MB Bietet etwa 10 bis 30 MB mehr Spielraum als P99 anon + swap, während kalte SQLite- und Thumbnail-Dateicaches freigegeben werden.
com.android.providers.media.module:PhotoPicker PID (<process>) 85 MB + 25 Byte pro Pixel
(~110 MB bei HD+, ~145 MB bei FHD+, ~190 MB bei ~4,5 MP Fold)
Wird proportional zur Anzahl der Displaypixel (additionalBytesPerDisplayPixel="25") skaliert, um dem Knickpunkt des Glide-Bitmap-Arbeitssatzes zu entsprechen.
com.android.networkstack UID (<application>) 65 MB 70 MB 85 MB Hauptsächlich anonymer Arbeitsspeicher, der auf die P95-Lebensdauer von memory.peak dimensioniert ist, um TLS-, DNS- und DHCP-Bursts von Captive Portals abzudecken.
com.android.appsearch.apk UID (<application>) 130 MB Deckt ca. 100 MB gesperrten, nicht entfernbaren Arbeitsspeicher plus einen ca. 5 MB großen Hot-Host-Arbeitssatz ab, mit Spielraum für das Wachstum des gesperrten Bereichs. Null Refaults bei ≥ 125 MB, während der kalte Seitencache (bis zu ~330 MB bei P99) freigegeben wird.
com.android.phone (TeleService) PID (<process>) 120 MB Auf PID-Ebene auf der gemeinsam genutzten UID 1001 beschränkt. Der Arbeitssatz verfolgt die Gerätegeneration und nicht den RAM. Daher liegt ein einzelnes Budget etwa 30 MB über dem höchsten P99-anon + swap pro Modell.

Wie OEMs die Budgets für System-Apps für ihre eigenen Geräte neu kalibrieren können

OEMs, die System-Apps anpassen (z. B. durch Hinzufügen von Bluetooth-Profilen von Drittanbietern, benutzerdefinierten Telefonieerweiterungen oder OEM-Medienindexern) oder zusätzliche vorinstallierte Systemdienste bereitstellen, können vorhandene Budgets neu kalibrieren oder neue *-budget.xml-Dateien für ihr Hardwareportfolio erstellen.

Aktive cgroup v2-Speicherprofile erfassen

Lesen Sie für jeden Zielsystemprozess die cgroup v2-Speicherknoten unter /sys/fs/cgroup/uid_<uid>/pid_<pid>/ für repräsentative Arbeitslasten und RAM-Stufen:

  • memory.current: Aktueller residenter Speicher, der der cgroup in Rechnung gestellt wird (ohne von Zygote freigegebene Seiten)
  • memory.peak: Höchstwert für memory.current seit Prozessstart
  • memory.swap.current: Aktuelle komprimierte Auslagerungsnutzung in ZRAM
  • memory.stat: Aufschlüsselung von anon, file, pgscan, pgsteal, workingset_refault_anon und workingset_refault_file
  • memory.events: Kumulative Anzahl von high-Verstößen (memory.high-Drosselungs- und Rückforderungsereignisse)

Wenn du Telemetriedaten von Dogfood- oder Feldgeräten aggregierst, trenne aktive Prozesszustände (TOP, FOREGROUND_SERVICE, aktive Bindungen und aktive Synchronisierungen) von inaktiven CACHED- oder inaktiven PERSISTENT-Zuständen, damit inaktive Snapshots deine P95- und P99-Berechnungen für den Arbeitssatz nicht nach unten verzerren.

Nicht komprimierbaren Heap vom wiederherstellbaren Datei-Cache trennen

Vergleichen Sie das 99. Perzentil von anon + swap mit dem 99. Perzentil von memory.current:

  • Dienste, die vom Dateicache dominiert werden (file >> anon, z. B. Datenbankanbieter, Suchindexer und Log-Daemons): Legen Sie android:maxMb so fest, dass ein ausreichender Spielraum (normalerweise das 1,5- bis 4-Fache) über P99 anon + swap plus ein Arbeitsfenster für häufig verwendete Dateiseiten vorhanden ist. Mit dieser Konfiguration kann der Kernel kontinuierlich kalte, nicht zugeordnete Dateiseiten entfernen, ohne den anonymen Heap zu berühren. Wenn unevictable oder file_mapped groß ist (wie bei gesperrtem Arbeitsspeicher oder speicherzugeordneten Datenbanken), wird dieser Speicherbedarf ebenfalls als Arbeitssatz gezählt.
  • Von anonymen Heaps dominierte oder bursty Dienste (anon macht den Großteil von memory.current aus, z. B. Netzwerk- oder Konnektivitätsstacks): Dimensioniere android:maxMb so, dass P90 bis P95 von memory.peak abgedeckt werden, damit vorübergehende Bursts keine synchrone ZRAM-Auslagerung auf latenzempfindlichen Threads auslösen.
  • Display-intensive UI-Prozesse (z. B. Auswahl, Galerien oder System-UI-Hilfsprogramme): Miss den nicht-UI-bezogenen Baseline-Speicherbedarf und den inkrementellen Bitmap-Speicherbedarf pro Megapixel auf deinen Displays mit der niedrigsten und höchsten Auflösung und gib das Budget mit android:maxMb plus android:additionalBytesPerDisplayPixel an.

„memory.high“ auf physischen Geräten bereinigen

Bevor du ein neues Budget für den Build festlegst, verwende am memory-limiter manual (oder schreibe bei einem Userdebug-Build direkt in den memory.high-cgroup-Knoten des Prozesses), um mögliche Limits zu testen, während du deine automatisierten CUJ-Tests ausführst:

# Override memory.high for a specific PID to 60 MB during a CUJ test
adb shell am memory-limiter manual <PID> 60MB

Beobachten Sie memory.stat und memory.events vor und nach der CUJ:

  • Budget ist zu niedrig: workingset_refault_anon oder workingset_refault_file steigt während der CUJ stark an, memory.swap.current wächst während der aktiven Arbeit im Vordergrund kontinuierlich oder die CUJ-Frame- oder Binder-Latenz verschlechtert sich. Erhöhen Sie das Budget über den Refault-Knick hinaus.
  • Budget ist zu hoch: memory.events:high bleibt auch nach hoher Arbeitslast durch Dateistreaming oder Indexierung bei 0 und file in memory.stat wächst unbegrenzt mit kalten Seiten. Reduzieren Sie das Budget auf den aktiven Arbeitssatz.
  • Budget ist gut kalibriert: Der stabile Zustand memory.current bleibt im Normalbetrieb unter memory.high. Bei starken Datei- oder Cache-Bursts werden von pgsteal kalte Dateiseiten zurückgefordert, während workingset_refault_anon nahe null bleibt und die CUJ-Latenz unverändert ist.