Understanding PHP Memory Management
Memory management in PHP refers to the automated process by which the Zend Engine—PHP's core runtime—allocates, tracks, and reclaims memory during script execution. Unlike languages such as C where developers manually call malloc() and free(), PHP abstracts these details away through a sophisticated garbage collection system. Understanding how this system operates under the hood is essential for building performant, resource-efficient applications that handle large datasets, long-running processes, and high concurrency loads without exhausting server resources.
The Zval: PHP's Fundamental Data Container
At the heart of PHP's memory model lies the zval (Zend Value), a C-level structure that wraps every PHP variable. Each zval contains the variable's type tag, its value, and critical metadata for memory tracking. When you assign a value to a variable, the engine allocates a zval and stores the data within it. Here's a simplified conceptual view of what happens internally:
// When you write this PHP code:
$name = "Alice";
$age = 42;
// Internally, two zval structures are created:
// zval_1: type=STRING, value="Alice", refcount=1
// zval_2: type=INTEGER, value=42, refcount=1
//
// Each zval occupies memory for:
// - Type information (IS_STRING, IS_LONG, etc.)
// - The actual data bytes
// - Reference counting fields
// - Garbage collection metadata
The zval structure is the foundation upon which reference counting and copy-on-write mechanisms are built. Without it, PHP would lack the ability to safely share memory between variables or detect when memory can be safely freed.
Why Memory Management Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Effective memory management directly impacts application performance, hosting costs, and user experience. Consider these real-world scenarios:
Batch Processing and Data Imports
When importing a 2 GB CSV file, naive approaches can cause memory exhaustion halfway through, corrupting the import and requiring restarts. Memory-aware code processes the file in streaming chunks, keeping the memory footprint constant regardless of file size.
High-Traffic Web Applications
Each PHP request typically consumes between 2 MB and 50 MB of memory depending on complexity. On a server handling 500 concurrent requests, unoptimized code can quickly exhaust the available RAM, triggering OOM (Out of Memory) kills by the operating system. Efficient memory usage allows more requests to be served simultaneously on the same hardware.
Long-Running CLI Processes
Daemonized PHP workers, queue consumers, and WebSocket servers may run for hours or days. Even a tiny memory leak—perhaps 50 KB per iteration—can accumulate to gigabytes over thousands of cycles, eventually crashing the process. Understanding garbage collection helps prevent these slow-burn failures.
Cost Optimization in Cloud Environments
Cloud platforms charge by compute resources. Reducing your application's memory footprint allows you to use smaller, cheaper instance tiers. A codebase that runs comfortably on a 256 MB container costs significantly less to operate at scale than one requiring 2 GB per instance.
Reference Counting: The Primary Memory Reclamation Mechanism
PHP uses reference counting as its first line of defense in memory management. Every zval stores an integer counter tracking how many symbols (variables, array elements, object properties) currently point to it. When the reference count drops to zero, the memory is immediately freed.
// Simple reference counting demonstration
$artist = "Miles Davis"; // zval created, refcount = 1
$musician = $artist; // refcount incremented to 2
// Both variables share the SAME zval
// No memory duplication yet
unset($artist); // refcount decremented to 1
// $musician still references the zval
// Memory NOT freed yet
unset($musician); // refcount drops to 0
// Zval is immediately destroyed
// Memory is reclaimed by the engine
Copy-on-Write Optimization
PHP employs a copy-on-write (COW) strategy to delay memory duplication until absolutely necessary. When you assign one variable to another, both variables initially share the same zval. Only when one of them is modified does the engine create a separate copy. This lazy copying dramatically reduces memory usage in read-heavy operations.
// Demonstrating copy-on-write behavior
$largeArray = range(1, 100000); // Allocates ~4 MB for the array zval
$readOnly = $largeArray; // NO new memory allocated
// Both share the same zval
// refcount is now 2
// Memory usage remains unchanged at this point
echo "Memory: " . memory_get_usage(true) . " bytes\n";
$readOnly[0] = 999; // MODIFICATION triggers COW!
// A new zval is allocated for $readOnly
// $largeArray retains the original
// Memory usage nearly doubles
// After modification, two separate arrays exist
echo "Memory after COW: " . memory_get_usage(true) . " bytes\n";
Understanding COW is crucial when passing large arrays to functions. If the function only reads the array, no copy occurs. But a single write operation triggers a full duplication, potentially doubling memory consumption at that moment.
Explicit References and Their Memory Impact
Using the & operator creates an explicit reference, which fundamentally changes how the zval is handled. Explicit references force the engine to create a reference set, which disables certain COW optimizations and can lead to unexpected memory growth.
// Normal assignment with COW benefits
$data = range(1, 50000);
$copy = $data; // Shares zval, no extra memory
$copy[0] = 42; // COW triggers, now two zvals exist
// Explicit reference disables COW
$data2 = range(1, 50000);
$ref = &$data2; // Creates reference set, refcount mechanism changes
$ref[0] = 42; // Both $data2 and $ref see the change
// No COW duplication, but reference tracking overhead
// Passing by reference in functions
function modifyByValue($arr) { // Array is shared via COW until modified
$arr[0] = 'changed'; // COW triggers here
return $arr;
}
function modifyByReference(&$arr) { // Reference forced, COW disabled
$arr[0] = 'changed'; // Original array is mutated directly
}
$original = range(1, 100000);
$result1 = modifyByValue($original); // $original unchanged, memory spikes during copy
$result2 = modifyByReference($original); // $original modified, lower memory spike
Garbage Collection for Cyclic References
Reference counting alone cannot reclaim memory when circular references exist—a scenario where object A references object B, and object B references back to object A. Even when both objects become unreachable from the script's root scope, their mutual references keep their reference counts above zero forever. This is where PHP's cycle-collecting garbage collector steps in.
// Classic circular reference problem
class TreeNode {
public ?TreeNode $parent = null;
public ?TreeNode $child = null;
public function __construct() {
// Object allocated, refcount starts at 1
}
}
// Create a parent-child cycle
$parent = new TreeNode(); // refcount of parent object: 1
$child = new TreeNode(); // refcount of child object: 1
$parent->child = $child; // child's refcount becomes 2
$child->parent = $parent; // parent's refcount becomes 2
// Both objects now have refcount = 2
// (one from the variable, one from the property reference)
unset($parent); // parent's refcount drops to 1
unset($child); // child's refcount drops to 1
// PROBLEM: Both objects still have refcount = 1
// They reference each other through their properties
// Neither will be freed by simple reference counting
// Memory leak occurs WITHOUT garbage collection intervention
// PHP's cycle collector periodically scans for such orphaned cycles
// and reclaims them when found
When the Garbage Collector Runs
The cycle collector does not run on every variable unset operation. Instead, PHP maintains a buffer of potential garbage roots. When this buffer fills up (default threshold: 10,000 roots), the collector activates and performs a mark-and-sweep analysis to identify and free unreachable cycles. You can control this behavior with configuration directives and runtime functions.
// Monitoring garbage collection activity
echo "GC runs: " . gc_collect_cycles() . "\n"; // Forces immediate collection
// Get GC statistics (PHP 7.3+)
$gcInfo = gc_status();
print_r($gcInfo);
/*
Output might show:
Array (
[runs] => 5
[collected] => 128
[threshold] => 10000
[buffer_size] => 42
[roots] => 42
)
*/
// Disabling garbage collector (rarely recommended)
gc_disable(); // Cycle collection stops
// Memory from cyclic references will NOT be reclaimed
gc_enable(); // Re-enable cycle collection
Practical Memory Monitoring Tools
PHP provides built-in functions for real-time memory introspection. These are indispensable for profiling code, identifying memory leaks, and validating optimizations.
// Core memory inspection functions
$currentUsage = memory_get_usage();
// Returns current memory usage in bytes (excluding overhead)
$currentUsageReal = memory_get_usage(true);
// Returns memory allocated from the system (including overhead)
$peakUsage = memory_get_peak_usage();
// Returns peak memory usage reached during script execution
$peakUsageReal = memory_get_peak_usage(true);
// Returns peak system-allocated memory
// Practical profiling helper
function profileMemory(string $label): void {
static $baseline = null;
if ($baseline === null) {
$baseline = memory_get_usage(true);
echo "=== Memory Profile Baseline ===\n";
}
$current = memory_get_usage(true) - $baseline;
$peak = memory_get_peak_usage(true) - $baseline;
echo sprintf("[%s] Current: %s | Peak: %s\n",
$label,
formatBytes($current),
formatBytes($peak)
);
}
function formatBytes(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
// Usage example
profileMemory('Start');
$dataset = array_fill(0, 50000, 'placeholder_data');
profileMemory('After array allocation');
unset($dataset);
profileMemory('After unset');
gc_collect_cycles();
profileMemory('After GC cycle');
The memory_limit Directive
The memory_limit ini setting defines the maximum amount of memory a single PHP script may consume. When exceeded, PHP throws a fatal error to prevent a runaway script from destabilizing the entire server. Understanding how to set and work within this limit is critical.
// Checking and working with memory limits
$limit = ini_get('memory_limit');
echo "Memory limit: $limit\n"; // e.g., "128M", "256M", "-1" (unlimited)
// Converting human-readable limit to bytes
function parseMemoryLimit(string $limit): int {
if ($limit === '-1') return PHP_INT_MAX;
$value = (int) $limit;
$unit = strtoupper(substr($limit, -1));
return match($unit) {
'G' => $value * 1024 * 1024 * 1024,
'M' => $value * 1024 * 1024,
'K' => $value * 1024,
default => (int) $limit,
};
}
$limitBytes = parseMemoryLimit($limit);
$currentUsage = memory_get_usage(true);
$remaining = $limitBytes - $currentUsage;
echo "Remaining: " . formatBytes($remaining) . "\n";
// Graceful handling of near-limit situations
if ($remaining < (5 * 1024 * 1024)) {
// Less than 5 MB remaining - take defensive action
trigger_error("Approaching memory limit, freeing resources", E_USER_WARNING);
// Flush caches, write partial results, etc.
}
// Setting limit dynamically (within constraints)
ini_set('memory_limit', '256M'); // Increase at runtime if permitted
Memory-Efficient Data Structures and Techniques
Generators: Streaming Data Without Preloading
Generators are arguably the most powerful memory optimization tool in PHP. Instead of building a complete array in memory, a generator yields values one at a time, maintaining only the current iteration state. For processing large datasets, this can reduce memory consumption from gigabytes to kilobytes.
// Memory-heavy approach: load everything into an array
function getAllLogLines(string $filepath): array {
$lines = file($filepath); // Entire file loaded into array
$results = [];
foreach ($lines as $line) {
if (str_contains($line, 'ERROR')) {
$results[] = trim($line);
}
}
return $results; // Another array of filtered results
}
// For a 500 MB log file, this could consume 1+ GB of memory
// Generator approach: yield lines one at a time
function streamErrorLogs(string $filepath): \Generator {
$handle = fopen($filepath, 'r');
while (($line = fgets($handle)) !== false) {
if (str_contains($line, 'ERROR')) {
yield trim($line); // Only current line in memory
}
}
fclose($handle);
}
// Usage - memory footprint remains tiny regardless of file size
$errorCount = 0;
foreach (streamErrorLogs('/var/log/massive_app.log') as $errorLine) {
$errorCount++;
if ($errorCount > 1000) break; // Can stop early without penalty
}
echo "Found $errorCount errors\n";
// Custom generator for database result streaming
function streamLargeQuery(\PDO $pdo, string $sql): \Generator {
// Use unbuffered query mode
$pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $pdo->prepare($sql);
$stmt->execute();
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
yield $row; // One row at a time
}
}
// Generator that produces infinite sequence with constant memory
function fibonacciGenerator(): \Generator {
$a = 0; $b = 1;
while (true) {
yield $a;
$next = $a + $b;
$a = $b;
$b = $next;
}
// Only three integers stored at any time
}
$fib = fibonacciGenerator();
$firstTen = [];
for ($i = 0; $i < 10; $i++) {
$firstTen[] = $fib->current();
$fib->next();
}
print_r($firstTen); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
SplFixedArray: Compact Numeric Arrays
Standard PHP arrays are hash maps with substantial per-element overhead (approximately 72 bytes per element for the hash table bucket structure). When you need a dense, numerically-indexed collection and know the size in advance, SplFixedArray allocates a contiguous block of zval pointers, dramatically reducing memory overhead.
// Comparing memory usage: standard array vs SplFixedArray
$size = 100000;
// Standard array
$standardArray = [];
$startMemory = memory_get_usage(true);
for ($i = 0; $i < $size; $i++) {
$standardArray[$i] = $i;
}
$standardMemory = memory_get_usage(true) - $startMemory;
// SplFixedArray
$startMemory = memory_get_usage(true);
$fixedArray = new \SplFixedArray($size);
for ($i = 0; $i < $size; $i++) {
$fixedArray[$i] = $i;
}
$fixedMemory = memory_get_usage(true) - $startMemory;
echo "Standard array: " . formatBytes($standardMemory) . "\n";
echo "SplFixedArray: " . formatBytes($fixedMemory) . "\n";
echo "Memory saved: " . formatBytes($standardMemory - $fixedMemory) . "\n";
// Typical result: SplFixedArray uses ~40-50% less memory
// Additional SplFixedArray operations
$fixedArray->setSize(200000); // Resize (reallocates if growing)
$fixedArray->count(); // Get current size
$fixedArray->toArray(); // Convert back to standard array when needed
// Caution: SplFixedArray does not support associative keys
// Only integer indices 0..size-1 are valid
Weak References (PHP 7.4+)
Weak references allow you to hold a reference to an object without preventing the garbage collector from reclaiming it. This is invaluable for caching and observer patterns where you want to track objects but not force them to stay alive.
// WeakReference usage for cache implementations
class CacheManager {
private array $cache = [];
public function cache(string $key, object $data): void {
// Store a weak reference - won't prevent GC
$this->cache[$key] = \WeakReference::create($data);
}
public function get(string $key): ?object {
if (!isset($this->cache[$key])) {
return null;
}
$ref = $this->cache[$key];
$object = $ref->get();
if ($object === null) {
// Object was garbage collected - clean up stale entry
unset($this->cache[$key]);
return null;
}
return $object;
}
}
// Demonstration
$manager = new CacheManager();
$heavyObject = new \stdClass();
$heavyObject->data = str_repeat('x', 1000000); // ~1 MB of data
$manager->cache('user_data', $heavyObject);
echo "Cached: " . ($manager->get('user_data') !== null ? 'yes' : 'no') . "\n";
unset($heavyObject); // Strong reference removed
gc_collect_cycles(); // Object may be collected
// Weak reference doesn't prevent collection
echo "After unset: " . ($manager->get('user_data') !== null ? 'yes' : 'no') . "\n";
// Output: 'no' - object was reclaimed despite cache holding a reference
Common Memory Pitfalls and How to Avoid Them
Unbounded Result Sets from Databases
Fetching large query results into memory all at once is a frequent source of memory exhaustion. Always prefer streaming or chunked retrieval for datasets of unknown or large size.
// Dangerous: fetching everything at once
$stmt = $pdo->query("SELECT * FROM massive_audit_log");
$allRows = $stmt->fetchAll(\PDO::FETCH_ASSOC); // 10 million rows = disaster
foreach ($allRows as $row) {
processRow($row);
}
// Safe: streaming with unbuffered queries
$pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $pdo->query("SELECT * FROM massive_audit_log");
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
processRow($row);
}
// Memory usage stays constant regardless of result set size
// Alternative: chunked processing with LIMIT/OFFSET
function processInChunks(\PDO $pdo, int $chunkSize = 1000): void {
$offset = 0;
while (true) {
$stmt = $pdo->prepare(
"SELECT * FROM massive_audit_log LIMIT ? OFFSET ?"
);
$stmt->execute([$chunkSize, $offset]);
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
if (empty($rows)) break;
foreach ($rows as $row) {
processRow($row);
}
// Free result set memory before next chunk
$stmt->closeCursor();
$offset += $chunkSize;
}
}
Accumulating Temporary Variables in Loops
Building large temporary structures inside loops without freeing them can cause memory to balloon. Always unset temporary variables or restructure logic to avoid unnecessary accumulation.
// Problematic: accumulating intermediate results
$summaries = [];
for ($i = 0; $i < 100000; $i++) {
$rawData = fetchLargeDataChunk($i); // 50 KB each call
$processed = heavyTransform($rawData); // Another 50 KB
$summaries[] = extractSummary($processed);
// $rawData and $processed persist until loop iteration ends
// but within the same scope, memory climbs
}
// Peak memory: all temporary chunks simultaneously alive
// Improved: explicit cleanup and scoping
$summaries = [];
for ($i = 0; $i < 100000; $i++) {
$summary = null;
{
$rawData = fetchLargeDataChunk($i);
$processed = heavyTransform($rawData);
$summary = extractSummary($processed);
// $rawData and $processed eligible for GC after block
}
$summaries[] = $summary;
}
// Better: use a dedicated function to scope temporaries
function fetchSummary(int $index): array {
$rawData = fetchLargeDataChunk($index);
$processed = heavyTransform($rawData);
return extractSummary($processed);
// All temporaries freed when function returns
}
$summaries = [];
for ($i = 0; $i < 100000; $i++) {
$summaries[] = fetchSummary($i);
}
Global and Static Variable Accumulation
Global variables and static properties persist for the entire request lifecycle. In long-running scripts, they can silently accumulate data until memory is exhausted.
// Dangerous global accumulation in long-running process
$globalLog = []; // Global scope
function logEvent(string $message): void {
global $globalLog;
$globalLog[] = [
'timestamp' => microtime(true),
'message' => $message,
'trace' => debug_backtrace() // Expensive data per entry
];
// This array grows unbounded - eventual OOM
}
// Solution: ring buffer with fixed capacity
class FixedRingBuffer {
private array $buffer = [];
private int $capacity;
private int $position = 0;
public function __construct(int $capacity = 1000) {
$this->capacity = $capacity;
}
public function push(array $entry): void {
$this->buffer[$this->position % $this->capacity] = $entry;
$this->position++;
}
public function getAll(): array {
return array_slice(
$this->buffer,
max(0, $this->position - $this->capacity),
$this->capacity
);
}
}
$logBuffer = new FixedRingBuffer(500);
function logEventSafe(string $message): void {
global $logBuffer;
$logBuffer->push([
'timestamp' => microtime(true),
'message' => $message
]);
// Memory usage bounded to 500 entries maximum
}
Image and File Processing Without Cleanup
Working with image resources in GD or file handles requires explicit destruction. These resources allocate memory outside PHP's internal memory tracking, making proper cleanup essential.
// Resource leak example
function createThumbnails(array $imagePaths): array {
$thumbnails = [];
foreach ($imagePaths as $path) {
$sourceImage = imagecreatefromjpeg($path); // Allocates image resource
$thumb = imagescale($sourceImage, 150, 150);
$thumbnails[] = $thumb;
// BUG: $sourceImage never freed with imagedestroy()
// Each iteration leaks the full source image resource
}
return $thumbnails;
}
// After processing 100 images, gigabytes may be leaked
// Correct resource management
function createThumbnailsFixed(array $imagePaths): array {
$thumbnails = [];
foreach ($imagePaths as $path) {
$sourceImage = null;
$thumb = null;
try {
$sourceImage = imagecreatefromjpeg($path);
$thumb = imagescale($sourceImage, 150, 150);
$thumbnails[] = $thumb;
} finally {
// Always free source image, even on errors
if ($sourceImage !== null && is_resource($sourceImage)) {
imagedestroy($sourceImage);
}
}
}
return $thumbnails;
}
// For file handles, use try-finally or the destructor pattern
function processLargeFile(string $path): void {
$handle = fopen($path, 'r');
try {
while (($line = fgets($handle)) !== false) {
// Process line
}
} finally {
if (is_resource($handle)) {
fclose($handle);
}
}
}
Advanced Memory Optimization Strategies
String Interning and Deduplication
When handling many duplicate string values—common in data imports with repeated categories or status codes—string deduplication can yield substantial memory savings by ensuring each unique string value is stored only once.
// String deduplication helper
class StringInterner {
private array $pool = [];
public function intern(string $value): string {
$hash = md5($value);
if (isset($this->pool[$hash]) && $this->pool[$hash] === $value) {
return $this->pool[$hash]; // Return existing instance
}
$this->pool[$hash] = $value;
return $value;
}
public function stats(): array {
return [
'unique_strings' => count($this->pool),
'total_memory' => array_sum(array_map('strlen', $this->pool)),
];
}
}
// Usage in data import scenario
$interner = new StringInterner();
$imported = [];
$rawData = fetchCsvData('massive_export.csv'); // 500,000 rows
foreach ($rawData as $row) {
$imported[] = [
'status' => $interner->intern($row['status']), // e.g., "active" repeated
'category' => $interner->intern($row['category']), // e.g., "electronics"
'region' => $interner->intern($row['region']), // e.g., "US-NE"
];
}
// Without interning: 500,000 separate copies of "active"
// With interning: 1 shared copy referenced 300,000 times
echo "Unique strings stored: " . $interner->stats()['unique_strings'] . "\n";
Lazy Loading and Proxy Objects
Deferring expensive data loading until actual access reduces peak memory by avoiding preloading of data that may never be used during a request.
// Lazy loading proxy pattern
class LazyLoadedCollection implements \IteratorAggregate {
private ?array $items = null;
private \PDO $pdo;
private string $query;
private array $params;
public function __construct(\PDO $pdo, string $query, array $params = []) {
$this->pdo = $pdo;
$this->query = $query;
$this->params = $params;
}
private function load(): void {
if ($this->items === null) {
$stmt = $this->pdo->prepare($this->query);
$stmt->execute($this->params);
$this->items = $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
}
public function getIterator(): \Traversable {
$this->load();
return new \ArrayIterator($this->items ?? []);
}
public function count(): int {
$this->load();
return count($this->items ?? []);
}
}
// The collection doesn't load data until actually accessed
$products = new LazyLoadedCollection(
$pdo,
"SELECT * FROM products WHERE category = ?",
['electronics']
);
// Memory usage at this point: minimal (just the object skeleton)
// Only loads when iterated
foreach ($products as $product) {
echo $product['name'] . "\n";
}
// After iteration, consider freeing:
unset($products);
Object Pooling for Frequent Allocations
For workloads that repeatedly create and destroy similar objects (e.g., database connection wrappers, request context objects), object pooling recycles instances instead of allocating new ones, reducing garbage collection pressure and allocation overhead.
// Simple object pool implementation
class ObjectPool {
private array $available = [];
private array $inUse = [];
private \Closure $factory;
private int $maxSize;
public function __construct(callable $factory, int $maxSize = 50) {
$this->factory = \Closure::fromCallable($factory);
$this->maxSize = $maxSize;
}
public function acquire(): object {
if (!empty($this->available)) {
$object = array_pop($this->available);
} elseif (count($this->inUse) < $this->maxSize) {
$object = ($this->factory)();
} else {
throw new \RuntimeException("Pool exhausted (max: {$this->maxSize})");
}
$hash = spl_object_id($object);
$this->inUse[$hash] = $object;
return $object;
}
public function release(object $object): void {
$hash = spl_object_id($object);
if (isset($this->inUse[$hash])) {
unset($this->inUse[$hash]);
$this->available[] = $object;
}
}
public function stats(): array {
return [
'available' => count($this->available),
'in_use' => count($this->inUse),
'total' => count($this->available) + count($this->inUse),
];
}
}
// Usage with database connection wrappers
class DbConnection {
public function __construct() {
// Expensive initialization (TLS setup, auth, etc.)
echo "New connection created\n";
}
public function query(string $sql): array {
// Execute query
return [];
}
public function reset(): void {
// Clean state for reuse
}
}
$pool = new ObjectPool(fn() => new DbConnection(), 20);
// Acquire, use, and release
$conn = $pool->acquire();
$conn->query("SELECT ...");
$conn->reset();
$pool->release($conn); // Back in pool for next acquisition
// Later, same object reused without allocation overhead
$conn2 = $pool->acquire(); // Gets recycled instance
echo "Pool stats: " . json_encode($pool->stats()) . "\n";
Profiling Memory in Development and Production
Using Xdebug for Detailed Memory Profiling
Xdebug provides function-level memory tracking that reveals exactly which function calls consume memory. Enable profiling mode to generate cachegrind-compatible files for visualization.
// Xdebug configuration in php.ini for memory profiling
/*
xdebug.mode = profile
xdebug.start_with_request = trigger
xdebug.output_dir = /tmp/profiles
xdebug.profiler_output_name = cachegrind.out.%p
*/
// Trigger profiling via GET parameter or cookie
// Request: https://example.com/api/endpoint?XDEBUG_TRIGGER=profile
// Analyzing the output with kcachegrind or qcachegrind
// Shows:
// - Inclusive memory cost per function
// - Self memory cost (memory allocated directly in function)
// - Call graph showing memory flow through application
// Programmatic memory tracking without Xdebug
class MemoryProfiler {
private static array $snapshots = [];
public static function snapshot(string $label): void {
self::$snapshots[] = [
'label' => $label,
'usage' => memory_get_usage(true),
'peak' => memory_get_peak_usage(true),
'time' => microtime(true),
'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3),
];
}
public static function report(): void {
echo "\n=== Memory Profile Report ===\n";
$previous = null;
foreach (self::$snapshots as $snap) {
$delta = $previous
? formatBytes($snap['usage'] - $previous['usage'])
: 'baseline';
echo sprintf(
"[%s] %s (delta: %s, peak: %s)\n",
$snap['label'],
formatBytes($snap['usage']),
$delta,
formatBytes($snap['peak'])
);
$previous = $snap;
}
}
}
// Usage in critical code paths
MemoryProfiler::snapshot('Before data load');
$data = loadLargeDataset();
MemoryProfiler::sn