# Zarządzanie pamięcią w PHP: zliczanie referencji i garbage collection

> Zarządzanie pamięcią w PHP zrozumiale: zliczanie referencji, copy-on-write, cykle i garbage collector, funkcje pomiarowe oraz związek z PHP2Shell.

Źródło: https://phpsos.de/pl/php/speicherverwaltung/  
Stan: 25.10.2026  
Dostawca: PHP SOS, Pomoc awaryjna PHP i bezpieczeństwo serwerów. Natychmiastowa pomoc przez WhatsApp: https://wa.me/491736494914?text=Dzie%C5%84%20dobry%2C%20PHP%20SOS%2C%20potrzebujemy%20pomocy%20z%20naszym%20serwerem%20PHP.%20Firma%3A%20%E2%80%A6%20Strona%20internetowa%3A%20%E2%80%A6

**Krótko:** PHP zwalnia pamięć przede wszystkim przez zliczanie referencji: każda wartość wie, ile zmiennych na nią wskazuje, i jest usuwana, gdy tylko ten licznik osiągnie zero. Dla odwołań cyklicznych, które w ten sposób nigdy nie zostałyby zwolnione, istnieje garbage collector cykli. Na końcu każdego żądania PHP dodatkowo sprząta wszystko, co żądanie zajęło. Obie luki PHP2Shell dotyczą stanów pamięci: stanu Out-of-Memory podczas przetwarzania żądania oraz stanu wewnętrznego, który po zwolnieniu pamięci utrzymuje się zbyt długo.

## Jak PHP zarządza pamięcią: przegląd

W językach takich jak C programista musi sam zamawiać pamięć i ją zwalniać. PHP zdejmuje z niego ten obowiązek. Każda zmienna, każda tablica i każdy obiekt zajmuje pamięć, którą PHP zarządza przez własnego menedżera pamięci. Granicę wyznacza `memory_limit`, według podręcznika domyślnie `128M`.

Dla osób podejmujących decyzje jeden punkt jest kluczowy: przy klasycznej pracy z PHP-FPM lub mod_php działają długowieczne procesy workerów, które obsługują po kolei wiele żądań. To, co zostawi żądanie, musi zostać niezawodnie posprzątane przed następnym. Właśnie w tym miejscu leży jedna z luk PHP2Shell.

## Zliczanie referencji: liczenie, kto używa wartości

PHP przechowuje każdą wartość w kontenerze zwanym zval. Obok typu i zawartości prowadzi on według podręcznika licznik referencji (`refcount`) oraz znacznik, czy wartość jest częścią referencji PHP z `&` (`is_ref`).

```php
<?php
$a = str_repeat('x', 1000);  // jedna wartość, refcount 1
$b = $a;                     // bez kopiowania, refcount 2
$b .= 'y';                   // dopiero teraz następuje kopiowanie (copy-on-write)
unset($a);                   // refcount oryginału spada
// Gdy licznik osiągnie 0, PHP od razu zwalnia pamięć.
```

Zasada nazywa się copy-on-write: PHP kopiuje wartość dopiero wtedy, gdy któraś ze zmiennych ją zmieni. Oszczędza to dużo pamięci. Licznik zmiennej można uwidocznić za pomocą rozszerzenia Xdebug i funkcji `xdebug_debug_zval()`. Podręcznik zwraca uwagę, że dokładne liczby mogą się różnić w zależności od wersji PHP.

## Cykle: gdy zliczanie referencji nie wystarcza

Zliczanie referencji ma znaną słabość. Jeśli struktura wskazuje na samą siebie, jej licznik nigdy nie spada do zera.

```php
<?php
$a = ['eins'];
$a[] = &$a;   // tablica zawiera referencję do samej siebie
unset($a);    // nikt nie może się już do niej odwołać, ale refcount pozostaje 1
```

Według podręcznika takie odwołania cykliczne występują szczególnie przy obiektach, na przykład w strukturach rodzic-dziecko lub w parserach. Pojedyncze wycieki są nieszkodliwe, ponieważ PHP sprząta na końcu żądania. W długo działających procesach, takich jak workery kolejek lub duże przebiegi testów, miliony takich wycieków mogą się jednak złożyć w prawdziwy problem z pamięcią.

## Garbage collector cykli

Od PHP 5.3.0 PHP używa algorytmu Bacona i Rajana do wyszukiwania takich cykli. Podstawowa idea: cykl może powstać tylko wtedy, gdy licznik spadnie do wartości większej od zera. Tylko te wartości PHP zapamiętuje jako możliwe korzenie.

1. Możliwe korzenie trafiają do bufora. Podręcznik podaje jako domyślny rozmiar 10 000 wpisów; aktualnie obowiązujący próg pokazuje `gc_status()` w polu `threshold`.
2. Gdy bufor jest pełny, PHP przechodzi przez wszystkie osiągalne wartości i próbnie zmniejsza ich liczniki.
3. W drugim przebiegu obowiązuje: wartości z licznikiem zero są śmieciami, a przy wszystkich pozostałych zmniejszenie jest cofane.
4. Korzenie są usuwane z bufora, a wszystkie wartości rozpoznane jako śmieci zostają zwolnione.

Steruje się tym przez `zend.enable_gc` (domyślnie włączone) lub w czasie działania przez `gc_enable()` i `gc_disable()`. `gc_collect_cycles()` wymusza przebieg i zwraca liczbę zebranych cykli.

## Mierzenie i obserwowanie pamięci

| Funkcja | Cel |
|---|---|
| `memory_get_usage()` | pamięć aktualnie zajęta przez skrypt |
| `memory_get_usage(true)` | pamięć zarezerwowana przez menedżera pamięci w systemie; względem tej wartości sprawdzany jest `memory_limit` |
| `memory_get_peak_usage()` | szczytowy stan zajętej pamięci |
| `gc_status()` | przebiegi, zebrane cykle, próg, korzenie; od PHP 8.3.0 także czasy |
| `gc_collect_cycles()` | natychmiastowe uruchomienie zbierania cykli |
| `WeakMap` | przypisanie do obiektów bez blokowania ich zwolnienia, od PHP 8.0 |

Według podręcznika `memory_get_usage()` obejmuje tylko pamięć, którą PHP zamawia przez własnego menedżera pamięci. To, co rozszerzenia pobierają bezpośrednio od systemu operacyjnego, w tych liczbach nie występuje. Za granicę pamięci całego procesu odpowiadają więc system operacyjny lub kontener.

## Zarządzanie pamięcią a PHP2Shell

Opisy obu luk prowadzą wprost do tego tematu. W CVE-2026-109618 stan Out-of-Memory może przerwać PHP, gdy dane żądania są zainicjalizowane tylko częściowo; przy sprzątaniu nieprawidłowe dane mogą być wtedy traktowane jako prawidłowe. Co dzieje się wewnętrznie przy przepełnieniu pamięci, wyjaśnia strona [memory_limit i Out-of-Memory](https://phpsos.de/pl/php/out-of-memory/). W CVE-2026-109619 stan wewnętrzny żądania może po zwolnieniu pamięci przetrwać i zostać użyty przez późniejsze żądanie w tym samym workerze, zanim zostanie zresetowany. Przebieg od startu do sprzątania opisuje strona [Cykl życia żądania](https://phpsos.de/pl/php/request-lebenszyklus/).

Jako środek przejściowy php.net zaleca ustawić `memory_limit=-1` tak, aby aplikacje nie mogły go przekroczyć, na przykład za pomocą `php_admin_value` w puli FPM, a pamięć ograniczać przez kontenery lub cgroups, a nie przez `RLIMIT_AS` czy `RLIMIT_DATA`. Jak to zrobić z systemd, pokazuje artykuł [Ustawianie limitów pamięci za pomocą systemd i cgroups](https://phpsos.de/pl/blog/speichergrenzen-systemd-cgroups/).

```ini
; pula FPM: środek przejściowy według php.net, potem zrestartować wszystkie workery
php_admin_value[memory_limit] = -1
```

> **Ważne:**
> Bez limitu przez kontener lub cgroup pojedynczy proces może obciążyć cały serwer. Żadne ustawienie nie zastępuje też aktualizacji do PHP 8.5.12, 8.4.27, 8.3.36 lub 8.2.35 wraz z restartem wszystkich workerów, ponieważ dopiero restart usuwa stare procesy z pamięci.

## Jak konkretnie pomaga PHP SOS?

- Sprawdzamy obowiązujące ustawienia pamięci dla każdej puli i to, czy aplikacje mogą je przekraczać.
- Wdrażamy środki przejściowe z limitem pamięci na poziomie kontenera lub cgroup i testujemy obciążenie.
- Instalujemy aktualizację, restartujemy wszystkie workery i dowodzimy, że nie działają już stare procesy, w ramach naszej [usługi poprawek PHP](https://phpsos.de/pl/leistungen/php-patch-service/).
- Analizujemy błędy pamięci i przerwane workery w logach od 20.10.2026.


## Częste pytania

**Czym jest zliczanie referencji w PHP?**  
PHP zlicza dla każdej wartości, ile zmiennych lub elementów na nią wskazuje. Gdy licznik spada do zera, pamięć jest od razu zwalniana. W ten sposób większość pamięci wraca bez osobnego przebiegu sprzątającego.

**Po co PHP potrzebuje więc jeszcze garbage collectora?**  
Dla cykli. Jeśli tablica wskazuje na samą siebie albo dwa obiekty znają się nawzajem, licznik nigdy nie spada do zera, nawet gdy nikt już się do nich nie odwołuje. Garbage collector cykli znajduje i usuwa takie struktury.

**Czy PHP zwalnia pamięć na końcu żądania?**  
Tak, pamięć zajętą przez skrypt podczas żądania PHP sprząta na końcu żądania. W PHP2Shell CVE-2026-109619 dotyczy jednak właśnie przypadku, w którym stan wewnętrzny może przetrwać po zwolnieniu i zostać użyty przez późniejsze żądanie w tym samym workerze.

**Dlaczego php.net zaleca memory_limit=-1 jako środek przejściowy?**  
Luka CVE-2026-109618 wiąże się ze stanem Out-of-Memory. php.net zaleca ustawić memory_limit=-1 tak, aby aplikacje nie mogły go przekroczyć, a pamięć ograniczać przez kontenery lub cgroups. To nie jest pełna ochrona i nie zastępuje aktualizacji.

## Źródła

- [Podręcznik PHP: podstawy zliczania referencji](https://www.php.net/manual/de/features.gc.refcounting-basics.php)
- [Podręcznik PHP: zbieranie cykli](https://www.php.net/manual/de/features.gc.collecting-cycles.php)
- [Podręcznik PHP: gc_status](https://www.php.net/manual/de/function.gc-status.php)
- [Podręcznik PHP: memory_get_usage](https://www.php.net/manual/en/function.memory-get-usage.php)
- [php.net: komunikaty o bezpieczeństwie](https://www.php.net/)
