PHP & Python 内存管理机制详解

深入理解两大语言的内存分配、引用计数、垃圾回收与性能优化策略

PHP 8.x
Python 3.x

内存管理概述

核心问题:程序运行需要内存,但物理内存有限且需要共享。操作系统通过虚拟内存机制,让每个进程拥有独立的地址空间。语言运行时在此基础上实现自己的内存管理策略。

🎯 核心目标

  • ▸ 自动化内存分配与释放
  • ▸ 防止内存泄漏和野指针
  • ▸ 优化内存使用效率
  • ▸ 提供统一的内存抽象

🔧 关键组件

  • ▸ 内存分配器 (Allocator)
  • ▸ 垃圾回收器 (Garbage Collector)
  • ▸ 引用计数 (Reference Counting)
  • ▸ 内存池 (Memory Pool)

性能考量

  • ▸ 分配/释放速度
  • ▸ 内存碎片化控制
  • ▸ GC 暂停时间
  • ▸ 缓存局部性

内存架构对比

PHP (Zend Engine)

应用层 (Application)

  • PHP 代码 / 用户变量
  • 函数调用栈
  • 类实例化

Zend VM (Zend Engine)

  • opcode 指令执行
  • 符号表管理
  • 作用域管理

Zend MM (内存管理器)

  • emalloc/efree 封装
  • 请求级内存池
  • 引用计数管理

系统层

  • glibc / jemalloc
  • 操作系统 malloc
  • 虚拟内存系统

Python (CPython)

应用层 (Application)

  • Python 代码 / 用户对象
  • 调用栈 (Frame Stack)
  • 模块与命名空间

Python VM (字节码解释器)

  • opcode 指令执行
  • PyObject 管理
  • 类型系统

PyMem (内存管理器)

  • PyMem_Malloc/Free
  • 对象分配器 (PyObject)
  • 引用计数管理

系统层

  • PyMalloc ( arenas )
  • 虚拟内存页
  • 底层内存分配

💎 PHP 特色:请求级内存池

PHP 每个请求结束时会释放所有内存,不会跨请求保留。这是 PHP 内存管理的最大特色,避免了内存泄漏但也意味着无法缓存大量数据。

请求开始
请求处理
响应生成
内存全部释放

引用计数机制

什么是引用计数?每个对象维护一个计数器,记录有多少个引用指向它。当计数归零时,对象立即被销毁释放内存。这是 PHP 和 Python 共同的内存管理基础。

引用计数工作流程

创建对象

String: "Hello" 1
1 $a

赋值引用

赋值给另一个变量

String: "Hello" 2
1 $a
2 $b

unset/unset

释放引用

已销毁
ref = 0

🐘 PHP 引用计数

  • refcount 字段存储在 zval 结构体中
  • ▸ 每次赋值 $b = $a 时 refcount++
  • ▸ 每次 unset() 时 refcount--
  • ▸ refcount=0 时立即调用 destructor
  • ▸ 循环引用无法自动回收(需 GC 模块)

🐍 Python 引用计数

  • ob_refcnt 在 PyObject 头部
  • b = a 时 refcount++
  • ▸ 变量销毁时 refcount--
  • ▸ refcount=0 时立即释放
  • ▸ 线程安全(有 GIL 保护)

写时复制 (Copy-on-Write)

核心思想:复制变量时不立即复制数据,而是共享同一份内存。只有当某个变量尝试修改数据时,才真正复制一份数据。这就是 PHP 的 COW 机制。

阶段1: 初始赋值

$a
"Hello World"

refcount = 1
is_ref = 0

阶段2: $b = $a

$a
$b
"Hello World"

refcount = 2
is_ref = 0

阶段3: $b = "New Value"

$a
$b
"Hello World"
"New Value"

$a refcount = 1
$b 新建独立副本

PHP 8.0+ 的变化:PHP 8 引入了更激进的内存优化,但在某些场景下 COW 语义也发生了变化。引用(&)创建时 is_ref=1,与 COW 互斥。

⚠️ is_ref 与 COW 的关系

场景 is_ref refcount 行为
$a = "hello"; $b = $a; 0 2 COW 共享内存
$a = "hello"; $b = &$a; 1 2 引用,修改互相影响
$a = "hello"; $b = $a; $b[0] = "H"; COW 分离 各1 $b 触发复制

垃圾回收机制

问题:引用计数无法处理循环引用(如双向链表、父对象引用子对象)。当两个对象互相引用但外部无引用时,refcount 永远不为零,造成内存泄漏。

🐘 PHP 垃圾回收器

PHP 使用"引用计数 + 同步垃圾回收"策略。当可能存在循环引用时,触发 GC 算法。

PHP GC 流程

发现可疑对象
(refcount 减少但非零)
标记根节点
疑似垃圾?
深度遍历
引用计数-1
计数=0?
真正的垃圾
释放内存
清理循环中的每个对象
gc_collect_cycles() 会暂停所有请求处理,大数组或复杂对象图会导致明显停顿。生产环境中应避免依赖 GC。

🐍 Python 垃圾回收器

Python 使用"引用计数 + 分代式 GC"策略。引用计数负责绝大多数对象的回收,分代 GC 处理循环引用。

分代回收策略

年轻代 (Gen 0)

新建对象
回收率最高

频繁回收
大部分对象早亡

中年代 (Gen 1)

经历一次回收
中等频率

过渡区
垃圾比例降低

老年代 (Gen 2)

长期存活对象
很少回收

静态变量、类定义
优先保留

Python GC 触发条件

新建对象
超过阈值?
Gen 0 回收
(标记-清除)
晋升条件?
→ Gen 1
多次存活 → Gen 2

🔄 循环引用示例

PHP

class Node { public $next; } $a = new Node(); $b = new Node(); $a->next = $b; // $a refcount = 1 $b->next = $a; // $b refcount = 1 // 外部无引用,但 $a↔$b 互相引用 // refcount 永远不为 0,需要 GC

Python

class Node: def __init__(self): self.next = None a = Node() b = Node() a.next = b # a refcount = 1 b.next = a # b refcount = 1 # 循环引用,refcount 不归零 # 分代 GC 最终会发现并回收

内存池机制 (Python)

PyMalloc 三层架构

第一层:Block(8字节对齐)

最小的内存单元,大小为 8, 16, 24, 32... 256 字节。请求大小向上取整到最近的 block 大小。

8B 16B 24B 32B ... 256B

第二层:Pool(4KB 页)

大小相同的 block 组成 pool。一个 pool 通常是 4KB(一个内存页),管理同一大小的所有 block。

1
2
3
4
5
6
7
8

已用 空闲

第三层:Arena(256KB)

多个 pool 组成 arena。 arenas 从系统申请 256KB 的大块内存,内部包含多个 pools。

Pool 1
Pool 2
Pool 3
...
超过 256 字节:PyMalloc 处理小对象分配,大于 256 字节的请求直接使用系统 malloc,以避免内存浪费。

核心对比

特性 PHP Python
主要策略 引用计数 + 同步 GC 引用计数 + 分代 GC
内存释放时机 refcount=0 立即释放;GC 周期清理循环 refcount=0 立即释放;分代 GC 清理循环
循环引用处理 需显式触发 GC 自动分代回收
内存池 请求级内存池(请求结束全部释放) PyMalloc(长期运行优化)
写时复制 支持(Zend MM) 不支持(直接赋值)
GC 暂停 仅在 gc_collect_cycles() 时 分代回收时可能 STW
线程安全 取决于扩展(pthread) 有 GIL 保护
适用场景 Web 请求/响应模型 长期运行服务
内存泄漏风险 低(请求结束释放) 较高(需主动清理)
Python 优势
PHP 特色
需注意

交互式演示

模拟 PHP 引用计数变化

// 点击上方按钮开始演示

变量表

暂无变量 -

内存对象

暂无对象

GC 状态

GC 启用:
待回收对象: 0

术语表

zval (Zend Value)
PHP 中表示值的结构体,包含类型、值、引用计数等信息
PyObject
Python 中所有对象的基类,包含引用计数和类型指针
Copy-on-Write (COW)
延迟复制策略,多个引用共享同一份数据,修改时才复制
Mark and Sweep
标记-清除算法,GC 通过标记可达对象来识别垃圾
Generational GC
分代垃圾回收,基于"大多数对象早亡"的假设优化
Arena
Python 内存池中的 256KB 内存块,由系统分配
Pool
同一大小的 blocks 组成的 4KB 内存池
GIL (Global Interpreter Lock)
Python 的全局解释器锁,保证引用计数的线程安全
Memory Leak
内存泄漏,分配的内存在不再使用时未能释放
Cycle Reference
循环引用,两个或多个对象互相引用形成环