PHP: Was tun bei "Allowed memory size exhausted"?
Fatal error: Allowed memory size of 8388608 bytes exhausted
(tried to allocate 12288 bytes)
Die erste Antwort, die man dazu findet, lautet: Limit hochsetzen. Das ist auch manchmal richtig. Meistens ist es aber die Reaktion darauf, dass eine Datei komplett in den Speicher geladen wird, die man auch zeilenweise lesen könnte — und der Unterschied ist Faktor tausend.
Erst einmal: Wo stehe ich?
Bevor man irgendetwas ändert, lohnt sich ein Blick auf die Zahlen:
<?php
echo ini_get('memory_limit'), "\n";
echo memory_get_usage(), "\n";
echo memory_get_usage(true), "\n";
echo memory_get_peak_usage(true), "\n";
?>
memory_limit : 512M
memory_get_usage() : 0.46 MiB
memory_get_usage(true) : 2.00 MiB
Der Unterschied zwischen den beiden ist erklärungsbedürftig. Ohne Parameter
bekommst du, was PHP für deine Variablen verwendet. Mit true bekommst du,
was PHP beim Betriebssystem angefordert hat — und das ist der Wert, an dem das
Limit gemessen wird. Für die Frage “wie nahe bin ich dran” ist also true der
ehrlichere.
memory_get_peak_usage() bleibt auf dem Höchststand stehen, auch wenn der
Speicher längst wieder frei ist:
Spitze bisher : 6.28 MiB
aktuell : 0.46 MiB
Das ist der Wert, den du am Ende eines Skripts ins Log schreiben willst. Denn wenn dein Skript abstürzt, ist es die Spitze gewesen, die das Limit gerissen hat — nicht der Verbrauch am Ende.
Der eine Punkt, an dem es sich meistens entscheidet
Dieselbe Datei, 11,63 MiB groß, 200.000 Zeilen. Drei Arten, sie zu lesen:
file_get_contents 11.64 MiB
file() als Array 22.31 MiB
fgets() Zeile fuer Zeile 0.01 MiB
Die dritte Zeile ist kein Tippfehler. Zehn Kilobyte.
file_get_contents() legt die ganze Datei als einen String ab, also
erwartungsgemäß etwa die Dateigröße. file() braucht fast das Doppelte,
weil jede der 200.000 Zeilen ein eigener String mit eigenem Verwaltungsaufwand
wird. Und der Streaming-Weg braucht praktisch nichts, weil zu jedem Zeitpunkt
nur eine einzige Zeile im Speicher liegt.
<?php
$fh = fopen($datei, 'r');
while (($zeile = fgets($fh)) !== false) {
// eine Zeile verarbeiten
}
fclose($fh);
?>
Bei einer 12-MiB-Datei ist das noch egal. Bei einer 2-GiB-Logdatei entscheidet
es darüber, ob dein Skript läuft oder nicht — und da hilft auch kein
memory_limit = -1, weil der Server den Speicher schlicht nicht hat.
Dasselbe Prinzip gilt an anderen Stellen:
- Datenbankabfragen: nicht
fetchAll(), sondern in einer Schleifefetch(). - CSV:
fgetcsv()statt die Datei einlesen und selbst zerlegen. - JSON: bei großen Dokumenten einen Streaming-Parser, denn
json_decode()baut das komplette Dokument im Speicher auf. - Bilder: GD hält ein Bild unkomprimiert im Speicher, vier Byte je Bildpunkt. Ein 4000×3000-Foto ist als JPEG vielleicht 2 MiB groß — gemessen belegt es in GD 46,91 MiB, was der Rechnung 4000 × 3000 × 4 Byte entspricht. Bei einem Standardlimit von 128 MiB sind drei solche Bilder gleichzeitig schon zu viel.
Was ein Array wirklich kostet
Damit die Zahlen nicht so überraschend sind:
range(1, 100000) : 2.00 MiB -> 21.0 Byte pro Integer
100.000 kurze Strings : 5.82 MiB
Ein Integer belegt für sich 8 Byte. Im Array kostet er 21, weil PHP zu jedem Element noch Hash-Tabelleneinträge und Typinformationen verwaltet.
Das erklärt, warum 50.000 Datenbankzeilen mit je zehn Spalten so viel mehr Speicher brauchen, als die Summe der Feldinhalte vermuten lässt.
Copy on write
Ein Effekt, der die Suche nach der Ursache erschwert:
$kopie = $gross : 0.00 MiB <- noch keine Kopie
nach einer Aenderung : 4.00 MiB <- jetzt wurde kopiert
Die Zuweisung kostet nichts. PHP merkt sich nur, dass zwei Variablen auf dieselben Daten zeigen. Erst wenn du eine der beiden änderst, wird tatsächlich kopiert.
Für die Fehlersuche heißt das: Der Speicher explodiert nicht an der Zeile mit der Zuweisung, sondern irgendwo weiter unten, an einer harmlos aussehenden Änderung. Wer das nicht weiß, sucht an der falschen Stelle.
Den Fehler abfangen — geht nicht, aber auslesen geht
Der naheliegende Versuch:
<?php
try {
// viel Speicher verbrauchen
} catch (Throwable $e) {
echo 'gefangen'; // wird nie erreicht
}
?>
Funktioniert nicht. Das Überschreiten des Limits ist ein Fatal Error und kein
Error-Objekt, also gibt es nichts zu fangen. Der catch-Zweig wird nicht
betreten, das Skript ist an dieser Stelle einfach zu Ende.
Was aber geht:
<?php
register_shutdown_function(function (): void {
$f = error_get_last();
if ($f !== null && $f['type'] === E_ERROR) {
// hier protokollieren, aufraeumen, Fehlerseite ausliefern
error_log($f['message'] . ' in ' . $f['file'] . ':' . $f['line']);
}
});
?>
[shutdown] Allowed memory size of 8388608 bytes exhausted (tried to allocate 12288 bytes)
[shutdown] in sprengen.php Zeile 16
Die Funktion läuft auch nach einem Fatal Error noch. Datei und Zeile bekommst du also — und das ist genau die Information, die du brauchst. Nur weiterlaufen lassen kannst du das Skript nicht.
Der Exit-Code des Prozesses ist übrigens 255, derselbe wie bei jedem anderen Fatal Error.
Und wann setzt man das Limit doch hoch?
Es gibt legitime Fälle. Ein Import-Skript, das einmal im Monat läuft und tatsächlich einen großen Datenbestand im Speicher halten muss, darf mehr bekommen als eine Webseite:
<?php
ini_set('memory_limit', '512M');
?>
vorher : 512M -> nachher: 256M -> wieder : 64M
Das funktioniert zur Laufzeit, in beide Richtungen. Zwei Einschränkungen: Bei einem Parse Error hilft es nicht, weil die Zeile nie ausgeführt wird (dazu mehr im Artikel über Fehlermeldungen), und manche Hoster sperren die Direktive.
Was ich nicht empfehle, ist memory_limit = -1 auf einem Webserver. Das Limit
ist ja nicht dazu da, dich zu ärgern, sondern damit ein einzelner
Fehler — eine Endlosschleife, eine Abfrage ohne LIMIT — nicht den ganzen
Server mitnimmt. Ohne Limit hast du statt eines Fatal Errors in einem Prozess
einen Server, der zu swappen beginnt oder vom OOM-Killer beendet wird.
Für ein Kommandozeilenskript, das du selbst startest, ist -1 dagegen völlig
in Ordnung.
Zusammenfassung
- Zuerst messen:
memory_get_peak_usage(true)am Ende des Skripts loggen. - Dateien zeilenweise lesen statt am Stück — im Test 0,01 MiB statt 11,64 MiB.
file()braucht das Doppelte der Dateigröße.- Ein Integer im Array kostet 21 Byte, nicht 8.
- Copy on write: Der Speicher wird bei der Änderung belegt, nicht bei der Zuweisung.
- Der Fehler ist nicht fangbar, aber über
register_shutdown_function()underror_get_last()auslesbar. memory_limit = -1gehört auf die Kommandozeile, nicht auf einen Webserver.
Hinweis zu Netcup (Werbung)
Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).