Skip to content

gc 函数族

概述

PHP 提供了一组 gc_* 函数用于控制垃圾回收器的行为,包括启用/禁用 GC、手动触发收集、获取 GC 状态等。这些函数对于调试内存问题、优化长运行脚本以及理解 GC 行为非常有用。

函数列表

gc_enable(), gc_disable(), gc_collect_cycles(), gc_enabled(), gc_status()

基础概念

gc_enable / gc_disable

启用或禁用循环引用收集器。禁用 GC 可以在特定场景下提升性能,但可能导致内存泄漏。

gc_collect_cycles

手动触发一轮循环引用收集。

gc_status

返回 GC 的当前状态信息,包括运行的轮次、收集的根数量等。

语法与代码

gc_enable / gc_disable

php
<?php
declare(strict_types=1);

// 检查 GC 是否启用
echo gc_enabled() ? 'GC 已启用' : 'GC 已禁用'; // GC 已启用

// 禁用 GC
gc_disable();
echo gc_enabled() ? 'GC 已启用' : 'GC 已禁用'; // GC 已禁用

// 重新启用 GC
gc_enable();
echo gc_enabled() ? 'GC 已启用' : 'GC 已禁用'; // GC 已启用

gc_collect_cycles

php
<?php
declare(strict_types=1);

// 手动触发循环引用收集
// 返回回收的循环引用数量
$count = gc_collect_cycles();
echo "回收了 {$count} 个循环引用\n";

gc_status

php
<?php
declare(strict_types=1);

$status = gc_status();
print_r($status);
// Array (
//     [runs] => 1
//     [collected] => 0
//     [threshold] => 10000
//     [roots] => 0
// )

echo "GC 运行次数: {$status['runs']}\n";
echo "已收集的根: {$status['collected']}\n";
echo "触发阈值: {$status['threshold']}\n";
echo "当前根数: {$status['roots']}\n";

GC 配置参数

php
<?php
declare(strict_types=1);

// 获取 GC 相关配置
echo "gc_enabled: " . ini_get('gc_enabled') . "\n";
echo "zend.enable_gc: " . ini_get('zend.enable_gc') . "\n";

// 设置 root buffer 大小
// ini_set('zend.gc_max_roots', 20000); // PHP 8.3+

详细说明

gc_status 返回值

字段说明
runsGC 运行的总轮数
collected累计收集的循环引用数
thresholdroot buffer 触发阈值
rootsroot buffer 当前条目数

PHP 8.3+ 的 gc_status 增强

PHP 8.3 增加了更多 GC 状态信息。

实战示例

实战:批处理中的 GC 管理

php
<?php
declare(strict_types=1);

class BatchProcessor
{
    private int $gcCounter = 0;
    private int $gcInterval;

    public function __construct(int $gcInterval = 1000)
    {
        $this->gcInterval = $gcInterval;
    }

    public function process(iterable $items): void
    {
        foreach ($items as $item) {
            $this->processItem($item);

            $this->gcCounter++;
            if ($this->gcCounter >= $this->gcInterval) {
                gc_collect_cycles();
                $this->gcCounter = 0;
            }
        }
    }

    private function processItem(mixed $item): void
    {
        // 处理逻辑...
    }
}

实战:性能敏感场景的 GC 优化

php
<?php
declare(strict_types=1);

function performanceCriticalTask(): void
{
    // 临时禁用 GC
    $wasEnabled = gc_enabled();
    gc_disable();

    try {
        // 执行性能敏感的操作...
        for ($i = 0; $i < 100000; $i++) {
            $obj = new stdClass();
            // ...
        }
    } finally {
        // 恢复 GC 状态
        if ($wasEnabled) {
            gc_enable();
        }
        gc_collect_cycles(); // 清理可能积累的循环引用
    }
}

注意事项

gc_enabled() 在不同环境下的默认值

php
<?php
declare(strict_types=1);

// Web 环境(php.ini)默认启用
// CLI 环境同样默认启用
echo "GC 启用: " . (gc_enabled() ? "yes" : "no") . "\n";

// 通过 php.ini 禁用
// zend.enable_gc = Off

gc_collect_cycles 的返回值

php
<?php
declare(strict_types=1);

// 返回值是本轮回收的循环引用数量
$count = gc_collect_cycles();
echo "回收了 {$count} 个循环引用\n";

// 如果没有循环引用需要回收,返回 0
$count2 = gc_collect_cycles();
echo "再次调用回收: {$count2}\n"; // 0

gc_status 在 PHP 8.3+ 中的增强

PHP 8.3 增加了更多状态字段,提供更详细的 GC 运行信息。

php
<?php
declare(strict_types=1);

if (PHP_VERSION_ID >= 80300) {
    $status = gc_status();
    // PHP 8.3+ 可能包含额外字段
    foreach ($status as $key => $value) {
        echo "{$key}: {$value}\n";
    }
}

实战:守护进程中的 GC 策略

php
<?php
declare(strict_types=1);

class GcManager
{
    private int $cycleCount = 0;
    private int $collectInterval;

    public function __construct(int $collectInterval = 100)
    {
        $this->collectInterval = $collectInterval;
    }

    public function tick(): void
    {
        $this->cycleCount++;

        if ($this->cycleCount >= $this->collectInterval) {
            $this->cycleCount = 0;
            $this->collect();
        }
    }

    public function collect(): int
    {
        $before = memory_get_usage();
        $collected = gc_collect_cycles();
        $after = memory_get_usage();

        if ($collected > 0) {
            echo "GC: 回收 {$collected} 个循环, "
               . "释放 " . ($before - $after) . " bytes\n";
        }

        return $collected;
    }

    public function getStatus(): array
    {
        return gc_status();
    }
}

// 在守护进程中使用
$gc = new GcManager(50);

for ($i = 0; $i < 1000; $i++) {
    // 处理任务...
    $gc->tick();
}

禁用 GC 的风险

禁用 GC 会导致循环引用无法被自动回收,可能导致内存泄漏。务必在完成后重新启用。

gc_collect_cycles 的调用频率

不要过于频繁地调用 gc_collect_cycles()。每次调用都有一定的性能开销。

最佳实践

  1. 大多数情况保持 GC 启用:默认行为适用于大多数场景。
  2. 长运行脚本定期触发 GC:如守护进程、队列消费者。
  3. 性能敏感代码临时禁用:处理后立即恢复。

常见问题

Q: gc_collect_cycles 会影响性能吗?

每次调用 gc_collect_cycles() 都会执行完整的循环引用检测算法,包括标记和清除两个阶段。在 root buffer 中有大量疑似根时,这个过程可能需要几毫秒。因此在性能敏感的循环中不要频繁调用。

Q: 如何判断是否需要手动触发 GC?

php
<?php
declare(strict_types=1);

function shouldCollectGarbage(): bool
{
    $status = gc_status();
    // 当 root buffer 超过阈值的 50% 时触发
    return $status['roots'] > $status['threshold'] * 0.5;
}

if (shouldCollectGarbage()) {
    gc_collect_cycles();
}

Q: GC 与 Composer autoload 的关系

Composer 的自动加载器在 autoload_files 阶段会加载类映射。已加载的类在请求结束前不会被卸载。GC 主要影响的是运行时创建的对象的循环引用,与 Composer autoload 无直接关系。

Q: CLI 和 Web 环境下 GC 行为相同吗?

GC 的核心行为相同,但在请求-响应模型中(Web),PHP 会在请求结束时自动释放所有内存,因此循环引用的影响被自动解决。在 CLI 长运行脚本中,循环引用必须手动或通过 gc_collect_cycles() 处理。

补充代码示例

监控 GC 活动的装饰器

php
<?php
declare(strict_types=1);

class GcMonitor
{
    private int $gcBefore = 0;
    private int $collected = 0;
    private float $timeSpent = 0.0;

    public function track(callable $operation): mixed
    {
        $this->gcBefore = gc_status()['collected'];
        $start = microtime(true);

        $result = $operation();

        $this->timeSpent = microtime(true) - $start;
        $this->collected = gc_status()['collected'] - $this->gcBefore;

        return $result;
    }

    public function report(): void
    {
        echo "GC 收集: {$this->collected} 个循环引用\n";
        echo "耗时: " . ($this->timeSpent * 1000) . "ms\n";
    }
}

$monitor = new GcMonitor();
$monitor->track(function (): void {
    for ($i = 0; $i < 500; $i++) {
        $a = new stdClass();
        $b = new stdClass();
        $a->ref = $b;
        $b->ref = $a;
        unset($a, $b);
    }
    gc_collect_cycles();
});
$monitor->report();

gc 函数族详解

gc_status() 完整输出

php
<?php
declare(strict_types=1);

// 获取当前 GC 状态
$status = gc_status();

echo "GC 状态信息:\n";
echo "  已启用: " . ($status['enabled'] ? 'yes' : 'no') . "\n";
echo "  正在运行: " . ($status['running'] ? 'yes' : 'no') . "\n";
echo "  已保护: " . ($status['protected'] ? 'yes' : 'no') . "\n";
echo "  root 缓冲区已满: " . ($status['full'] ? 'yes' : 'no') . "\n";
echo "  root 缓冲区大小: " . $status['buffer_size'] . "\n";

print_r($status);

gc_collect_cycles 的返回值

php
<?php
declare(strict_types=1);

// gc_collect_cycles 返回回收的循环引用数量
$count = gc_collect_cycles();
echo "回收了 {$count} 个循环引用\n";

// 批量回收模式
$totalCollected = 0;
$maxAttempts = 5;

for ($i = 0; $i < $maxAttempts; $i++) {
    $collected = gc_collect_cycles();
    $totalCollected += $collected;

    if ($collected === 0) {
        echo "第 " . ($i + 1) . " 轮: 无可回收对象,停止\n";
        break;
    }

    echo "第 " . ($i + 1) . " 轮: 回收了 {$collected} 个对象\n";
}

echo "总共回收: {$totalCollected}\n";

GC 在不同场景中的配置

php
<?php
declare(strict_types=1);

// CLI 脚本中的 GC 配置
// 短生命周期脚本,GC 开销可能大于收益
if (php_sapi_name() === 'cli') {
    gc_disable();  // 禁用自动 GC
    register_shutdown_function(function (): void {
        gc_enable();
        gc_collect_cycles();  // 脚本结束时统一回收
    });
}

// 长期运行进程中的 GC 配置
// 如队列消费者、守护进程
if (php_sapi_name() === 'cli' && isLongRunningProcess()) {
    // 增大 root buffer 触发阈值
    ini_set('gc_threshold', 10000);

    // 定期手动触发 GC
    register_tick_function(function (): void {
        static $counter = 0;
        if (++$counter % 1000 === 0) {
            $collected = gc_collect_cycles();
            if ($collected > 0) {
                echo "[" . date('Y-m-d H:i:s') . "] GC 回收: {$collected}\n";
            }
        }
    });
}

GC 与内存监控

php
<?php
declare(strict_types=1);

function logMemoryUsage(string $label = ''): void
{
    $memUsage = memory_get_usage(true);
    $memPeak = memory_get_peak_usage(true);
    $gcStatus = gc_status();

    echo "[{$label}] 内存使用: " . formatBytes($memUsage);
    echo " / 峰值: " . formatBytes($memPeak);
    echo " / GC roots: " . $gcStatus['buffer_size'] . "\n";
}

function formatBytes(int $bytes): string
{
    return match (true) {
        $bytes >= 1073741824 => round($bytes / 1073741824, 2) . ' GB',
        $bytes >= 1048576 => round($bytes / 1048576, 2) . ' MB',
        $bytes >= 1024 => round($bytes / 1024, 2) . ' KB',
        default => $bytes . ' B',
    };
}

logMemoryUsage('开始');

// 创建一些循环引用
$objects = [];
for ($i = 0; $i < 10000; $i++) {
    $a = new \stdClass();
    $b = new \stdClass();
    $a->ref = $b;
    $b->ref = $a;
    $objects[] = $a;
}

logMemoryUsage('创建循环引用后');

// 清除引用
$objects = null;

logMemoryUsage('清除引用后');

$collected = gc_collect_cycles();
logMemoryUsage("手动GC后 (回收: {$collected})");

常见误区与 FAQ

gc_disable 会让内存泄露吗?

不会。gc_disable 只是关闭循环引用收集器的自动运行,普通引用计数仍然有效。只有循环引用无法自动回收,需要手动调用 gc_collect_cycles()

gc_collect_cycles 什么时候应该手动调用?

在以下场景中手动调用 GC 更合适:

  1. CLI 脚本中处理大量数据后
  2. 长期运行进程的空闲时段
  3. 批量任务完成后
  4. 单元测试中的 setUp/tearDown

如何判断是否需要手动 GC?

如果 memory_get_usage() 持续增长且 gc_collect_cycles() 返回值大于 0,说明有循环引用需要手动回收。

参考链接