# Muistinhallinta PHP:ssä: viitelaskenta ja roskienkeruu

> PHP:n muistinhallinta ymmärrettävästi: viitelaskenta, copy-on-write, kehät ja roskienkerääjä, mittausfunktiot ja syy, miksi PHP2Shell kohdistuu juuri tänne.

Lähde: https://phpsos.de/fi/php/speicherverwaltung/  
Tilanne: 25.10.2026  
Palveluntarjoaja: PHP SOS, PHP-hätäapu ja palvelimen tietoturva. Pikaapu WhatsAppilla: https://wa.me/491736494914?text=Hei%20PHP%20SOS%2C%20tarvitsemme%20apua%20PHP-palvelimemme%20kanssa.%20Yritys%3A%20%E2%80%A6%20Verkkosivusto%3A%20%E2%80%A6

**Lyhyesti:** PHP vapauttaa muistia ennen kaikkea viitelaskennalla: jokainen arvo tietää, kuinka moni muuttuja osoittaa siihen, ja se poistetaan heti, kun tämä laskuri saavuttaa nollan. Kehäviittauksille, joita ei näin koskaan vapautettaisi, on olemassa kehiä keräävä roskienkerääjä. Jokaisen pyynnön päättyessä PHP lisäksi siivoaa kaiken, minkä pyyntö on varannut. Kumpikin PHP2Shell-aukko kohdistuu muistitiloihin: Out-of-Memory-tilaan pyynnön käsittelyn aikana ja sisäiseen tilaan, joka säilyy vapautuksen jälkeen liian pitkään.

## Miten PHP hallitsee muistia: yleiskatsaus

Kielissä kuten C ohjelmoijan on itse pyydettävä muistia ja vapautettava se. PHP ottaa tämän huolehdittavakseen. Jokainen muuttuja, taulukko ja olio varaa muistia, jota PHP hallitsee omalla muistinhallinnallaan. Rajan tälle asettaa `memory_limit`, joka on käsikirjan mukaan oletuksena `128M`.

Päättäjälle yksi seikka on keskeinen: perinteisessä PHP-FPM- tai mod_php-käytössä pitkäikäiset worker-prosessit käsittelevät peräkkäin monta pyyntöä. Mitä pyyntö jättää jälkeensä, sen on oltava luotettavasti siivottu ennen seuraavaa. Juuri tässä kohdassa on toinen PHP2Shell-aukoista.

## Viitelaskenta: lasketaan, kuka käyttää arvoa

PHP tallentaa jokaisen arvon säiliöön, jota kutsutaan nimellä zval. Tyypin ja sisällön lisäksi siinä on käsikirjan mukaan viitelaskuri (`refcount`) ja merkintä siitä, onko arvo osa PHP-viitettä merkillä `&` (`is_ref`).

```php
<?php
$a = str_repeat('x', 1000);  // yksi arvo, refcount 1
$b = $a;                     // ei kopioida, refcount 2
$b .= 'y';                   // vasta nyt kopioidaan (copy-on-write)
unset($a);                   // alkuperäisen refcount laskee
// Kun laskuri saavuttaa arvon 0, PHP vapauttaa muistin heti.
```

Periaatetta kutsutaan nimellä copy-on-write: PHP kopioi arvon vasta, kun jokin muuttujista muuttaa sitä. Se säästää paljon muistia. Muuttujan laskurin voi tehdä näkyväksi Xdebug-laajennuksella ja funktiolla `xdebug_debug_zval()`. Käsikirja huomauttaa, että tarkat luvut voivat vaihdella PHP-versioittain.

## Kehät: kun viitelaskenta ei riitä

Viitelaskennalla on tunnettu heikkous. Jos rakenne osoittaa itseensä, sen laskuri ei koskaan putoa nollaan.

```php
<?php
$a = ['eins'];
$a[] = &$a;   // taulukko sisältää viitteen itseensä
unset($a);    // kukaan ei enää pääse siihen käsiksi, mutta refcount pysyy arvossa 1
```

Käsikirjan mukaan tällaisia kehäviittauksia esiintyy erityisesti olioissa, esimerkiksi vanhempi–lapsi-rakenteissa tai jäsentimissä. Yksittäiset vuodot ovat vaarattomia, koska PHP siivoaa pyynnön päättyessä. Pitkään käynnissä olevissa prosesseissa, kuten jonotyöntekijöissä tai suurissa testiajoissa, miljoonat tällaiset voivat kuitenkin kasvaa todelliseksi muisti-ongelmaksi.

## Kehiä keräävä roskienkerääjä

Versiosta PHP 5.3.0 lähtien PHP käyttää Baconin ja Rajanin algoritmia tällaisten kehien löytämiseen. Perusajatus: kehä voi syntyä vain, jos laskuri laskee arvoon, joka on suurempi kuin nolla. Vain nämä arvot PHP muistaa mahdollisina juurina.

1. Mahdolliset juuret päätyvät puskuriin. Käsikirja mainitsee oletuskooksi 10 000 alkiota; nykyisen kynnysarvon näyttää `gc_status()` kentässä `threshold`.
2. Kun puskuri on täynnä, PHP käy läpi kaikki tavoitettavissa olevat arvot ja pienentää kokeeksi niiden laskureita.
3. Toisella läpikäynnillä pätee: arvot, joiden laskuri on nolla, ovat roskaa; kaikilla muilla pienennys perutaan.
4. Juuret poistetaan puskurista ja kaikki roskaksi tunnistetut arvot vapautetaan.

Toimintaa ohjataan asetuksella `zend.enable_gc` (oletuksena päällä) tai ajon aikana funktioilla `gc_enable()` ja `gc_disable()`. `gc_collect_cycles()` pakottaa ajon ja palauttaa kerättyjen kehien määrän.

## Muistin mittaus ja seuranta

| Funktio | Tarkoitus |
|---|---|
| `memory_get_usage()` | komentosarjan tällä hetkellä varaama muisti |
| `memory_get_usage(true)` | muistinhallinnan järjestelmältä varaama muisti; tätä arvoa vasten `memory_limit` tarkistetaan |
| `memory_get_peak_usage()` | varatun muistin huippuarvo |
| `gc_status()` | ajot, kerätyt kehät, kynnysarvo, juuret; versiosta PHP 8.3.0 lähtien myös ajoajat |
| `gc_collect_cycles()` | käynnistää kehien keräyksen heti |
| `WeakMap` | liitos olioihin estämättä niiden vapautusta, versiosta PHP 8.0 lähtien |

Käsikirjan mukaan `memory_get_usage()` laskee vain muistin, jonka PHP pyytää omalla muistinhallinnallaan. Se, minkä laajennukset hakevat suoraan käyttöjärjestelmältä, puuttuu näistä luvuista. Koko prosessin muistirajasta vastaavat siksi käyttöjärjestelmä tai kontti.

## Muistinhallinta ja PHP2Shell

Molempien aukkojen kuvaukset johtavat suoraan tähän aiheeseen. Haavoittuvuudessa CVE-2026-109618 Out-of-Memory-tila voi keskeyttää PHP:n, kun pyyntödata on vasta osittain alustettu; siivouksessa virheellistä dataa voidaan sitten käsitellä kelvollisena. Mitä muistin ylityksessä tapahtuu sisäisesti, selittää sivu [memory_limit ja Out-of-Memory](https://phpsos.de/fi/php/out-of-memory/). Haavoittuvuudessa CVE-2026-109619 pyynnön sisäinen tila voi säilyä muistin vapautuksen jälkeen, ja myöhempi pyyntö samassa workerissa voi käyttää sitä ennen kuin se nollataan. Kulun alusta siivoukseen kuvaa sivu [Pyynnön elinkaari](https://phpsos.de/fi/php/request-lebenszyklus/).

Väliaikaisena toimenpiteenä php.net suosittelee asettamaan `memory_limit=-1` niin, etteivät sovellukset voi ylikirjoittaa sitä, esimerkiksi `php_admin_value`-direktiivillä FPM-poolissa, ja rajaamaan muistia sen sijaan kontilla tai cgroupeilla, ei `RLIMIT_AS`- tai `RLIMIT_DATA`-rajoilla. Miten se tehdään systemd:llä, näyttää artikkeli [Muistirajojen asettaminen systemd:llä ja cgroupeilla](https://phpsos.de/fi/blog/speichergrenzen-systemd-cgroups/).

```ini
; FPM-pooli: php.netin mukainen väliaikainen toimenpide, sen jälkeen kaikkien workerien uudelleenkäynnistys
php_admin_value[memory_limit] = -1
```

> **Tärkeää:**
> Ilman kontin tai cgroupin kautta asetettua rajaa yksittäinen prosessi voi kuormittaa palvelimen täyteen. Eikä mikään asetus korvaa päivitystä versioon PHP 8.5.12, 8.4.27, 8.3.36 tai 8.2.35 sekä kaikkien workerien uudelleenkäynnistystä, sillä vasta uudelleenkäynnistys poistaa vanhat prosessit muistista.

## Miten PHP SOS auttaa konkreettisesti?

- Tarkistamme voimassa olevat muistiasetukset poolikohtaisesti ja sen, voivatko sovellukset ylikirjoittaa ne.
- Toteutamme väliaikaiset toimenpiteet muistirajalla kontin tai cgroupin tasolla ja testaamme kuorman.
- Asennamme päivityksen, käynnistämme kaikki workerit uudelleen ja osoitamme, ettei vanhoja prosesseja enää ole käynnissä, [PHP-korjauspalvelumme](https://phpsos.de/fi/leistungen/php-patch-service/) puitteissa.
- Analysoimme lokeista muistivirheet ja keskeytyneet workerit 20.10.2026 alkaen.


## Usein kysytyt kysymykset

**Mitä viitelaskenta on PHP:ssä?**  
PHP laskee jokaiselle arvolle, kuinka moni muuttuja tai alkio viittaa siihen. Kun laskuri putoaa nollaan, muisti vapautetaan heti. Näin suurin osa muistista palautetaan ilman erillistä siivousajoa.

**Miksi PHP tarvitsee silti roskienkerääjän?**  
Kehiä varten. Jos taulukko viittaa itseensä tai kaksi oliota tuntee toisensa, laskuri ei koskaan putoa nollaan, vaikka kukaan ei enää käytä niitä. Kehiä keräävä roskienkerääjä löytää ja poistaa tällaiset rakenteet.

**Vapauttaako PHP muistin pyynnön päättyessä?**  
Kyllä, muistin, jonka komentosarja varaa pyynnön aikana, PHP siivoaa pyynnön päättyessä. PHP2Shellissä CVE-2026-109619 koskee kuitenkin juuri tapausta, jossa sisäinen tila voi säilyä vapautuksen jälkeen ja myöhempi pyyntö samassa workerissa voi käyttää sitä.

**Miksi php.net suosittelee memory_limit=-1 väliaikaisena toimenpiteenä?**  
Haavoittuvuus CVE-2026-109618 liittyy Out-of-Memory-tilaan. php.net suosittelee asettamaan memory_limit=-1 niin, etteivät sovellukset voi ylikirjoittaa sitä, ja rajaamaan muistia sen sijaan kontilla tai cgroupeilla. Se ei ole täydellinen suoja eikä korvaa päivitystä.

## Lähteet

- [PHP-käsikirja: viitelaskennan perusteet](https://www.php.net/manual/de/features.gc.refcounting-basics.php)
- [PHP-käsikirja: kehien kerääminen](https://www.php.net/manual/de/features.gc.collecting-cycles.php)
- [PHP-käsikirja: gc_status](https://www.php.net/manual/de/function.gc-status.php)
- [PHP-käsikirja: memory_get_usage](https://www.php.net/manual/en/function.memory-get-usage.php)
- [php.net: tietoturvailmoitukset](https://www.php.net/)
