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
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
declare(strict_types=1);
// 手动触发循环引用收集
// 返回回收的循环引用数量
$count = gc_collect_cycles();
echo "回收了 {$count} 个循环引用\n";gc_status
<?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
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 返回值
| 字段 | 说明 |
|---|---|
runs | GC 运行的总轮数 |
collected | 累计收集的循环引用数 |
threshold | root buffer 触发阈值 |
roots | root buffer 当前条目数 |
PHP 8.3+ 的 gc_status 增强
PHP 8.3 增加了更多 GC 状态信息。
实战示例
实战:批处理中的 GC 管理
<?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
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
declare(strict_types=1);
// Web 环境(php.ini)默认启用
// CLI 环境同样默认启用
echo "GC 启用: " . (gc_enabled() ? "yes" : "no") . "\n";
// 通过 php.ini 禁用
// zend.enable_gc = Offgc_collect_cycles 的返回值
<?php
declare(strict_types=1);
// 返回值是本轮回收的循环引用数量
$count = gc_collect_cycles();
echo "回收了 {$count} 个循环引用\n";
// 如果没有循环引用需要回收,返回 0
$count2 = gc_collect_cycles();
echo "再次调用回收: {$count2}\n"; // 0gc_status 在 PHP 8.3+ 中的增强
PHP 8.3 增加了更多状态字段,提供更详细的 GC 运行信息。
<?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
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()。每次调用都有一定的性能开销。
最佳实践
- 大多数情况保持 GC 启用:默认行为适用于大多数场景。
- 长运行脚本定期触发 GC:如守护进程、队列消费者。
- 性能敏感代码临时禁用:处理后立即恢复。
常见问题
Q: gc_collect_cycles 会影响性能吗?
每次调用 gc_collect_cycles() 都会执行完整的循环引用检测算法,包括标记和清除两个阶段。在 root buffer 中有大量疑似根时,这个过程可能需要几毫秒。因此在性能敏感的循环中不要频繁调用。
Q: 如何判断是否需要手动触发 GC?
<?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
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
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
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
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
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 更合适:
- CLI 脚本中处理大量数据后
- 长期运行进程的空闲时段
- 批量任务完成后
- 单元测试中的 setUp/tearDown
如何判断是否需要手动 GC?
如果 memory_get_usage() 持续增长且 gc_collect_cycles() 返回值大于 0,说明有循环引用需要手动回收。