# Gestão de memória em PHP: contagem de referências e garbage collection

> Gestão de memória do PHP explicada: contagem de referências, copy-on-write, ciclos e garbage collector, funções de medição e por que o PHP2Shell atua aqui.

Fonte: https://phpsos.de/pt/php/speicherverwaltung/  
Atualizado em: 25/10/2026  
Prestador: PHP SOS, Assistência de emergência PHP e segurança de servidores. Ajuda imediata por WhatsApp: https://wa.me/491736494914?text=Ol%C3%A1%2C%20PHP%20SOS%2C%20precisamos%20de%20ajuda%20com%20o%20nosso%20servidor%20PHP.%20Empresa%3A%20%E2%80%A6%20Site%3A%20%E2%80%A6

**Em resumo:** O PHP liberta memória sobretudo através da contagem de referências: cada valor sabe quantas variáveis apontam para ele e é eliminado assim que esse contador chega a zero. Para as referências circulares, que de outro modo nunca seriam libertadas, existe um garbage collector de ciclos. No fim de cada pedido, o PHP limpa ainda tudo o que o pedido ocupou. Ambas as vulnerabilidades do PHP2Shell atuam sobre estados de memória: sobre um estado de falta de memória durante o processamento do pedido e sobre um estado interno que persiste demasiado tempo após a libertação.

## Como o PHP gere a memória: visão geral

Em linguagens como o C, o programador tem de pedir e libertar a memória ele próprio. O PHP dispensa-o disso. Cada variável, cada array e cada objeto ocupa memória, que o PHP gere através do seu próprio gestor de memória. O limite para isso é definido pelo `memory_limit`, segundo o manual `128M` por predefinição.

Para quem decide, um ponto é central: na operação clássica com PHP-FPM ou mod_php correm processos worker de longa duração, que tratam muitos pedidos em sequência. O que um pedido deixa para trás tem de estar limpo de forma fiável antes do seguinte. É precisamente aqui que está uma das vulnerabilidades do PHP2Shell.

## Contagem de referências: contar quem usa um valor

O PHP guarda cada valor num contentor, o chamado zval. Além do tipo e do conteúdo, segundo o manual, mantém um contador de referências (`refcount`) e uma marca que indica se o valor faz parte de uma referência PHP com `&` (`is_ref`).

```php
<?php
$a = str_repeat('x', 1000);  // um valor, refcount 1
$b = $a;                     // sem cópia, refcount 2
$b .= 'y';                   // só agora é copiado (copy-on-write)
unset($a);                   // o refcount do original desce
// Quando um contador chega a 0, o PHP liberta a memória de imediato.
```

O princípio chama-se copy-on-write: o PHP só copia um valor quando uma das variáveis o altera. Isto poupa muita memória. O contador de uma variável pode tornar-se visível com a extensão Xdebug e a função `xdebug_debug_zval()`. O manual adverte que os números exatos podem diferir consoante a versão do PHP.

## Ciclos: quando a contagem de referências não basta

A contagem de referências tem uma fraqueza conhecida. Se uma estrutura aponta para si própria, o seu contador nunca desce a zero.

```php
<?php
$a = ['eins'];
$a[] = &$a;   // o array contém uma referência a si próprio
unset($a);    // ninguém lhe consegue aceder, mas o refcount continua em 1
```

Segundo o manual, estas referências circulares ocorrem sobretudo em objetos, por exemplo em estruturas pai-filho ou em analisadores. Fugas isoladas são inofensivas, porque o PHP limpa no fim do pedido. Em processos de longa duração, como workers de filas ou grandes execuções de testes, milhões delas podem, porém, somar-se num verdadeiro problema de memória.

## O garbage collector de ciclos

Desde o PHP 5.3.0, o PHP usa um algoritmo de Bacon e Rajan para encontrar esses ciclos. A ideia de base: um ciclo só pode surgir quando um contador desce para um valor superior a zero. Só esses valores são retidos pelo PHP como possíveis raízes.

1. As possíveis raízes vão para um buffer. O manual indica como tamanho predefinido 10 000 entradas; o limiar atualmente em vigor é mostrado pelo `gc_status()` no campo `threshold`.
2. Quando o buffer está cheio, o PHP percorre todos os valores alcançáveis e reduz, a título de teste, os seus contadores.
3. Numa segunda passagem aplica-se: os valores com contador a zero são lixo; em todos os outros, a redução é revertida.
4. As raízes são retiradas do buffer e todos os valores reconhecidos como lixo são libertados.

Isto é controlado através de `zend.enable_gc` (ativo por predefinição) ou, em tempo de execução, com `gc_enable()` e `gc_disable()`. O `gc_collect_cycles()` força uma execução e devolve o número de ciclos recolhidos.

## Medir e observar a memória

| Função | Finalidade |
|---|---|
| `memory_get_usage()` | memória atualmente ocupada pelo script |
| `memory_get_usage(true)` | memória reservada pelo gestor de memória junto do sistema; é contra este valor que se verifica o `memory_limit` |
| `memory_get_peak_usage()` | valor máximo da memória ocupada |
| `gc_status()` | execuções, ciclos recolhidos, limiar, raízes; desde o PHP 8.3.0 também tempos de execução |
| `gc_collect_cycles()` | desencadear de imediato a recolha de ciclos |
| `WeakMap` | associação a objetos sem impedir a sua libertação, desde o PHP 8.0 |

Segundo o manual, o `memory_get_usage()` regista apenas a memória que o PHP pede através do seu próprio gestor de memória. O que as extensões obtêm diretamente junto do sistema operativo não consta destes números. O limite de memória do processo inteiro cabe, por isso, ao sistema operativo ou ao contentor.

## Gestão de memória e PHP2Shell

As descrições de ambas as vulnerabilidades levam diretamente a este tema. Na CVE-2026-109618, um estado de falta de memória pode interromper o PHP enquanto os dados do pedido estão apenas parcialmente inicializados; na limpeza, dados inválidos podem então ser tratados como válidos. O que acontece por dentro num excesso de memória explica-o a página [memory_limit e Out-of-Memory](https://phpsos.de/pt/php/out-of-memory/). Na CVE-2026-109619, o estado interno de um pedido pode persistir após a libertação da memória e ser usado por um pedido posterior no mesmo worker antes de ser reposto. O percurso do arranque à limpeza é descrito na página [O ciclo de vida do pedido](https://phpsos.de/pt/php/request-lebenszyklus/).

Como medida provisória, o php.net recomenda definir `memory_limit=-1` de modo que as aplicações não o possam sobrepor, por exemplo com `php_admin_value` no pool FPM, e limitar a memória através de contentor ou cgroups, e não através de `RLIMIT_AS` ou `RLIMIT_DATA`. Como fazê-lo com o systemd mostra-o o artigo [Definir limites de memória com systemd e cgroups](https://phpsos.de/pt/blog/speichergrenzen-systemd-cgroups/).

```ini
; Pool FPM: medida provisória segundo o php.net, depois reiniciar todos os workers
php_admin_value[memory_limit] = -1
```

> **Importante:**
> Sem um limite através de contentor ou cgroup, um único processo pode esgotar o servidor. E nenhuma definição substitui a atualização para o PHP 8.5.12, 8.4.27, 8.3.36 ou 8.2.35, com o reinício de todos os workers, pois só o reinício remove os processos antigos da memória.

## Como ajuda concretamente a PHP SOS?

- Verificamos as definições de memória efetivas em cada pool e se as aplicações as podem sobrepor.
- Aplicamos as medidas provisórias com um limite de memória ao nível do contentor ou do cgroup e testamos a carga.
- Aplicamos a atualização, reiniciamos todos os workers e comprovamos que já não correm processos antigos, no âmbito do nosso [serviço de patches PHP](https://phpsos.de/pt/leistungen/php-patch-service/).
- Avaliamos erros de memória e workers interrompidos nos registos a partir de 20/10/2026.


## Perguntas frequentes

**O que é a contagem de referências em PHP?**  
O PHP conta, para cada valor, quantas variáveis ou elementos apontam para ele. Quando o contador desce a zero, a memória é libertada de imediato. Assim, a maior parte da memória é devolvida sem uma execução de limpeza própria.

**Para que precisa então o PHP de um garbage collector?**  
Para os ciclos. Se um array aponta para si próprio ou dois objetos se conhecem mutuamente, o contador nunca desce a zero, mesmo que ninguém lhes aceda. O garbage collector de ciclos encontra e remove essas estruturas.

**O PHP liberta a memória no fim de um pedido?**  
Sim, a memória que um script ocupa durante um pedido, o PHP limpa-a no fim do pedido. No PHP2Shell, porém, a CVE-2026-109619 diz respeito precisamente ao caso em que um estado interno persiste após a libertação e pode ser usado por um pedido posterior no mesmo worker.

**Porque é que o php.net recomenda memory_limit=-1 como medida provisória?**  
A vulnerabilidade CVE-2026-109618 está relacionada com um estado de falta de memória. O php.net recomenda definir memory_limit=-1 de modo que as aplicações não o possam sobrepor e limitar a memória através de contentor ou cgroups. Não é uma proteção completa e não substitui a atualização.

## Fontes

- [Manual do PHP: fundamentos da contagem de referências](https://www.php.net/manual/de/features.gc.refcounting-basics.php)
- [Manual do PHP: recolha de ciclos](https://www.php.net/manual/de/features.gc.collecting-cycles.php)
- [Manual do PHP: gc_status](https://www.php.net/manual/de/function.gc-status.php)
- [Manual do PHP: memory_get_usage](https://www.php.net/manual/en/function.memory-get-usage.php)
- [php.net: avisos de segurança](https://www.php.net/)
