# Správa paměti v PHP: počítání referencí a garbage collection

> Správa paměti v PHP srozumitelně: počítání referencí, copy-on-write, cykly a garbage collector, měření a proč PHP2Shell míří právě sem.

Zdroj: https://phpsos.de/cs/php/speicherverwaltung/  
Stav: 25.10.2026  
Poskytovatel: PHP SOS, Nouzová pomoc pro PHP a bezpečnost serverů. Okamžitá pomoc přes WhatsApp: https://wa.me/491736494914?text=Dobr%C3%BD%20den%2C%20PHP%20SOS%2C%20pot%C5%99ebujeme%20pomoc%20s%20na%C5%A1%C3%ADm%20PHP%20serverem.%20Firma%3A%20%E2%80%A6%20Web%3A%20%E2%80%A6

**Stručně:** PHP uvolňuje paměť především počítáním referencí: každá hodnota ví, kolik proměnných na ni ukazuje, a smaže se, jakmile tento čítač dosáhne nuly. Pro kruhové odkazy, které by se tak nikdy neuvolnily, existuje garbage collector cyklů. Na konci každého požadavku PHP navíc uklidí vše, co požadavek obsadil. Obě zranitelnosti PHP2Shell míří na stavy paměti: na stav nedostatku paměti při zpracování požadavku a na interní stav, který po uvolnění zůstává příliš dlouho zachován.

## Jak PHP spravuje paměť: přehled

V jazycích jako C si programátor musí paměť sám vyžádat a znovu uvolnit. PHP to převezme. Každá proměnná, každé pole a každý objekt zabírají paměť, kterou PHP spravuje vlastním správcem paměti. Hranici určuje `memory_limit`, podle příručky standardně `128M`.

Pro rozhodovatele je klíčový jeden bod: při klasickém provozu s PHP-FPM nebo mod_php běží dlouhověké procesy workerů, které postupně zpracovávají mnoho požadavků. Co po sobě požadavek zanechá, musí být před dalším spolehlivě uklizeno. Právě v tomto místě leží jedna ze zranitelností PHP2Shell.

## Počítání referencí: počítat, kdo hodnotu používá

PHP ukládá každou hodnotu do kontejneru, takzvaného zval. Vedle typu a obsahu vede podle příručky čítač referencí (`refcount`) a značku, zda je hodnota součástí reference PHP pomocí `&` (`is_ref`).

```php
<?php
$a = str_repeat('x', 1000);  // jedna hodnota, refcount 1
$b = $a;                     // žádné kopírování, refcount 2
$b .= 'y';                   // teprve nyní se kopíruje (copy-on-write)
unset($a);                   // refcount originálu klesá
// Když čítač dosáhne 0, PHP paměť okamžitě uvolní.
```

Princip se jmenuje copy-on-write: PHP hodnotu zkopíruje, teprve když ji některá z proměnných změní. To ušetří hodně paměti. Čítač proměnné lze zviditelnit pomocí rozšíření Xdebug a funkce `xdebug_debug_zval()`. Příručka upozorňuje, že přesná čísla se mohou podle verze PHP lišit.

## Cykly: když počítání referencí nestačí

Počítání referencí má známou slabinu. Pokud struktura ukazuje sama na sebe, její čítač nikdy neklesne na nulu.

```php
<?php
$a = ['eins'];
$a[] = &$a;   // pole obsahuje referenci samo na sebe
unset($a);    // nikdo k němu už nemůže přistoupit, ale refcount zůstává 1
```

Podle příručky se takové kruhové odkazy vyskytují zejména u objektů, například ve strukturách rodič-potomek nebo v parserech. Jednotlivé úniky jsou neškodné, protože PHP na konci požadavku uklidí. V dlouho běžících procesech, jako jsou workery front nebo velké testovací běhy, se ale miliony takových úniků mohou sečíst ve skutečný problém s pamětí.

## Garbage collector cyklů

Od PHP 5.3.0 používá PHP k nalezení takových cyklů algoritmus podle Bacona a Rajana. Základní myšlenka: cyklus může vzniknout jen tehdy, když čítač klesne na hodnotu větší než nula. Jen tyto hodnoty si PHP zapamatuje jako možné kořeny.

1. Možné kořeny skončí v bufferu. Příručka uvádí jako standardní velikost 10 000 položek; aktuálně platnou prahovou hodnotu ukazuje `gc_status()` v poli `threshold`.
2. Když je buffer plný, PHP projde všechny dosažitelné hodnoty a zkusmo sníží jejich čítače.
3. Ve druhém průchodu platí: hodnoty s čítačem nula jsou odpad, u všech ostatních se snížení vrátí zpět.
4. Kořeny se odstraní z bufferu a všechny hodnoty rozpoznané jako odpad se uvolní.

Řídí se to pomocí `zend.enable_gc` (standardně zapnuto) nebo za běhu pomocí `gc_enable()` a `gc_disable()`. `gc_collect_cycles()` vynutí běh a vrátí počet nasbíraných cyklů.

## Měření a sledování paměti

| Funkce | Účel |
|---|---|
| `memory_get_usage()` | paměť aktuálně obsazená skriptem |
| `memory_get_usage(true)` | paměť, kterou správce paměti rezervoval u systému; proti této hodnotě se kontroluje `memory_limit` |
| `memory_get_peak_usage()` | maximum obsazené paměti |
| `gc_status()` | běhy, nasbírané cykly, prahová hodnota, kořeny; od PHP 8.3.0 i doby běhu |
| `gc_collect_cycles()` | okamžité spuštění sbírání cyklů |
| `WeakMap` | přiřazení k objektům, aniž by bránilo jejich uvolnění, od PHP 8.0 |

Podle příručky `memory_get_usage()` zachycuje jen paměť, kterou PHP vyžádá přes vlastní správce paměti. Co si rozšíření berou přímo od operačního systému, v těchto číslech chybí. Za limit paměti celého procesu proto odpovídá operační systém nebo kontejner.

## Správa paměti a PHP2Shell

Popisy obou zranitelností vedou přímo k tomuto tématu. U CVE-2026-109618 může stav nedostatku paměti přerušit PHP ve chvíli, kdy jsou data požadavku inicializována jen částečně; při úklidu pak mohou být neplatná data považována za platná. Co se při přetečení paměti děje uvnitř, vysvětluje stránka [memory_limit a Out-of-Memory](https://phpsos.de/cs/php/out-of-memory/). U CVE-2026-109619 může interní stav požadavku po uvolnění paměti zůstat zachován a pozdější požadavek ve stejném workeru jej může využít dříve, než se vynuluje. Průběh od startu po úklid popisuje stránka [Životní cyklus požadavku](https://phpsos.de/cs/php/request-lebenszyklus/).

Jako přechodné opatření php.net doporučuje nastavit `memory_limit=-1` tak, aby jej aplikace nemohly přepsat, například pomocí `php_admin_value` v poolu FPM, a paměť omezit místo toho přes kontejner nebo cgroups, ne přes `RLIMIT_AS` nebo `RLIMIT_DATA`. Jak to jde se systemd, ukazuje článek [Nastavení limitů paměti pomocí systemd a cgroups](https://phpsos.de/cs/blog/speichergrenzen-systemd-cgroups/).

```ini
; pool FPM: přechodné opatření podle php.net, poté restartovat všechny workery
php_admin_value[memory_limit] = -1
```

> **Důležité:**
> Bez limitu přes kontejner nebo cgroup může jediný proces vytížit celý server. A žádné nastavení nenahrazuje aktualizaci na PHP 8.5.12, 8.4.27, 8.3.36 nebo 8.2.35 včetně restartu všech workerů, protože staré procesy z paměti odstraní jen restart.

## Jak konkrétně pomáhá PHP SOS?

- Zkontrolujeme účinná nastavení paměti pro každý pool a zda je aplikace mohou přepsat.
- Zavedeme přechodná opatření s limitem paměti na úrovni kontejneru nebo cgroup a otestujeme zátěž.
- Nasadíme aktualizaci, restartujeme všechny workery a prokážeme, že už neběží žádné staré procesy, v rámci naší [služby záplatování PHP](https://phpsos.de/cs/leistungen/php-patch-service/).
- Vyhodnotíme chyby paměti a ukončené workery v lozích od 20. 10. 2026.


## Časté dotazy

**Co je počítání referencí v PHP?**  
PHP u každé hodnoty počítá, kolik proměnných nebo prvků na ni odkazuje. Když čítač klesne na nulu, paměť se okamžitě uvolní. Většina paměti se tak vrací bez vlastního úklidového běhu.

**K čemu pak PHP ještě potřebuje garbage collector?**  
Pro cykly. Pokud pole odkazuje samo na sebe nebo se dva objekty navzájem znají, čítač nikdy neklesne na nulu, i když k nim už nikdo nepřistupuje. Garbage collector cyklů takové struktury najde a odstraní.

**Uvolní PHP paměť na konci požadavku?**  
Ano, paměť, kterou skript během požadavku obsadil, PHP na konci požadavku uklidí. U PHP2Shell se ale CVE-2026-109619 týká právě případu, kdy může interní stav po uvolnění zůstat zachován a může jej využít pozdější požadavek ve stejném workeru.

**Proč php.net doporučuje memory_limit=-1 jako přechodné opatření?**  
Zranitelnost CVE-2026-109618 souvisí se stavem nedostatku paměti. php.net doporučuje nastavit memory_limit=-1 tak, aby jej aplikace nemohly přepsat, a paměť omezit místo toho přes kontejner nebo cgroups. Není to úplná ochrana a aktualizaci to nenahrazuje.

## Zdroje

- [Příručka PHP: Základy počítání referencí](https://www.php.net/manual/de/features.gc.refcounting-basics.php)
- [Příručka PHP: Sbírání cyklů](https://www.php.net/manual/de/features.gc.collecting-cycles.php)
- [Příručka PHP: gc_status](https://www.php.net/manual/de/function.gc-status.php)
- [Příručka PHP: memory_get_usage](https://www.php.net/manual/en/function.memory-get-usage.php)
- [php.net: Bezpečnostní oznámení](https://www.php.net/)
