Wie viel RAM ein Objekt wirklich kostet und warum leer nicht leer bedeutet
Ein PHP-Objekt ohne eine einzige Property belegt trotzdem mehrere Dutzend Bytes im Speicher, allein durch den internen zend_object-Header. Wer bei tausenden Objekten pro Request unerwartet hohen Speicherverbrauch sieht, findet die Ursache meist im Zusammenspiel aus Objekt-Header, Property-Tabelle und der Größe typisierter Properties, nicht im eigentlichen Nutzdatenanteil.
Inhaltsverzeichnis
- 1. Warum das Speicherprofil von Objekten überhaupt zählt
- 2. Der zend_object-Header im Detail
- 3. Property-Tabelle und die Kosten pro Eigenschaft
- 4. zval-Größe und Referenzzählung
- 5. Typisierte Properties und ihr Einfluss auf die Größe
- 6. Vererbung, Traits und geteilte Metadaten
- 7. Speicherverbrauch praktisch messen
- 8. Objektspeicher gezielt reduzieren
- 9. Objekttypen im Speichervergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum das Speicherprofil von Objekten überhaupt zählt
Solange eine Anwendung nur wenige hundert Objekte pro Request erzeugt, ist das genaue Speicherprofil eines einzelnen PHP-Objekts kaum relevant. Sobald aber ein Import-Skript zehntausende Value Objects aus einer CSV-Datei erzeugt, ein ORM tausende Entity-Objekte für eine Collection hydratisiert oder ein Report-Job Millionen kleiner Datenpunkte als Objekte hält, summiert sich der Overhead pro Objekt zu einem echten Speicherproblem, das entweder das memory_limit sprengt oder den Garbage Collector unter Dauerlast setzt.
Das grundlegende Missverständnis dabei: Viele Entwickler schätzen den Speicherbedarf eines Objekts, indem sie nur die Größe der enthaltenen Werte addieren, also etwa einen Integer mit acht Byte und einen kurzen String mit seiner Zeichenlänge. Das reale Speicherprofil eines PHP-Objekts ist aber deutlich größer, weil PHP für jedes Objekt einen internen Header, eine Property-Tabelle und für jede einzelne Property einen eigenen zval-Container verwaltet, unabhängig vom eigentlichen Wert.
Dieser Artikel schlüsselt das Speicherprofil eines PHP-Objekts Schicht für Schicht auf: vom zend_object-Header über die Property-Tabelle bis zu den Auswirkungen typisierter Properties. Am Ende steht eine praktische Anleitung, wie sich der tatsächliche Speicherverbrauch messen und in Grenzen gezielt reduzieren lässt, ohne auf die Vorteile von Objekten gegenüber Arrays zu verzichten.
2. Der zend_object-Header im Detail
Jedes PHP-Objekt wird intern durch eine C-Struktur namens zend_object repräsentiert. Diese Struktur enthält unter anderem einen Referenzzähler (refcount), einen Zeiger auf die zugehörige Klassendefinition (zend_class_entry), ein Handle für den internen Objekt-Store sowie Zeiger auf Handler-Tabellen, die festlegen, wie sich das Objekt bei Vergleichen, Klonen oder der Garbage Collection verhält. Auf einem typischen 64-Bit-System belegt dieser Header allein zwischen 40 und 56 Byte, abhängig von der genauen PHP-Version und Build-Konfiguration.
Entscheidend ist: Dieser Header existiert für jedes einzelne Objekt, unabhängig davon, wie viele Properties es hat oder ob es überhaupt Properties besitzt. Ein Objekt einer komplett leeren Klasse ohne jede Property kostet also bereits den vollen Header-Overhead, bevor überhaupt ein einziges Byte Nutzdaten hinzukommt. Das erklärt, warum das Instanziieren vieler kleiner, spezialisierter Objekte, etwa für jedes einzelne Zeichen eines Textes oder jeden einzelnen Wert einer sehr großen Zahlenreihe, in der Praxis unverhältnismäßig viel Speicher kostet.
Ein zweiter, oft übersehener Bestandteil des Speicherprofils ist die geteilte Klassendefinition selbst. Der zend_class_entry mit Methodentabelle, Property-Namen und Typinformationen wird nur einmal pro Klasse im Speicher gehalten, unabhängig davon, wie viele Instanzen existieren. Diese Struktur zählt deshalb nicht zum Speicherprofil einer einzelnen Instanz, ist aber bei der Gesamtbetrachtung des Speicherbudgets einer Anwendung mit vielen unterschiedlichen Klassen durchaus relevant.
3. Property-Tabelle und die Kosten pro Eigenschaft
Neben dem festen Header-Overhead trägt jede deklarierte Property eines Objekts zum Speicherprofil bei. PHP legt für die Properties eines Objekts eine Tabelle direkt im Anschluss an den zend_object-Header an, die sogenannten Properties Table. Jeder Eintrag in dieser Tabelle ist ein zval, ein Container fester Größe, der den eigentlichen Wert der Property sowie ein Typ-Tag enthält, unabhängig davon, ob die Property einen Integer, einen String oder ein weiteres Objekt hält.
Auf einem typischen 64-Bit-System belegt ein einzelner zval 16 Byte: acht Byte für den eigentlichen Wert oder einen Zeiger auf komplexere Daten, und weitere Bytes für Typ-Information und interne Flags. Ein Objekt mit zehn typisierten Properties trägt also allein durch die Property-Tabelle etwa 160 Byte zum Speicherprofil bei, zusätzlich zum Header-Overhead von 40 bis 56 Byte. Bei Strings, Arrays und weiteren Objekten als Property-Werten kommt zusätzlich der Speicherbedarf der referenzierten Struktur selbst hinzu, weil der zval in diesen Fällen nur einen Zeiger enthält, nicht den vollständigen Wert.
declare(strict_types=1);
// Measuring the real memory footprint of objects with many properties
final class OrderLine
{
public function __construct(
public readonly string $sku,
public readonly int $quantity,
public readonly float $unitPrice,
public readonly string $currency,
public readonly ?string $note = null,
) {}
}
$before = memory_get_usage();
// Allocate 100,000 small objects, five properties each
$lines = [];
for ($i = 0; $i < 100_000; $i++) {
$lines[] = new OrderLine(
sku: 'SKU-' . $i,
quantity: 1,
unitPrice: 9.99,
currency: 'EUR',
);
}
$after = memory_get_usage();
$perObject = ($after - $before) / count($lines);
printf("Memory per object: %.1f bytes\n", $perObject);
// Typically well above the naive sum of scalar values alone,
// because header + property table dominate for small objects
Bei diesem Beispiel liegt der reale Speicherverbrauch pro Objekt deutlich über der naiven Summe der Skalarwerte, weil Header und Property-Tabelle den größten Anteil ausmachen, während die eigentlichen Nutzdaten, ein kurzer String und drei Zahlen, nur einen kleinen Bruchteil beitragen. Genau dieses Verhältnis kehrt sich bei größeren Objekten mit langen Strings oder verschachtelten Strukturen um, wo die referenzierten Daten den Header-Overhead schnell übersteigen.
4. zval-Größe und Referenzzählung
Der zval-Container ist die kleinste Speichereinheit für Werte in PHP und taucht nicht nur in Objekt-Properties auf, sondern auch bei Array-Elementen und lokalen Variablen. Für die Größenbetrachtung von Objekten ist wichtig: Ein zval selbst hat eine feste Größe, unabhängig vom Typ des enthaltenen Wertes. Ein Integer, ein Boolean und ein Objekt-Zeiger belegen im zval gleich viel Platz, der eigentliche Unterschied entsteht erst durch die Daten, auf die bei komplexeren Typen zusätzlich verwiesen wird.
Für Strings, Arrays und Objekte als Property-Werte kommt die Referenzzählung ins Spiel: Der zval in der Property-Tabelle enthält lediglich einen Zeiger auf die eigentliche Zend-String- oder zend_array-Struktur, deren Referenzzähler bei jeder weiteren Zuweisung erhöht wird. Das bedeutet, dass zwei Objekte, die auf denselben unveränderten String verweisen, diesen String nicht doppelt im Speicher halten, solange keine der beiden Referenzen den Wert modifiziert und dadurch eine Kopie auslöst. Dieses Verhalten hängt eng mit Copy-on-Write bei Arrays zusammen, gilt in abgeschwächter Form aber auch für Strings.
Praktisch bedeutet das: Objekte mit vielen Properties, die auf denselben, unveränderten String verweisen, etwa ein wiederkehrender Kategoriename oder ein Status-Code, kosten pro zusätzlichem Objekt nur den zval-Overhead selbst, nicht die volle Stringlänge erneut. Erst wenn ein Objekt seine eigene, unabhängige Kopie eines Strings modifiziert, entsteht durch Copy-on-Write eine echte, zusätzliche Speicherallokation.
5. Typisierte Properties und ihr Einfluss auf die Größe
Seit PHP 7.4 unterstützt die Sprache typisierte Properties, und das hat direkte Auswirkungen auf das Speicherprofil eines Objekts. Eine typisierte Property, die noch keinen Wert erhalten hat, existiert intern in einem speziellen uninitialisierten Zustand, der weniger Speicher belegt als ein vollständig initialisierter zval. Sobald der Property jedoch ein Wert zugewiesen wird, wechselt sie in den regulären Zustand und belegt denselben Speicher wie eine untypisierte Property.
Der eigentliche Vorteil typisierter Properties liegt deshalb nicht primär im reduzierten Speicherverbrauch initialisierter Objekte, sondern in der Fehlervermeidung: Ein Zugriff auf eine typisierte, aber uninitialisierte Property wirft einen Error, statt stillschweigend null zurückzugeben, was Bugs deutlich früher sichtbar macht. Für das Speicherprofil relevant ist vor allem, dass typisierte Skalarwerte wie int oder float in PHP weiterhin über das volle zval-Format gespeichert werden, PHP also intern keine kompakteren, C-artigen Structs für typisierte Objekte anlegt, wie es etwa in Sprachen mit echtem Value-Type-System der Fall wäre.
Ein praktischer Vergleich zeigt den Unterschied zwischen einem Objekt mit lauter public-Properties und einem funktional äquivalenten assoziativen Array. Ein Array benötigt für jeden Schlüssel zusätzlich zum zval auch einen Hash des Schlüssels und einen Bucket-Eintrag in der internen Hashtabelle, was bei vielen kleinen Einträgen tendenziell mehr Overhead erzeugt als die Property-Tabelle eines Objekts mit denselben Werten. Objekte sind bei vielen kleinen, wiederkehrenden Datensätzen deshalb oft die speichereffizientere Wahl gegenüber assoziativen Arrays, auch wenn beide Varianten ähnliche Nutzdaten transportieren.
6. Vererbung, Traits und geteilte Metadaten
Vererbung wirkt sich auf zwei unterschiedliche Weisen auf das Speicherprofil aus. Erstens vergrößert jede zusätzliche Property, die eine Elternklasse deklariert, die Property-Tabelle jeder Instanz einer Kindklasse, weil geerbte Properties Teil derselben zusammenhängenden Tabelle sind wie die eigenen Properties der Kindklasse. Zweitens bleibt die Methodentabelle, anders als Properties, vollständig auf Klassenebene geteilt: Methoden kosten pro Instanz keinen zusätzlichen Speicher, unabhängig davon, wie viele Methoden eine Klasse oder ihre Elternklassen definieren.
Traits verhalten sich beim Speicherprofil ähnlich wie direkt in der Klasse deklarierte Properties und Methoden: Der PHP-Compiler kopiert die Trait-Deklarationen effektiv in die verwendende Klasse hinein, bevor die Klasse selbst kompiliert wird. Properties aus einem Trait tragen deshalb genauso zur Property-Tabelle jeder Instanz bei wie direkt deklarierte Properties, es entsteht kein zusätzlicher Overhead allein durch die Verwendung eines Traits gegenüber einer direkten Deklaration.
Für Anwendungen mit tiefen Vererbungshierarchien, etwa in Magento oder Symfony, wo Basisklassen häufig zahlreiche Properties für Caching, Event-Handling oder Konfiguration mitbringen, kann sich das schnell summieren: Eine Kindklasse mit fünf eigenen Properties, deren Elternklasse bereits fünfzehn Properties für interne Zwecke deklariert, erzeugt Instanzen mit zwanzig Einträgen in der Property-Tabelle, auch wenn im eigentlichen Anwendungscode nur fünf davon aktiv genutzt werden.
7. Speicherverbrauch praktisch messen
Die zuverlässigste Methode, das reale Speicherprofil von Objekten in der eigenen Anwendung zu messen, ist der direkte Vergleich von memory_get_usage() vor und nach der Erzeugung einer definierten Anzahl von Instanzen, wie im vorherigen Codebeispiel gezeigt. Wichtig dabei: memory_get_usage(true) mit dem Parameter true liefert den vom System tatsächlich allozierten Speicher inklusive interner Fragmentierung, während memory_get_usage() ohne Parameter nur den von PHP als belegt gemeldeten Speicher zeigt, was für die reine Objektgrößen-Analyse meist die genauere Metrik ist.
Für eine detailliertere Analyse einzelner Objekte eignet sich die Erweiterung xdebug_debug_zval() aus Xdebug, die Referenzzähler und interne Struktur eines einzelnen Werts anzeigt, sowie Tools wie memory_get_peak_usage(), um den Spitzenverbrauch während eines kompletten Requests zu erfassen, nicht nur eine Momentaufnahme. Externe Profiler wie Blackfire oder XHProf liefern zusätzlich eine Aufschlüsselung des Speicherverbrauchs nach Funktionsaufruf, was hilft, genau die Stelle im Code zu identifizieren, an der unerwartet viele Objekte gleichzeitig im Speicher gehalten werden.
Ein praktischer Fallstrick beim Messen: Der PHP Garbage Collector räumt zirkuläre Referenzen nicht sofort auf, sondern sammelt sie in einem separaten Root-Buffer und verarbeitet sie erst, wenn dieser Buffer eine bestimmte Füllstandsgrenze erreicht. Wer Speicher unmittelbar nach dem Löschen von Objektreferenzen misst, ohne vorher gc_collect_cycles() explizit aufzurufen, sieht unter Umständen einen höheren Speicherverbrauch, als tatsächlich final benötigt wird, weil noch nicht eingesammelte zirkuläre Referenzen den Speicher blockieren.
declare(strict_types=1);
// Force garbage collection before measuring released memory,
// otherwise circular references may still hold memory
$before = memory_get_usage();
$objects = [];
for ($i = 0; $i < 50_000; $i++) {
$objects[] = new OrderLine('SKU-' . $i, 1, 9.99, 'EUR');
}
$peak = memory_get_peak_usage();
unset($objects);
// Without an explicit collection cycle, freed memory may not
// be reflected immediately if circular references exist
gc_collect_cycles();
$after = memory_get_usage();
printf("Peak: %.2f MB, released to: %.2f MB\n", $peak / 1_048_576, $after / 1_048_576);
8. Objektspeicher gezielt reduzieren
Wer nach dem Messen feststellt, dass das Speicherprofil der eigenen Objekte tatsächlich zum Problem wird, hat mehrere praktikable Hebel. Der erste und wirkungsvollste: die Anzahl gleichzeitig im Speicher gehaltener Objekte reduzieren, statt die Größe jedes einzelnen Objekts zu optimieren. Ein Generator, der Objekte einzeln erzeugt und verarbeitet, statt eine komplette Collection als Array im Speicher zu halten, reduziert den Spitzenverbrauch oft um Größenordnungen, ohne am Objektdesign selbst etwas zu ändern.
Der zweite Hebel betrifft die Anzahl der Properties selbst. Value Objects mit vielen selten genutzten, optionalen Properties können in kleinere, gezielt zusammengesetzte Objekte aufgeteilt werden, sodass nur die tatsächlich benötigten Daten im Speicher gehalten werden, statt eines großen Objekts mit vielen ungenutzten null-Properties. Auch hier gilt: Jede zusätzliche Property kostet mindestens einen zval-Eintrag in der Property-Tabelle, unabhängig davon, ob sie im konkreten Anwendungsfall überhaupt einen sinnvollen Wert trägt.
Ein dritter, weniger offensichtlicher Hebel ist die bewusste Wahl zwischen Objekten und Arrays für sehr große, homogene Datenmengen. Für Millionen gleichartiger, einfacher Datensätze kann ein flaches, typisiertes Array mit fester Struktur, etwa bei der Verarbeitung von CSV-Zeilen, tatsächlich weniger Speicher benötigen als dieselbe Anzahl an Objektinstanzen, weil der feste Header-Overhead pro Objekt bei Arrays entfällt. Die Entscheidung zwischen Objekt und Array sollte deshalb nicht allein an Lesbarkeitsargumenten, sondern bei sehr großen Mengen auch am tatsächlich gemessenen Speicherprofil festgemacht werden.
Ein direkter Vergleich verdeutlicht, wie unterschiedliche Datenstrukturen bei gleicher Nutzlast im Speicherprofil abschneiden.
9. Objekttypen im Speichervergleich
| Struktur | Header-Overhead | Overhead pro Feld | Wann sinnvoll |
|---|---|---|---|
| Objekt, 5 Properties | ~48 Byte zend_object | 16 Byte je zval | Typsicherheit, Methoden nötig |
| Assoziatives Array, 5 Keys | ~56 Byte zend_array | zval + Hash + Bucket | Dynamische, unbekannte Keys |
| Numerisches Array, 5 Werte | ~56 Byte zend_array | zval + Bucket, kein Hash | Große, homogene Listen |
| Generator statt Array | Kein Objekt pro Element | Ein Element gleichzeitig | Sehr große Datenmengen |
| Vererbte Basisklasse | Header + geerbte Properties | Kumuliert über Hierarchie | Prüfen bei tiefer Vererbung |
Der Vergleich zeigt: Es gibt keine pauschal beste Struktur, sondern jede Wahl hat ein eigenes Speicherprofil mit klaren Vor- und Nachteilen. Für kleine, überschaubare Objektmengen spielt der Unterschied selten eine Rolle, für Massenverarbeitung mit hunderttausenden oder Millionen Instanzen entscheidet die richtige Wahl über spürbar unterschiedlichen Speicherbedarf.
10. Zusammenfassung
Das Speicherprofil von PHP-Objekten setzt sich aus drei Bestandteilen zusammen: dem festen zend_object-Header von etwa 40 bis 56 Byte, der Property-Tabelle mit einem 16 Byte großen zval pro Eigenschaft, und dem tatsächlichen Speicherbedarf komplexerer Werte wie Strings, Arrays oder verschachtelter Objekte, auf die nur verwiesen wird. Typisierte Properties verändern diesen Speicherbedarf im initialisierten Zustand kaum, reduzieren aber das Fehlerrisiko durch frühzeitige Exceptions bei uninitialisierten Zugriffen.
Wer den Speicherverbrauch der eigenen Anwendung tatsächlich optimieren will, sollte zuerst mit memory_get_usage() und memory_get_peak_usage() messen, statt zu raten, und dann primär an der Anzahl gleichzeitig gehaltener Objekte ansetzen, etwa über Generatoren, statt an der Größe einzelner Instanzen. Das Speicherprofil einzelner Objekte zu kennen, ist die Grundlage jeder fundierten Entscheidung zwischen Objekten, Arrays und Generatoren bei großen Datenmengen.
Speicherprofil von PHP-Objekten, das Wichtigste auf einen Blick
zend_object-Header
Etwa 40 bis 56 Byte fester Overhead pro Instanz, unabhängig von der Anzahl der Properties.
zval pro Property
16 Byte je deklarierter Property, unabhängig vom Typ. Strings und Objekte kommen als zusätzliche Referenz oben drauf.
Typisierte Properties
Reduzieren Fehler durch frühzeitige Exceptions, verändern aber die Größe initialisierter Werte kaum.
Messen statt raten
memory_get_usage() vor und nach der Objekterzeugung zeigt den realen Overhead pro Instanz zuverlässig.
11. FAQ: Speicherprofil von PHP-Objekten
1Wie viel Speicher belegt ein leeres PHP-Objekt?
2Wie groß ist ein zval in PHP?
3Sparen typisierte Properties Speicher?
4Objekte oder Arrays: was ist effizienter?
5Wie messe ich realen Objektspeicher?
6Kostet Vererbung zusätzlichen Speicher?
7Erzeugen Traits Speicher-Overhead?
8Warum Generatoren statt Arrays?
9Was bewirkt gc_collect_cycles bei der Messung?
10Wann lohnt sich Array statt Objekt bei großen Mengen?
Mironsoft
PHP Speicher-Profiling, Performance-Audits und Refactoring datenintensiver Prozesse
Speicherprobleme bei großen Objektmengen im Griff?
Wir analysieren das Speicherprofil eurer Datenverarbeitung, identifizieren unnötigen Objekt-Overhead und bauen speicherschonende Alternativen mit Generatoren und gezielt reduzierten Value Objects.
Speicher-Profiling
Messung des realen Overheads über memory_get_usage und Peak-Analyse in Produktionslasten
Datenmodell-Review
Bewertung von Objekt- versus Array-Strukturen für große, homogene Datenmengen
Import- und Batch-Optimierung
Umstellung speicherintensiver Import- und Report-Skripte auf Generatoren und Streaming