Ein nächtlicher Import läuft vierzig Minuten und bricht dann mit Allowed memory size of 134217728 bytes exhausted ab. Welcher Teil des Skripts den Speicher frisst, kann niemand sagen, denn die Meldung nennt nur die Grenze und die Zeile, an der sie gerissen ist. PHP memory_get_usage() ist das Werkzeug, mit dem sich die Stelle finden lässt. Wie lange ein Skript läuft, misst microtime(), wie viel Rechenzeit es dem Prozessor abverlangt, getrusage(). Hier geht es um die dritte Größe: den Speicher, den PHP für das Skript belegt.
PHP führt zwei Zahlen, den aktuellen Stand und die Spitze, und für eine ehrliche Messung braucht man beide.
Was PHP memory_get_usage() zurückgibt: int in Bytes
Eine ganze Zahl in Bytes. Sie beschreibt den Stand im Augenblick des Aufrufs, nicht einen Durchschnitt und nicht den Höchstwert. Zwei Aufrufe hintereinander können deshalb zwei verschiedene Werte liefern, je nachdem, was dazwischen passiert ist. Einen Fehlerfall gibt es nicht: PHP memory_get_usage() gibt immer eine Zahl zurück und nie false.
Gezählt wird der Speicher, den der Zend Memory Manager an Variablen, Arrays, Objekte und interne Strukturen des Skripts vergeben hat. Über diesen Speichermanager wickelt PHP fast alle Allokationen ab. Was an ihm vorbeiläuft, taucht in der Zahl nicht auf, und genau das wird am Ende noch wichtig.
Die Signatur im Original
Drei Funktionen gehören zusammen:
memory_get_usage(bool $real_usage = false): int
/* real_usage: false = memory used by
emalloc() (default)
true = total memory allocated
from the system,
including unused pages
Returns the memory amount in bytes. */
memory_get_peak_usage(
bool $real_usage = false
): int
/* highest value since the script started
or since the last reset */
memory_reset_peak_usage(): void
/* PHP 8.2 and later: sets the peak
back to the current usage */
Die Einheit ist in allen Fällen Bytes. Der einzige Parameter von PHP memory_get_usage() entscheidet nicht über die Genauigkeit, sondern darüber, welche Ebene gemessen wird. Wer das übersieht, vergleicht später zwei Zahlen, die nichts miteinander zu tun haben.
real_usage: true vs. false
Mit false, dem Standard, meldet PHP memory_get_usage() die Bytes, die das Skript belegt. Mit true meldet sie, wie viel der Speichermanager dafür beim Betriebssystem geholt hat. Er holt nicht byteweise, sondern in Blöcken zu 2 MiB, den sogenannten Chunks. Bei gewöhnlichen Werten springt der true-Wert deshalb in Schritten von 2097152 Bytes und ist fast immer der größere. Einzelne große Allokationen ab knapp 2 MiB bekommen einen eigenen Block, den PHP unter Windows ebenfalls auf 2 MiB aufrundet, unter Linux aber nur auf 4 KiB. Nach range(1, 100000) stand der true-Wert unter Windows bei 6291456 Bytes, unter Linux bei 4198400. Ungenutzter Platz in einem geholten Block zählt mit. Der true-Wert ist also nicht der „tatsächlich genutzte“ Speicher, wie man oft liest, sondern der reservierte. Der genutzte Anteil ist gerade der false-Wert.
<?php
echo memory_get_usage(), "\n";
echo memory_get_usage(true), "\n";
/* Linux, PHP 8.5.8, leeres Skript:
rund 0,45 MiB und genau 2,00 MiB */
Neben den beiden Werten von PHP memory_get_usage() gibt es eine dritte Zahl, die des Betriebssystems, etwa in top oder im Task-Manager. Sie umfasst den ganzen Prozess: das PHP-Programm selbst, geladene Erweiterungen, Bibliotheken und den Anteil am OPcache. Im selben Versuch stand die VmRSS des Prozesses bei 22,24 MiB. Das folgende Bild zeigt, wie die drei Ebenen aufeinander liegen und an welcher Stelle die Speichergrenze prüft:
flowchart TD
A[PHP-Skript] -->|false: vergebene Bytes| B
B[Zend Memory Manager] --> C
C[Chunks zu 2 MiB] -->|true: geholte Blöcke| D
D[Betriebssystem]
C -.-> E[memory_limit prüft hier]
A --> F[Erweiterung mit eigener Lib]
F -->|nicht gezählt| D
Speicherverbrauch lesbar in KiB und MiB ausgeben
Die rohen Bytes aus PHP memory_get_usage() liest niemand gern. Umgerechnet wird mit 1024, nicht mit 1000, weil PHP selbst so rechnet: 128M im memory_limit sind 134217728 Bytes, also 128 × 1024 × 1024. Streng genommen heißen diese Einheiten KiB und MiB. Im PHP-Umfeld steht oft „MB“, gemeint sind aber fast immer dieselben 1024er-Schritte.
<?php
function speicher_lesbar(int $bytes): string
{
if ($bytes < 1024) {
return $bytes . ' Bytes';
}
$einheiten = ['KiB', 'MiB', 'GiB'];
$wert = $bytes / 1024;
$i = 0;
while ($wert >= 1024 && $i < 2) {
$wert /= 1024;
$i++;
}
return number_format($wert, 2, ',', '.')
. ' ' . $einheiten[$i];
}
echo speicher_lesbar(memory_get_usage());
/* Windows, PHP 8.4.8: 408,42 KiB */
echo speicher_lesbar(134217728);
/* 128,00 MiB */
Die Funktion setzt das deutsche Zahlenformat mit Komma. Für ein Protokoll, das später maschinell ausgewertet wird, ist der rohe Bytewert besser. Dort gehört die Umrechnung in die Auswertung und nicht in die Datei.
Differenzen messen statt Einzelwerte lesen
Ein einzelner Wert von PHP memory_get_usage() sagt wenig. Er enthält alles, was bis dahin geladen wurde: Autoloader, Konfiguration, Bibliotheken. Aussagekräftig ist erst die Differenz um einen Codeabschnitt. Der Messrahmen ist schlicht: vorher messen, den Abschnitt ausführen, nachher messen. Eine Zeitmessung lässt sich mit microtime() in denselben Rahmen setzen.
<?php
$vorher = memory_get_usage();
$zahlen = range(1, 100000);
$nachher = memory_get_usage();
echo $nachher - $vorher, ' Bytes';
/* Linux, PHP 8.5.8: 2101328 Bytes
Windows, PHP 8.4.8: 4194384 Bytes */
Zwei Ergebnisse von PHP memory_get_usage() verwirren beim ersten Messen fast jeden. Eine Zuweisung wie $t = 'abc'; kostet 0 Bytes. Der Text liegt als interne Zeichenkette schon seit dem Kompilieren im Speicher, die Variable verweist nur darauf. Auch die Kopie eines Arrays mit 100.000 Elementen kostet zunächst nichts. PHP teilt sich die Daten, bis eine der beiden Variablen verändert wird, und erst dann wird kopiert. Das Verfahren heißt Copy-on-Write:
<?php
$a = range(1, 100000);
$m = memory_get_usage();
$t = 'abc';
echo memory_get_usage() - $m; /* 0 */
$m = memory_get_usage();
$c = $a;
echo memory_get_usage() - $m; /* 0 */
$c[] = 1;
echo memory_get_usage() - $m;
/* Linux, PHP 8.5.8: 2101328
Windows, PHP 8.4.8: 4194384 */
Umgekehrt gibt unset() den Speicher sofort frei, sofern keine weitere Variable auf die Daten verweist. Unter Windows mit PHP 8.4.8 fiel der Wert von PHP memory_get_usage() nach dem Löschen eines Arrays von 4,40 MiB auf 0,40 MiB, und auch der true-Wert sank zurück auf 2,00 MiB. Die Garbage Collection mit gc_collect_cycles() braucht es nur für zirkuläre Verweise, etwa bei Objekten, die sich gegenseitig referenzieren.
memory_get_peak_usage(): der Höchststand des Skripts
Der Stand am Ende eines Skripts ist meist der kleinste, den es je hatte. Temporäre Arrays sind dann schon freigegeben, Ergebnismengen verworfen. Wer wissen will, wie viel Speicher eine Seite braucht, fragt deshalb nach der Spitze. memory_get_peak_usage() liefert den höchsten Wert seit dem Start, mit demselben Parameter und in derselben Einheit wie PHP memory_get_usage().
Die Spitze steigt, fällt aber nie von selbst. Nach dem unset() aus dem vorigen Abschnitt blieb sie bei 4,40 MiB stehen, obwohl der aktuelle Stand schon wieder bei 0,40 MiB lag. Für eine Statusleiste am Seitenende ist sie damit der richtige Wert:
<?php
/* ganz unten im Layout */
printf(
'<!-- Peak %s, reserviert %s -->',
speicher_lesbar(memory_get_peak_usage()),
speicher_lesbar(memory_get_peak_usage(true))
);
Den Peak mit memory_reset_peak_usage() zurücksetzen
In einem Worker, der Hunderte Aufträge nacheinander abarbeitet, hilft der Gesamthöchststand wenig. Nach dem ersten großen Auftrag zeigt er für immer denselben Wert. Seit PHP 8.2 setzt memory_reset_peak_usage() die Spitze auf den aktuellen Stand zurück, und damit lässt sich jeder Auftrag einzeln vermessen. Im Beispiel von oben fiel die Spitze nach dem Zurücksetzen von 4,40 MiB auf 0,40 MiB (Windows, PHP 8.4.8).
<?php
foreach ($auftraege as $auftrag) {
memory_reset_peak_usage();
verarbeite($auftrag);
error_log(sprintf(
'Auftrag %d: Peak %d Bytes',
$auftrag['id'],
memory_get_peak_usage()
));
}
Der protokollierte Wert enthält den Grundbedarf des Workers, der beim Zurücksetzen schon belegt war. Für den Vergleich der Aufträge untereinander stört das nicht, er steckt in jeder Zeile gleich. Auf älteren Versionen bleibt nur die Differenz zwischen Spitze und Stand vor dem Abschnitt. Sie stimmt aber nur, wenn der Abschnitt einen neuen Höchststand erreicht. Sonst zeigt sie die Spitze eines früheren Auftrags.
memory_limit und die Zahl in der Fehlermeldung
Die Zahl in Allowed memory size of 134217728 bytes exhausted ist das Limit in Bytes, hier 128 MiB. Geprüft wird es gegen die geholten Blöcke, also gegen den true-Wert von PHP memory_get_usage(). Ein Versuch mit ini_set('memory_limit', '8M') und einer wachsenden Schleife zeigt das: Beim Abbruch stand der true-Wert auf exakt 8388608 Bytes, dem Limit. PHP memory_get_usage() ohne Parameter meldete im selben Moment 8136328 Bytes (Linux, PHP 8.5.8) beziehungsweise 8122848 Bytes (Windows, PHP 8.4.8).
Wer den eigenen Verbrauch mit dem Limit vergleichen will, braucht es in Bytes. ini_get() liefert aber einen String wie '128M'. Seit PHP 8.2 rechnet ini_parse_quantity() ihn um, auch Schreibweisen wie 1G, die selbstgebaute Umrechnungen mit substr() gern übersehen. Der Wert -1 bedeutet: kein Limit.
<?php
$roh = ini_get('memory_limit'); /* '128M' */
$limit = ini_parse_quantity($roh); /* 134217728 */
if ($limit === -1) {
echo 'kein memory_limit gesetzt';
} else {
$peak = memory_get_peak_usage(true);
printf(
'%.1f %% des Limits belegt',
$peak / $limit * 100
);
}
Um einen Speicherfehler einzugrenzen, braucht es außerdem ein Protokoll, das auch nach dem Abbruch noch schreibt. Eine mit register_shutdown_function() registrierte Funktion läuft nach dem fatalen Fehler weiter, und memory_get_peak_usage() ist dort noch abrufbar. Im Versuch mit dem 8-MiB-Limit stand in dieser Zeile die Fehlermeldung samt Spitzenwert.
<?php
register_shutdown_function(function (): void {
$fehler = error_get_last();
error_log(sprintf(
'%s peak=%d real=%d fehler=%s',
$_SERVER['REQUEST_URI'] ?? 'cli',
memory_get_peak_usage(),
memory_get_usage(true),
$fehler['message'] ?? '-'
));
});
Erst mit diesem Protokoll lohnt die Frage, ob das Limit steigen soll. Wer es hochsetzt, bevor er die Stelle kennt, verschiebt den Abbruch nur auf den nächsten größeren Import.
Speicher sparen: Generator statt Array
Wächst der Wert von PHP memory_get_usage() in einer Schleife von Eintrag zu Eintrag, wird irgendwo gesammelt statt verarbeitet. Eine Protokollzeile alle 1000 Durchläufe genügt, um das zu sehen:
<?php
foreach ($datensaetze as $i => $satz) {
importiere($satz);
if ($i % 1000 === 0) {
error_log($i . ': ' . memory_get_usage());
}
}
Der häufigste Grund für einen hohen Wert ist ein Array, das alle Daten auf einmal hält. Ein Generator liefert die Werte dagegen einzeln mit yield, im Speicher steht immer nur einer davon. Der Vergleich, mit memory_reset_peak_usage() vor jedem Lauf, gemessen unter Linux mit PHP 8.5.8:
<?php
function zahlen(int $bis): Generator
{
for ($i = 1; $i <= $bis; $i++) {
yield $i;
}
}
memory_reset_peak_usage();
$basis = memory_get_peak_usage();
$summe = array_sum(range(1, 1000000));
echo memory_get_peak_usage() - $basis;
/* 16781392 Bytes, rund 16 MiB */
memory_reset_peak_usage();
$basis = memory_get_peak_usage();
$summe = 0;
foreach (zahlen(1000000) as $z) {
$summe += $z;
}
echo memory_get_peak_usage() - $basis;
/* 608 Bytes */
Aus 16 MiB Spitze werden 608 Bytes, bei identischem Ergebnis. Beim Einlesen von Dateien ist der Abstand ähnlich groß. file() erhöhte die Spitze bei einer 20-MB-Datei mit 200.000 Zeilen um 49807928 Bytes, also um rund 47,5 MiB und damit mehr als das Doppelte der Datei. Eine Schleife mit fgets() kam auf rund 9 KiB. Wie das zeilenweise Lesen im Einzelnen funktioniert, zeigt der Artikel Dateihandling mit PHP.
Was PHP memory_get_usage() nicht sieht
PHP memory_get_usage() zählt nur, was über den Zend-Speichermanager läuft. Erweiterungen, die externe Bibliotheken mit eigener Speicherverwaltung einbinden, bleiben unsichtbar. Das deutlichste Beispiel ist libxml, die Grundlage von DOM und SimpleXML. Gemessen im Linux-Container mit PHP 8.5.8: Ein DOMDocument mit 300.000 Elementen ließ den gemeldeten Wert unverändert bei 5,31 MiB und den true-Wert bei 6,87 MiB. Die VmRSS des Prozesses stieg im selben Moment von 27,48 MiB auf 178,89 MiB.
Dieser Speicher wird weder gezählt noch vom memory_limit begrenzt. Ein XML-Import kann deshalb einen Server an seine Grenzen bringen, ohne dass PHP je eine Speicherfehlermeldung ausgibt. Für jede Erweiterung gilt das aber nicht. mysqlnd holt seinen Ergebnispuffer über den PHP-Speichermanager, und die Messung im Tutorial zu mysqli_free_result() zeigt ihn deshalb sauber an.
Auch absolute Zahlen aus PHP memory_get_usage() sind mit Vorsicht zu lesen. Dasselbe range(1, 100000) kostete unter Linux mit PHP 8.2.33 und 8.5.8 jeweils 2101328 Bytes, unter Windows mit PHP 8.4.8 aber 4194384 Bytes. Ursache ist genau die unterschiedliche Rundung großer Blöcke. Vergleiche taugen nur auf derselben Plattform, Richtwerte aus dem Netz lassen sich nicht übertragen. Unter Linux zeigt eine Diagnosezeile alle drei Ebenen nebeneinander:
<?php
$status = file_get_contents('/proc/self/status');
preg_match('/VmRSS:\s+(\d+) kB/', $status, $m);
printf(
"usage %d\nreal %d\nrss %d\n",
memory_get_usage(),
memory_get_usage(true),
(int) ($m[1] ?? 0) * 1024
);
/* /proc gibt es nur unter Linux, unter
Windows entfällt die dritte Zeile */
Wer den Speicher eines Prozesses als Ganzes bewerten muss, etwa für die Zahl der FPM-Prozesse auf einem Server, braucht also mehr als eine Zahl. Shell-Kommandos wie ps messen dabei nicht dieselbe Größe genauer, sondern die dritte Ebene. Die Werkzeuge im Überblick:
| Werkzeug | misst | taugt für |
| PHP memory_get_usage() und memory_get_peak_usage() | Speicher im Zend Memory Manager | Abschnitte im Code vergleichen, Protokoll pro Anfrage |
getrusage() mit ru_maxrss | Höchststand des Prozesses laut Betriebssystem | Gesamtbedarf einschließlich Erweiterungen |
| Xdebug-Profiler | Allokationen je Funktion | Fehlersuche in der Entwicklung, nicht im Betrieb |
top, Task-Manager, /proc | aktueller Speicher des ganzen Prozesses | Server bemessen, Prozesspools planen |
Fazit
PHP memory_get_usage() liefert Bytes, und zwar den Stand im Augenblick des Aufrufs. Ohne Parameter ist das der Speicher, den das Skript belegt, mit true der Speicher, den PHP beim System geholt hat, überwiegend in Blöcken zu 2 MiB. Gegen diesen zweiten Wert prüft das memory_limit.
Nützlich wird PHP memory_get_usage() erst als Differenz um einen Abschnitt und zusammen mit memory_get_peak_usage(), das den Höchststand festhält. Seit PHP 8.2 setzt memory_reset_peak_usage() diesen pro Auftrag zurück. Zeigt das System deutlich mehr Speicher als PHP, arbeitet vermutlich eine Bibliothek wie libxml am Speichermanager vorbei. Für diesen Teil hilft nur der Blick auf den Prozess.