# Управление на паметта в PHP: броене на препратки и garbage collection

> Управление на паметта в PHP: броене на препратки, copy-on-write, цикли и garbage collector, функции за измерване и защо PHP2Shell засяга именно това.

Източник: https://phpsos.de/bg/php/speicherverwaltung/  
Актуалност: 25.10.2026  
Доставчик: PHP SOS, Спешна помощ за PHP и сигурност на сървъри. Незабавна помощ по WhatsApp: https://wa.me/491736494914?text=%D0%97%D0%B4%D1%80%D0%B0%D0%B2%D0%B5%D0%B9%D1%82%D0%B5%2C%20PHP%20SOS%2C%20%D0%B8%D0%BC%D0%B0%D0%BC%D0%B5%20%D0%BD%D1%83%D0%B6%D0%B4%D0%B0%20%D0%BE%D1%82%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%20%D1%81%20%D0%BD%D0%B0%D1%88%D0%B8%D1%8F%20PHP%20%D1%81%D1%8A%D1%80%D0%B2%D1%8A%D1%80.%20%D0%A4%D0%B8%D1%80%D0%BC%D0%B0%3A%20%E2%80%A6%20%D0%A3%D0%B5%D0%B1%D1%81%D0%B0%D0%B9%D1%82%3A%20%E2%80%A6

**Накратко:** PHP освобождава паметта най-вече чрез броене на препратки: всяка стойност знае колко променливи сочат към нея и се изтрива, щом този брояч достигне нула. За циклични препратки, които така никога не биха се освободили, има garbage collector за цикли. В края на всяка заявка PHP допълнително почиства всичко, което заявката е заела. И двете уязвимости PHP2Shell засягат състояния на паметта: състояние на недостиг на памет по време на обработката на заявката и вътрешно състояние, което остава твърде дълго след освобождаването.

## Как PHP управлява паметта: преглед

В езици като C програмистът сам трябва да заявява и отново да освобождава паметта. PHP му го спестява. Всяка променлива, всеки масив и всеки обект заема памет, която PHP управлява чрез собствен мениджър на паметта. Границата за това задава `memory_limit`, според ръководството по подразбиране `128M`.

За вземащите решения една точка е централна: при класическата работа с PHP-FPM или mod_php работят дълготрайни worker процеси, които обработват последователно много заявки. Онова, което една заявка оставя, трябва да бъде надеждно почистено преди следващата. Точно тук се намира една от уязвимостите PHP2Shell.

## Броене на препратки: отчитане кой използва дадена стойност

PHP съхранява всяка стойност в контейнер, т.нар. zval. Освен тип и съдържание, според ръководството той води брояч на препратките (`refcount`) и маркер дали стойността е част от PHP препратка с `&` (`is_ref`).

```php
<?php
$a = str_repeat('x', 1000);  // една стойност, refcount 1
$b = $a;                     // без копиране, refcount 2
$b .= 'y';                   // едва сега се копира (Copy-on-Write)
unset($a);                   // refcount на оригинала намалява
// Щом един брояч достигне 0, PHP освобождава паметта веднага.
```

Принципът се нарича Copy-on-Write: PHP копира една стойност едва когато някоя от променливите я промени. Това спестява много памет. Броячът на една променлива може да се направи видим с разширението Xdebug и функцията `xdebug_debug_zval()`. Ръководството посочва, че точните числа могат да се различават според версията на PHP.

## Цикли: когато броенето на препратки не стига

Броенето на препратки има известна слабост. Ако една структура сочи към себе си, нейният брояч никога не пада до нула.

```php
<?php
$a = ['eins'];
$a[] = &$a;   // масивът съдържа препратка към себе си
unset($a);    // никой вече не може да го достъпи, но refcount остава 1
```

Според ръководството такива циклични препратки се срещат особено при обекти, например в структури родител-дете или в парсери. Отделни течове са безобидни, защото PHP почиства в края на заявката. В дълго работещи процеси като worker-и на опашки или големи тестови изпълнения обаче милиони от тях могат да се сумират в истински проблем с паметта.

## Garbage collector-ът за цикли

От PHP 5.3.0 PHP използва алгоритъм на Bacon и Rajan, за да намира такива цикли. Основната идея: цикъл може да възникне само ако брояч падне до стойност, по-голяма от нула. Само тези стойности PHP запомня като възможни корени.

1. Възможните корени попадат в буфер. Ръководството посочва като размер по подразбиране 10 000 записа; текущо действащия праг показва `gc_status()` в полето `threshold`.
2. Когато буферът се напълни, PHP обхожда всички достижими стойности и пробно намалява техните броячи.
3. При втори обход важи: стойностите с брояч нула са боклук, а при всички останали намаляването се връща.
4. Корените се премахват от буфера и всички разпознати като боклук стойности се освобождават.

Управлява се чрез `zend.enable_gc` (по подразбиране включено) или по време на изпълнение с `gc_enable()` и `gc_disable()`. `gc_collect_cycles()` принуждава изпълнение и връща броя на събраните цикли.

## Измерване и наблюдение на паметта

| Функция | Предназначение |
|---|---|
| `memory_get_usage()` | заетата в момента от скрипта памет |
| `memory_get_usage(true)` | памет, резервирана от мениджъра на паметта в системата; спрямо тази стойност се проверява `memory_limit` |
| `memory_get_peak_usage()` | най-висока стойност на заетата памет |
| `gc_status()` | изпълнения, събрани цикли, праг, корени; от PHP 8.3.0 и времена на изпълнение |
| `gc_collect_cycles()` | незабавно стартиране на събирането на цикли |
| `WeakMap` | съпоставяне към обекти, без да се пречи на освобождаването им, от PHP 8.0 |

Според ръководството `memory_get_usage()` отчита само паметта, която PHP заявява чрез собствения си мениджър на паметта. Онова, което разширенията вземат директно от операционната система, липсва в тези числа. За паметовата граница на целия процес затова са отговорни операционната система или контейнерът.

## Управление на паметта и PHP2Shell

Описанията на двете уязвимости водят директно към тази тема. При CVE-2026-109618 състояние на недостиг на памет може да прекъсне PHP, докато данните от заявката са само частично инициализирани; при почистването невалидни данни могат да бъдат третирани като валидни. Какво се случва вътрешно при препълване на паметта, обяснява страницата [memory_limit и Out-of-Memory](https://phpsos.de/bg/php/out-of-memory/). При CVE-2026-109619 вътрешно състояние на една заявка може да остане след освобождаването на паметта и да бъде използвано от по-късна заявка в същия worker, преди да бъде нулирано. Последователността от старта до почистването описва страницата [Жизненият цикъл на заявката](https://phpsos.de/bg/php/request-lebenszyklus/).

Като временна мярка php.net препоръчва `memory_limit=-1` да се задава така, че приложенията да не могат да го презапишат, например с `php_admin_value` в FPM пула, а паметта да се ограничава чрез контейнер или cgroups, не чрез `RLIMIT_AS` или `RLIMIT_DATA`. Как става това със systemd, показва статията [Задаване на граници на паметта със systemd и cgroups](https://phpsos.de/bg/blog/speichergrenzen-systemd-cgroups/).

```ini
; FPM пул: временна мярка според php.net, след това рестартирайте всички worker процеси
php_admin_value[memory_limit] = -1
```

> **Важно:**
> Без граница чрез контейнер или cgroup един-единствен процес може да натовари сървъра докрай. И никоя настройка не замества актуализацията до PHP 8.5.12, 8.4.27, 8.3.36 или 8.2.35 заедно с рестартиране на всички worker процеси, защото само рестартирането премахва старите процеси от паметта.

## Как помага PHP SOS конкретно?

- Проверяваме действащите настройки на паметта за всеки пул и дали приложенията могат да ги презапишат.
- Прилагаме временните мерки с граница на паметта на ниво контейнер или cgroup и тестваме натоварването.
- Внедряваме актуализацията, рестартираме всички worker процеси и доказваме, че вече не работят стари процеси, в рамките на нашата [услуга за PHP пачове](https://phpsos.de/bg/leistungen/php-patch-service/).
- Анализираме грешките поради памет и прекратените worker процеси в логовете от 20.10.2026 г.


## Често задавани въпроси

**Какво е броене на препратки в PHP?**  
PHP отчита за всяка стойност колко променливи или елементи сочат към нея. Когато броячът падне до нула, паметта се освобождава веднага. Така по-голямата част от паметта се връща без отделно почистване.

**За какво тогава PHP има нужда от garbage collector?**  
За цикли. Ако масив сочи към себе си или два обекта се познават взаимно, броячът никога не пада до нула, макар никой вече да не достъпва структурата. Garbage collector-ът за цикли намира и премахва такива структури.

**PHP освобождава ли паметта в края на заявката?**  
Да, паметта, която скриптът е заел по време на заявка, PHP почиства в края ѝ. При PHP2Shell обаче CVE-2026-109619 засяга точно случая, при който вътрешно състояние може да остане след освобождаването и да бъде използвано от по-късна заявка в същия worker.

**Защо php.net препоръчва memory_limit=-1 като временна мярка?**  
Уязвимостта CVE-2026-109618 е свързана със състояние на недостиг на памет. php.net препоръчва memory_limit=-1 да се задава така, че приложенията да не могат да го презапишат, а паметта да се ограничава чрез контейнер или cgroups. Това не е пълна защита и не замества актуализацията.

## Източници

- [Ръководство на PHP: Основи на броенето на препратки](https://www.php.net/manual/de/features.gc.refcounting-basics.php)
- [Ръководство на PHP: Събиране на цикли](https://www.php.net/manual/de/features.gc.collecting-cycles.php)
- [Ръководство на PHP: gc_status](https://www.php.net/manual/de/function.gc-status.php)
- [Ръководство на PHP: memory_get_usage](https://www.php.net/manual/en/function.memory-get-usage.php)
- [php.net: Съобщения за сигурност](https://www.php.net/)
