# Upravljanje memorijom u PHP-u: brojanje referenci i garbage collection

> Upravljanje memorijom u PHP-u razumljivo: brojanje referenci, copy-on-write, ciklusi i garbage collector, funkcije mjerenja i zašto PHP2Shell cilja upravo ovdje.

Izvor: https://phpsos.de/hr/php/speicherverwaltung/  
Stanje: 25.10.2026  
Pružatelj: PHP SOS, PHP hitna pomoć i sigurnost poslužitelja. Hitna pomoć putem WhatsAppa: https://wa.me/491736494914?text=Pozdrav%20PHP%20SOS%2C%20trebamo%20pomo%C4%87%20s%20na%C5%A1im%20PHP%20poslu%C5%BEiteljem.%20Tvrtka%3A%20%E2%80%A6%20Web-stranica%3A%20%E2%80%A6

**Ukratko:** PHP memoriju oslobađa ponajprije brojanjem referenci: svaka vrijednost zna koliko varijabli na nju pokazuje i briše se čim taj brojač dosegne nulu. Za kružne reference, koje se tako nikad ne bi oslobodile, postoji garbage collector ciklusa. Na kraju svakog zahtjeva PHP dodatno čisti sve što je zahtjev zauzeo. Obje ranjivosti PHP2Shella tiču se stanja memorije: stanja nedostatka memorije tijekom obrade zahtjeva i internog stanja koje nakon oslobađanja ostaje predugo.

## Kako PHP upravlja memorijom: pregled

U jezicima poput C-a programer sâm mora zatražiti memoriju i ponovno je osloboditi. PHP to preuzima. Svaka varijabla, svako polje i svaki objekt zauzimaju memoriju kojom PHP upravlja putem vlastitog upravitelja memorije. Granicu za to postavlja `memory_limit`, prema priručniku prema zadanim postavkama `128M`.

Za donositelje odluka jedna je točka ključna: kod klasičnog rada s PHP-FPM-om ili mod_php-om izvode se dugovječni worker procesi koji jedan za drugim obrađuju mnogo zahtjeva. Što zahtjev ostavi iza sebe, mora se pouzdano počistiti prije sljedećeg. Upravo na tom mjestu leži jedna od ranjivosti PHP2Shella.

## Brojanje referenci: brojati tko koristi vrijednost

PHP svaku vrijednost sprema u spremnik koji se naziva zval. Uz tip i sadržaj prema priručniku vodi brojač referenci (`refcount`) i oznaku je li vrijednost dio PHP reference s `&` (`is_ref`).

```php
<?php
$a = str_repeat('x', 1000);  // jedna vrijednost, refcount 1
$b = $a;                     // bez kopiranja, refcount 2
$b .= 'y';                   // tek sada se kopira (copy-on-write)
unset($a);                   // refcount izvornika pada
// Kad brojač dosegne 0, PHP odmah oslobađa memoriju.
```

Načelo se zove copy-on-write: PHP kopira vrijednost tek kad je jedna od varijabli promijeni. To štedi mnogo memorije. Brojač varijable može se učiniti vidljivim ekstenzijom Xdebug i funkcijom `xdebug_debug_zval()`. Priručnik upozorava da se točni brojevi razlikuju od verzije do verzije PHP-a.

## Ciklusi: kad brojanje referenci nije dovoljno

Brojanje referenci ima poznatu slabost. Ako struktura upućuje sama na sebe, njezin brojač nikad ne padne na nulu.

```php
<?php
$a = ['eins'];
$a[] = &$a;   // polje sadrži referencu na samo sebe
unset($a);    // nitko mu više ne može pristupiti, ali refcount ostaje 1
```

Prema priručniku takve kružne reference javljaju se osobito kod objekata, primjerice u strukturama roditelj-dijete ili parserima. Pojedinačna curenja bezazlena su jer PHP na kraju zahtjeva čisti. U dugotrajnim procesima poput workera redova čekanja ili velikih testnih pokretanja milijuni ih se ipak mogu zbrojiti u pravi problem s memorijom.

## Garbage collector ciklusa

Od PHP-a 5.3.0 PHP za pronalaženje takvih ciklusa koristi algoritam prema Baconu i Rajanu. Osnovna ideja: ciklus može nastati samo ako brojač padne na vrijednost veću od nule. Samo te vrijednosti PHP pamti kao moguće korijene.

1. Mogući korijeni završavaju u međuspremniku. Priručnik kao zadanu veličinu navodi 10 000 unosa; trenutačno važeći prag pokazuje `gc_status()` u polju `threshold`.
2. Kad je međuspremnik pun, PHP prolazi kroz sve dostižne vrijednosti i probno smanjuje njihove brojače.
3. U drugom prolazu vrijedi: vrijednosti s brojačem nula su smeće, a kod svih ostalih smanjenje se poništava.
4. Korijeni se uklanjaju iz međuspremnika, a sve vrijednosti prepoznate kao smeće oslobađaju se.

Upravlja se time putem `zend.enable_gc` (prema zadanim postavkama uključeno) ili tijekom izvođenja funkcijama `gc_enable()` i `gc_disable()`. `gc_collect_cycles()` prisiljava prolaz i vraća broj skupljenih ciklusa.

## Mjerenje i promatranje memorije

| Funkcija | Svrha |
|---|---|
| `memory_get_usage()` | memorija koju skripta trenutačno zauzima |
| `memory_get_usage(true)` | memorija koju je upravitelj memorije rezervirao kod sustava; prema ovoj se vrijednosti provjerava `memory_limit` |
| `memory_get_peak_usage()` | najviša razina zauzete memorije |
| `gc_status()` | prolazi, skupljeni ciklusi, prag, korijeni; od PHP-a 8.3.0 i vremena izvođenja |
| `gc_collect_cycles()` | odmah pokreće skupljanje ciklusa |
| `WeakMap` | pridruživanje objektima bez sprječavanja njihova oslobađanja, od PHP-a 8.0 |

Prema priručniku `memory_get_usage()` bilježi samo memoriju koju PHP traži putem vlastitog upravitelja memorije. Ono što ekstenzije uzimaju izravno od operacijskog sustava nedostaje u tim brojkama. Za granicu memorije cijelog procesa zato su zaduženi operacijski sustav ili container.

## Upravljanje memorijom i PHP2Shell

Opisi obiju ranjivosti vode izravno u ovu temu. Kod CVE-2026-109618 stanje nedostatka memorije može prekinuti PHP dok su podaci zahtjeva tek djelomično inicijalizirani; pri čišćenju se nevaljani podaci tada mogu tretirati kao valjani. Što se interno događa pri prekoračenju memorije, objašnjava stranica [memory_limit i Out-of-Memory](https://phpsos.de/hr/php/out-of-memory/). Kod CVE-2026-109619 interno stanje zahtjeva može nakon oslobađanja memorije ostati i upotrijebiti ga kasniji zahtjev u istom workeru prije nego što se vrati na početno. Tijek od pokretanja do čišćenja opisuje stranica [Životni ciklus zahtjeva](https://phpsos.de/hr/php/request-lebenszyklus/).

Kao prijelaznu mjeru php.net preporučuje postaviti `memory_limit=-1` tako da ga aplikacije ne mogu prepisati, primjerice s `php_admin_value` u FPM poolu, a memoriju umjesto toga ograničiti putem containera ili cgroupa, a ne putem `RLIMIT_AS` ili `RLIMIT_DATA`. Kako to ide sa systemdom, pokazuje članak [Postavljanje ograničenja memorije pomoću systemda i cgroupa](https://phpsos.de/hr/blog/speichergrenzen-systemd-cgroups/).

```ini
; FPM pool: prijelazna mjera prema php.net, zatim ponovno pokrenuti sve workere
php_admin_value[memory_limit] = -1
```

> **Važno:**
> Bez granice putem containera ili cgroupa jedan proces može opteretiti cijeli poslužitelj. I nijedna postavka ne zamjenjuje ažuriranje na PHP 8.5.12, 8.4.27, 8.3.36 ili 8.2.35 uz ponovno pokretanje svih workera, jer samo ponovno pokretanje uklanja stare procese iz memorije.

## Kako PHP SOS konkretno pomaže?

- Provjeravamo djelotvorne postavke memorije po poolu i mogu li ih aplikacije prepisati.
- Prijelazne mjere provodimo uz ograničenje memorije na razini containera ili cgroupa i testiramo opterećenje.
- Instaliramo ažuriranje, ponovno pokrećemo sve workere i dokazujemo da više ne rade stari procesi, u okviru našeg [PHP patch servisa](https://phpsos.de/hr/leistungen/php-patch-service/).
- Analiziramo pogreške memorije i prekinute workere u zapisnicima od 20. 10. 2026.


## Česta pitanja

**Što je brojanje referenci u PHP-u?**  
PHP za svaku vrijednost broji koliko varijabli ili elemenata na nju upućuje. Kad brojač padne na nulu, memorija se odmah oslobađa. Tako se većina memorije vraća bez zasebnog prolaza čišćenja.

**Čemu PHP onda još treba garbage collector?**  
Za cikluse. Ako polje upućuje samo na sebe ili se dva objekta međusobno poznaju, brojač nikad ne padne na nulu, iako im više nitko ne pristupa. Garbage collector ciklusa pronalazi i uklanja takve strukture.

**Oslobađa li PHP memoriju na kraju zahtjeva?**  
Da, memoriju koju skripta zauzme tijekom zahtjeva PHP čisti na kraju zahtjeva. Kod PHP2Shella CVE-2026-109619 pogađa upravo slučaj u kojem unutarnje stanje nakon oslobađanja može ostati i upotrijebiti ga kasniji zahtjev u istom workeru.

**Zašto php.net kao prijelaznu mjeru preporučuje memory_limit=-1?**  
Ranjivost CVE-2026-109618 povezana je sa stanjem nedostatka memorije (out-of-memory). php.net preporučuje da se memory_limit=-1 postavi tako da ga aplikacije ne mogu prepisati, a memoriju umjesto toga ograničiti putem containera ili cgroupa. To nije potpuna zaštita i ne zamjenjuje ažuriranje.

## Izvori

- [PHP priručnik: Osnove brojanja referenci](https://www.php.net/manual/de/features.gc.refcounting-basics.php)
- [PHP priručnik: Skupljanje ciklusa](https://www.php.net/manual/de/features.gc.collecting-cycles.php)
- [PHP priručnik: gc_status](https://www.php.net/manual/de/function.gc-status.php)
- [PHP priručnik: memory_get_usage](https://www.php.net/manual/en/function.memory-get-usage.php)
- [php.net: Sigurnosne obavijesti](https://www.php.net/)
