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
循环引用,两个或多个对象互相引用形成环