享元模式通过分离内在状态(可共享、不可变)与外在状态(由客户端传入、不可共享),在PHP中减少海量小对象的内存重复占用;需用静态池管理、禁止对象内保存外部状态、确保构造轻量无副作用。

memory_limit 不是救命稻草,海量小对象溢出的根因往往不是“不够用”,而是“重复存”。
享元模式(Flyweight)在 PHP 中确实能缓解这类问题,但它不是开箱即用的银弹——PHP 的对象模型、引用机制和 GC 行为决定了你必须手动控制共享粒度和生命周期。
为什么直接 new 大量小对象会爆内存?
每个 new 出的对象都携带 Zend 对象头(约 56 字节)、属性哈希表、引用计数器等开销。哪怕对象只有 2–3 个属性,10 万个实例轻松吃掉 10+ MB;若属性值本身是字符串或数组,还会触发深层复制。
常见诱因包括:
- 循环中反复
new User($id, $name, $role)构建临时 DTO - 解析 CSV/JSON 后批量生成对象,未区分「内在状态」和「外在状态」
- 缓存层未复用已有实例,每次调用都新建
享元模式在 PHP 中怎么落地?
核心是把对象拆成两部分:
-
内在状态(Intrinsic State):可共享、不可变,如
$type、$icon、$defaultConfig -
外在状态(Extrinsic State):不可共享、上下文相关,如
$userId、$position、$timestamp
享元类本身不保存外在状态,而是由客户端传入。PHP 实现时注意:
立即学习“PHP免费学习笔记(深入)”;
- 用静态数组或
SplObjectStorage管理共享池,键必须是能唯一标识内在状态的组合(如md5(serialize($config))) - 禁止在享元对象内修改任何属性——否则破坏共享安全性
- 避免用
__clone,享元本就不该被克隆;若需差异化行为,应通过策略对象注入
示例(轻量享元工厂):
class IconFlyweight
{
private static array $pool = [];
private function __construct(
public readonly string $name,
public readonly string $svgPath,
public readonly int $size
) {}
public static function get(string $name, string $svgPath, int $size): self
{
$key = md5("{$name}:{$svgPath}:{$size}");
return self::$pool[$key] ??= new self($name, $svgPath, $size);
}
}
// 使用时只传外在状态
foreach ($items as $item) {
$icon = IconFlyweight::get($item['type'], $item['theme'], $item['size']);
echo $icon->render($item['x'], $item['y']); // 外在坐标由调用方传入
}
比享元更关键的是:别让对象活过必要周期
享元解决的是「重复创建」,但 PHP 内存压力更多来自「不该活的对象还活着」:
- 循环中用
unset($obj)或赋值为null主动切断引用,尤其在长循环里 - 数据库结果集别用
fetchAll()全部载入,改用PDO::FETCH_CLASS+ 生成器逐行构建享元 - 避免在闭包中隐式捕获大对象(
use ($bigArray)),改用 ID 或 key 延迟查 - 检查是否误将享元实例存进全局数组或静态属性——这会让它们永远无法被 GC 回收
验证共享是否真正生效?
光看逻辑不够,得用数据确认:
- 在享元工厂的
get()方法里加计数器:self::$created++和self::$reused++,对比两者比例 - 用
memory_get_usage(true)在循环前后打点,观察单次迭代内存增量是否趋近于 0(说明复用成功) - 运行
xdebug_info()查看是否启用了 GC,再用gc_collect_cycles()强制回收测试泄漏点
最常被忽略的一点:享元对象的构造函数里不能有 I/O、查询或复杂计算——它必须是纯的、快的、无副作用的。否则共享反而变成性能瓶颈。



















