Limitador de memória

O Android 17 e versões mais recentes incluem o Limitador de Memória, um serviço do sistema que monitora e limita o uso da memória de processos de aplicativos usando o cgroup v2 do Linux. O limitador de memória impede que apps individuais consumam memória do sistema de forma desproporcional, o que reduz a pressão geral da memória e evita o encerramento agressivo de processos críticos em primeiro plano e em cache por falta de memória.

Mecanismo

O Limitador de Memória se integra ao Activity Manager Service (AMS) para rastrear eventos de ciclo de vida do processo e mudanças de estado. O limitador de memória aplica limites de memória usando a hierarquia cgroup v2 do kernel do Linux.

Para oferecer suporte ao limitador de memória, o kernel do dispositivo precisa ativar o cgroup v2 e o controlador memory. O serviço depende especificamente dos seguintes atributos:

memory.high
Um limite de recuperação flexível aplicado ao cgroup do UID do pacote ou a um cgroup do PID do processo quando um orçamento de memória por processo está ativo. O padrão é 90% do limite de memória da plataforma, a menos que seja reduzido por um orçamento de memória. Quando excedido, o kernel limita as alocações e recupera páginas desse cgroup.
memory.max
Um limite rígido de memória aplicado ao cgroup do UID do pacote em 100% do limite de memória da plataforma. Se a recuperação não conseguir manter o uso da memória abaixo de memory.max, o kernel vai encerrar o processo de falta de memória.
memory.swap.max
Limita o espaço de troca máximo, como ZRAM, que cada cgroup de PID de processo pode usar.

Requisitos de cgroup e kernel

Os dispositivos com o Android 17 (nível 37 da API) e versões mais recentes precisam atender aos seguintes requisitos de cgroup v2 e kernel para limites de plataforma, orçamentos de manifesto e MemoryBudgetManager no nível 37.2 da API e versões mais recentes:

  • Ativação do cgroup por processo: mantenha "NeedsActivation": true no controlador memory do cgroup v2 em /system/etc/cgroups.json para que o init inicial ative +memory antes de criar /sys/fs/cgroup/apps/uid_<UID>/pid_<PID> e /sys/fs/cgroup/system/uid_<UID>/pid_<PID>. Não desative "NeedsActivation", defina ro.config.per_app_memcg=false nem adicione substituições de fornecedor cgroups.json que desativem cgroups de memória por UID ou por processo. Se /vendor/etc/task_profiles.json substituir atributos do controlador de memória, como MemLimit ou MemSoftLimit, mantenha "File" não vazio e inclua mapeamentos de "FileV2" nos nomes de arquivo do cgroup v2.

  • Permissões de arquivo do Cgroup: os arquivos de controle memory.high, memory.max e memory.swap.max pai uid_<UID> e por processo pid_<PID> em /sys/fs/cgroup/apps e /sys/fs/cgroup/system precisam ser graváveis por system_server no UID ou GID 1000 para que os limites de UID, os orçamentos de processo e os limites de troca sejam aplicados sem erros EACCES. Os arquivos memory.current, memory.events, memory.events.local, memory.stat e memory.high por UID e por processo, além de memory.max e memory.swap.max por UID, precisam ser legíveis pelo UID do app proprietário para que as consultas MemoryBudgetManager e os listeners acima do orçamento operem no processo.

  • Isolamento de PSI local: isole as paradas de recuperação por cgroup memory.high das informações de parada de pressão de memória global (PSI, na sigla em inglês) incluindo a mudança de PSI do memcg local da GKI e mantendo record_psi=false em qualquer gancho de fornecedor android_vh_mem_cgroup_handle_over_high. Sem o isolamento de PSI, a limitação localizada de memory.high aumenta o PSI de memória global e pode acionar encerramentos desnecessários de processos lmkd quando a memória de todo o sistema não está sob pressão.

Impacto nos apps

Os apps que operam dentro dos limites de memória não são afetados pelo limitador de memória.

Quando um app ultrapassa o limite de memory.high, o kernel remove a memória limpa com suporte a arquivos do app e troca a memória anônima inativa para manter o app dentro do limite. Como resultado dessa atividade de remoção e troca de páginas, o app pode sofrer uma limitação temporária de execução, mas continua em execução.

Em situações extremas, como um vazamento de memória descontrolado em que o app continua alocando memória além de memory.max ou depois que o espaço de troca é esgotado, o app não consegue alocar memória e é encerrado.

Monitoramento de processos

O limitador de memória aplica os limites da plataforma nesta página aos processos do app (UID >= 10000). Os processos com um UID abaixo de 10000 não recebem limites de plataforma, nem o pacote do serviço de inferência em sandbox padrão no dispositivo (config_defaultOnDeviceSandboxedInferenceService). Os apps e serviços do sistema ainda podem declarar orçamentos de memória mais restritos, conforme descrito em Orçamentos de memória do sistema.

O limitador de memória atribui limites de memória com base no estado de visibilidade do processo:

  • Processos visíveis: processos que hospedam uma interface visível para o usuário, como a atividade em foco ou uma interação ativa. Como a exibição de uma interface requer um conjunto de trabalho maior para pipelines de renderização e gráficos, os processos visíveis recebem um limite de memória mais generoso.

  • Processos não visíveis: processos que realizam trabalho em segundo plano sem apresentar uma interface, como serviços em segundo plano ou broadcast receivers. Como eles não renderizam componentes de IU, operam sob um limite mais restritivo.

A tabela a seguir mapeia estados de processo específicos para classificações de limite de memória da plataforma:

Estado do processoLimite de memória
PERSISTENT Irrestrito
PERSISTENT_UI Irrestrito
TOP visible
BOUND_TOP visible
FOREGROUND_SERVICE not-visible
BOUND_FOREGROUND_SERVICE not-visible
IMPORTANT_FOREGROUND visible
IMPORTANT_BACKGROUND not-visible
TRANSIENT_BACKGROUND not-visible
BACKUP not-visible
SERVICE not-visible
RECEIVER not-visible
TOP_SLEEPING visible
HEAVY_WEIGHT not-visible
HOME not-visible
LAST_ACTIVITY not-visible
CACHED_ACTIVITY not-visible
CACHED_ACTIVITY_CLIENT not-visible
CACHED_RECENT not-visible
CACHED_EMPTY not-visible

Os processos PERSISTENT e PERSISTENT_UI não recebem um limite de plataforma. Se um desses processos declarar um orçamento de memória, o orçamento em primeiro plano será aplicado, conforme descrito em Buckets de orçamento de memória.

No estado em cache, os processos mantêm o limite da plataforma not-visible, além do congelamento e da compactação de apps em cache, mantendo-os limitados mesmo que o congelamento ou a recuperação máxima não ocorram.

Quando um processo excede o limite de memory.high atribuído, o Limitador de Memória detecta o evento e pode acionar ações de diagnóstico, como capturar um Memory Profiler ou registrar uma anomalia no statsd.

Buckets de orçamento de memória

No nível 37.2 da API e versões mais recentes, os apps e os serviços do sistema podem declarar orçamentos de memória mais restritos do que os limites da plataforma ou definir orçamentos de execução mais restritos com MemoryBudgetManager. Os apps configuram orçamentos conforme descrito em Definir orçamentos de memória do app. Para apps e serviços do sistema, consulte Orçamentos de memória do sistema. Um orçamento define memory.high e não pode exceder o limite da plataforma para o mesmo estado do processo, e um orçamento de ambiente de execução não pode exceder um orçamento de manifesto aplicável.

Quando um processo declara um orçamento de manifesto, o Limitador de Memória mapeia o estado do processo para um dos três buckets de orçamento, enquanto um orçamento de ambiente de execução aplica um único limite até que seja atualizado ou limpo. Os valores de manifesto android:state foreground, perceptible e background especificam o orçamento para o bucket correspondente. Os processos em cache nos estados de CACHED_ACTIVITY a CACHED_EMPTY mantêm o orçamento de background, além do congelamento e da compactação de apps em cache.

Estado do processoIntervalo de orçamento
PERSISTENTPrimeiro plano
PERSISTENT_UIPrimeiro plano
TOPPrimeiro plano
BOUND_TOPPrimeiro plano
TOP_SLEEPINGPrimeiro plano
FOREGROUND_SERVICEPerceptível
BOUND_FOREGROUND_SERVICEPerceptível
IMPORTANT_FOREGROUNDPerceptível
IMPORTANT_BACKGROUNDPerceptível
TRANSIENT_BACKGROUNDContexto
BACKUPContexto
SERVICEContexto
RECEIVERContexto
HEAVY_WEIGHTContexto
HOMEContexto
LAST_ACTIVITYContexto
CACHED_ACTIVITYContexto
CACHED_ACTIVITY_CLIENTContexto
CACHED_RECENTContexto
CACHED_EMPTYContexto

Requisitos de implementação do dispositivo

Os orçamentos pertencem ao app. Um app pode definir, mudar ou limpar os próprios orçamentos de execução usando MemoryBudgetManager, por exemplo, em resposta à própria configuração remota, e só muda os orçamentos do manifesto por uma atualização do app. As implementações de dispositivos, incluindo mudanças em system_server, serviços de fornecedores e indicadores de um framework de OEM ou app complementar, não podem definir, aumentar, limpar ou ignorar o orçamento de manifesto ou de execução de um app em nome dele. Isso não impede que os OEMs declarem ou recalibrem orçamentos nos manifestos dos apps do sistema que criam, conforme descrito em Orçamentos de memória do sistema.

A seção 8.6 do Documento de definição de compatibilidade do Android proíbe métodos, listas de permissões ou políticas que ignoram os limites de memória do app. As versões de produção não podem incluir listas de permissões, substituições por app ou chaves globais que desativem os limites de memória da plataforma ou os orçamentos de memória do app.

Configuração

No Android 17 e versões mais recentes, o Limitador de Memória é configurado usando o arquivo de configuração padrão da plataforma na partição do sistema em /system/etc/memory-limiter-config.xml.

Formato XML

O arquivo de configuração segue o esquema definido em memory-limiter-config.xsd. O arquivo define vários conjuntos de limites ordenados por memória disponível. O serviço seleciona o conjunto de limites mais alto com base na RAM disponível do dispositivo. Todos os valores de memória são especificados em mebibytes (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
Um número inteiro positivo que identifica a versão da configuração. Precisa ser 1.
minimumRequiredMemTotal

O MemTotal mínimo do sistema (em MiB, incluindo o piso) necessário para que esse conjunto de limites seja aplicado. O serviço avalia isso em relação à memória total disponível informada pelo kernel em /proc/meminfo.

Noções básicas sobre as divisões e os limites de memória: o valor MemTotal do kernel reflete a DRAM física acessível ao sistema operacional após as divisões de memória de hardware e firmware serem deduzidas. As exclusões reservam RAM para componentes de hardware especializados, como GPU, modem de banda base, ISP de câmera, ambientes de execução seguros e hipervisores, normalmente consumindo entre 500 MiB e mais de 2 GiB de memória física.

Como MemTotal é menor do que a DRAM física anunciada do dispositivo, os limites de minimumRequiredMemTotal incluem uma margem para essas reduções. Por exemplo, uma configuração para dispositivos de 16 GB especifica um limite mínimo de 14336 (14 GiB) em vez de 16384 (16 GiB), e um nível para dispositivos de 12 GB especifica um limite mínimo de 9216 (9 GiB). Isso garante que cada dispositivo corresponda ao nível pretendido, independentemente das variações de carve-out no nível da placa.

maximumAllowedMemTotal

O MemTotal máximo do sistema (em MiB, teto exclusivo) para que esse conjunto de limites seja aplicado. Se MemTotal for maior ou igual a esse teto, o limite definido não será aplicado. Para dispositivos com MemTotal menor que 3.200 MiB ou pelo menos 18.432 MiB, o Limitador de Memória não aplica os limites padrão da plataforma, mas os cgroups de memória por processo permanecem ativos (junto com os orçamentos de manifesto, sistema e ambiente de execução MemoryBudgetManager no nível 37.2 da API e versões mais recentes).

memVisible

O limite de memória rígido de memory.max em MiB aplicado ao cgroup do UID do pacote quando o pacote tem um processo visível. O limite padrão de recuperação flexível de memory.high é 90% desse valor, arredondado para o MiB mais próximo.

memNotVisible

O limite de memória rígido de memory.max em MiB aplicado ao cgroup do UID do pacote quando o pacote não tem processos visíveis. O limite padrão de recuperação flexível de memory.high é 90% desse valor, arredondado para o MiB mais próximo.

swapVisible

O limite de espaço de troca memory.swap.max em MiB aplicado a cada cgroup de PID de processo visível.

swapNotVisible

O limite de espaço de troca de memory.swap.max em MiB aplicado a cada cgroup de PID de processo não visível.

Limites padrão da plataforma

No Android 17 e versões mais recentes, a plataforma especifica limites padrão de memória e troca em /system/etc/memory-limiter-config.xml em níveis comuns de RAM física usando intervalos de [floor, ceiling):

RAM física de destino Intervalo de MemTotal [mínimo, máximo)
(minimumRequiredMemTotal,
maximumAllowedMemTotal)
Limite de memória visível
(memVisible)
Limite de memória não visível
(memNotVisible)
Limite de troca visível
(swapVisible)
Limite de troca não visível
(swapNotVisible)
16 GB [14336, 18432) MiB 10.240 MiB (10 GiB) 5.120 MiB (5 GiB) 5.120 MiB (5 GiB) 5.120 MiB (5 GiB)
12 GB [9216, 14336) MiB 8.192 MiB (8 GiB) 4.096 MiB (4 GiB) 4.096 MiB (4 GiB) 4.096 MiB (4 GiB)
8 GB [6800, 9216) MiB 5.120 MiB (5 GiB) 3.072 MiB (3 GiB) 3.072 MiB (3 GiB) 3.072 MiB (3 GiB)
6 GB [4800, 6800) MiB 4.096 MiB (4 GiB) 2.048 MiB (2 GiB) 2.048 MiB (2 GiB) 2.048 MiB (2 GiB)
4 GB [3200, 4800) MiB 2.048 MiB (2 GiB) 1.024 MiB (1 GiB) 1.024 MiB (1 GiB) 1.024 MiB (1 GiB)

Princípios de limite de memória

A configuração do limitador de memória foi projetada com base nos seguintes princípios da plataforma:

  • Consistência do ecossistema e compatibilidade de apps: limites de memória consistentes em todos os dispositivos garantem um desempenho previsível do app em todo o ecossistema Android. Os apps são desenvolvidos e testados de acordo com as expectativas de memória padrão. Manter limites uniformes da plataforma evita limitações inesperadas ou encerramentos prematuros.

  • Alocação proporcional de recursos: os limites da plataforma são calibrados de acordo com a capacidade física de RAM:

    • Processos visíveis: alocam aproximadamente de 1/2 a 2/3 da RAM física total para dar suporte a cargas de trabalho ativas de interface, composição e renderização.
    • Processos não visíveis: alocam aproximadamente de 1/4 a 1/3 do total de RAM física para tarefas em segundo plano.
  • Aplicabilidade universal: os limites se aplicam de maneira uniforme a todos os processos de aplicativos no dispositivo (UID >= 10000), incluindo apps pré-instalados do sistema e do OEM. Além do serviço de inferência em sandbox padrão no dispositivo, o limitador de memória não isenta apps individuais, garantindo uma governança de memória justa em todos os apps. Para configurar limites de proteção de ajuste automático de memory.min em daemons interativos críticos e limites de memory.high mais rígidos e calibrados para cargas de trabalho em apps e serviços individuais do sistema, consulte Orçamentos de memória do sistema.

  • Orçamentos de ambiente de execução previsíveis: no nível 37.2 da API e em versões mais recentes, os apps podem consultar os limites de memória e o uso de memória do cgroup e definir orçamentos de ambiente de execução mais rígidos usando MemoryBudgetManager. Preservar os limites padrão da plataforma e do manifesto sem substituições do fornecedor mantém o comportamento do orçamento de execução consistente em todos os dispositivos.

  • Dinâmica de recuperação e troca do kernel: quando um processo de app se aproxima do limite de memory.high, o kernel do Linux direciona a recuperação de memória especificamente para o cgroup desse processo. Isso inclui descartar páginas inativas com suporte de arquivos e trocar páginas anônimas frias para a ZRAM, mantendo a fluidez do sistema sem impor pressão de memória global.

Comandos do shell

O comando am memory-limiter permite que desenvolvedores e integradores de sistemas interajam com o serviço no momento da execução para desenvolvimento, teste e depuração:

am memory-limiter <SUB-COMMAND>

status

O subcomando status informa o status de funcionamento, os limites de plataforma configurados e os contadores de monitoramento ativos do limitador de memória:

adb shell am memory-limiter status

A execução de adb shell am memory-limiter status version mostra 2 quando o limitador de memória de dois limites está ativado ou 0 quando está desativado.

Confira abaixo um exemplo de saída de status em um dispositivo de 8 GB:

Memory limiter
  enabled           limits=true       killing=true      ignored=none
  visible:          memHigh=4608MB    memMax=5120MB     swapMax=3072MB
  not-visible:      memHigh=2765MB    memMax=3072MB     swapMax=3072MB
  active=64         uid-started=32    uid-current=28    uid-updated=4
  started=72        watched=72        watch-failed=0
  limiters=64       limiters-hwm=70   red-poll=0        red-limiter=0
  events=0          false-events=0    modify=84

Os campos principais na saída incluem:

limits e killing
Indica se os limites de memória da plataforma memory.high e memory.max estão configurados e aplicados ao nível de RAM do dispositivo.
ignored
O UID excluído pelo subcomando ignore, all quando a limitação está pausada globalmente ou none.
visible e not-visible
O limite padrão de UID memHigh para memory.high, o limite de UID memMax para memory.max e o limite de swapMax por processo para memory.swap.max aplicado a cada classificação de visibilidade.
active
O número de limitadores de processo e UID rastreados em system_server.
uid-started, uid-current e uid-updated
A contagem cumulativa de iniciados, a contagem de ativos e a contagem cumulativa de substituições para limitadores de UID atualizados com orçamentos de manifesto.
watched e watch-failed
O número cumulativo de observações de cgroup memory.events registradas e com falha.
limiters e limiters-hwm
O número e o limite máximo de observadores de cgroup nativos ativos.
events e false-events
As contagens de eventos acima do limite enviados para notificações de system_server e memory.events que não ultrapassaram um limite monitorado.

ignorar

O subcomando ignore exclui temporariamente um UID específico ou todos os processos da limitação de memória. Isso é útil para comparar o desempenho, fazer testes de estresse, calibrar automaticamente o orçamento de memória do app ou diagnosticar o comportamento da memória:

# 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

O subcomando manual substitui os limites calculados de memory.high, memory.max e memory.swap.max por um teto inteiro personalizado até os limites configurados do destino. Transmita um PID, com ou sem -p, para segmentar um único processo ou transmita -u <UID> para segmentar todos os processos em um UID de pacote. Especifique o tamanho em bytes, anexe MB para MiB ou GB para GiB ou transmita none para limpar uma substituição. Para compatibilidade com versões anteriores, números inteiros simples abaixo de 20 sem um sufixo são interpretados como MiB:

# Set a 1 GiB limit for PID 1234, with or without -p
adb shell am memory-limiter manual 1234 1073741824
adb shell am memory-limiter manual 1234 1024MB
adb shell am memory-limiter manual -p 1234 1GB
# Set a 1 GiB limit for all processes in UID 10087
adb shell am memory-limiter manual -u 10087 1GB
# Remove the manual override for PID 1234 or UID 10087
adb shell am memory-limiter manual 1234 none
adb shell am memory-limiter manual -u 10087 none

As substituições manuais de processos se aplicam apenas ao ciclo de vida dessa instância de processo específica. Se o processo for reiniciado, ele vai retornar aos limites padrão com base no estado dele. Por outro lado, as substituições de manual -u <UID> persistem enquanto o UID permanece ativo, até que todos os processos no UID sejam encerrados ou você limpe a substituição com none.

Uma substituição manual não pode exceder os limites físicos do sistema.