伴随着PHP官方对OPcache的不断优化,使得OPcache已成为提成PHP效率的重要工具。咱们这边文章就详细说说它的实现原理。
代码执行流程
传统执行流程(无 OPcache)
PHP 源代码 (source code)
↓
词法分析 (Lexical Analysis)
↓
语法分析 (Syntax Analysis / Parsing)
↓
生成 AST (Abstract Syntax Tree)
↓
编译为操作码 (Opcode)
↓
执行操作码 (Execution)
↓
输出结果
详细说明:
词法分析(Lexical Analysis)
- 将源代码字符串分解为标记(tokens)
- 例如:$a = 1 + 2; → [$a, =, 1, +, 2, ;
语法分析(Parsing)
- 根据 PHP 语法规则构建抽象语法树(AST)
- 检查语法错误
编译为操作码(Opcode Compilation)
- 将 AST 转换为操作码(字节码)
- 操作码是 PHP 虚拟机的指令
执行操作码(Execution)
- Zend 虚拟机执行操作码
- 产生最终结果
问题:每次请求都要重复执行步骤 1-3,造成性能损耗。
使用 OPcache 后的执行流程
第一次请求:
PHP 源代码
↓
词法分析 + 语法分析 + 编译
↓
生成操作码
↓
【缓存到共享内存】
↓
执行操作码
↓
输出结果
后续请求:
PHP 源代码
↓
【从共享内存读取操作码】← 跳过编译步骤
↓
执行操作码
↓
输出结果
优势:后续请求直接使用缓存的操作码,跳过编译步骤,大幅提升性能。
核心概念
操作码(Opcode)
操作码是 PHP 虚拟机(Zend VM)的指令,类似于 Java 的字节码。
// PHP 源代码
$a = 1 + 2;
echo $a;
// 对应的操作码-简化
ASSIGN $a, 1
ADD $a, 2
ECHO $a
// 实际 Zend 操作码(更复杂)
ZEND_ASSIGN
ZEND_ADD
ZEND_ECHO
共享内存(Shared Memory)
OPcache 使用共享内存存储编译后的操作码,多个 PHP 进程可以共享同一份缓存。
内存类型:
* System V 共享内存(Linux)
* mmap 内存映射(跨平台)
* Windows 共享内存(Windows)
优势:
* 多个 PHP-FPM 进程共享同一份缓存
* 减少内存占用
* 提高缓存命中率
缓存键(Cache Key)
每个 PHP 文件对应一个缓存键,基于文件路径生成。
键生成规则:
// 伪代码
$cache_key = hash('sha256', realpath($file_path));
共享内存中的哈希表:
[
'file_path_hash' => {
'opcodes': [...], // 编译后的操作码
'timestamp': 1234567, // 文件修改时间
'checksum': 'abc123', // 文件校验和
'memory_size': 1024, // 占用内存大小
},
...
]
OPcache 工作流程
初始化阶段-PHP 启动
1. 加载 OPcache 扩展
2. 分配共享内存(根据 opcache.memory_consumption)
3. 初始化哈希表结构
4. 加载预加载脚本(如果配置了 opcache.preload)
请求处理流程
用户请求
↓
PHP-FPM 进程接收请求
↓
include/require PHP 文件
↓
OPcache 拦截文件加载
↓
计算文件路径的哈希键
↓
在共享内存中查找缓存
↓
┌─────────────────┬─────────────────┐
│ 缓存命中 │ 缓存未命中 │
│ (Hit) │ (Miss) │
└─────────────────┴─────────────────┘
↓ ↓
读取操作码 编译 PHP 文件
↓ ↓
验证时间戳 生成操作码
(如果启用) ↓
↓ 存储到共享内存
执行操作码 ↓
↓ 执行操作码
返回结果 ↓
返回结果
内存回收机制
情况 1:文件更新
1. 检测到文件修改时间变化
2. 标记旧缓存条目为无效
3. 释放旧操作码占用的内存
4. 新请求时重新编译并缓存
情况 2:缓存过期
1. 检查 max_life_time
2. 清理过期的缓存条目
3. 回收内存空间
情况 3:内存不足
1. 检查可用内存
2. 清理最少使用的缓存(LRU)
3. 或返回缓存已满错误
字符串驻留(Interned Strings)
原理:
- PHP 中相同的字符串字面量只存储一份
- 多个变量引用同一个字符串对象
- 减少内存占用
// 源代码
$a = "hello";
$b = "hello";
$c = "hello";
// 无字符串驻留:3 个字符串对象,占用 3 份内存
// 有字符串驻留:1 个字符串对象,3 个引用,占用 1 份内存
// OPcache 中的实现
// 伪代码
interned_strings_pool = {
"hello" => string_object_1,
"world" => string_object_2,
...
};
// 所有相同的字符串共享同一个对象
配置:
opcache.interned_strings_buffer=16 ; 字符串缓存池大小(MB)
预加载(Preloading)
原理:
- PHP 启动时(而非请求时)加载和编译文件
- 将操作码直接加载到共享内存
- 避免首次请求的编译开销
PHP 启动
↓
加载 opcache.preload 脚本
↓
执行 preload.php
↓
调用 opcache_compile_file()
↓
编译文件并存储到共享内存
↓
PHP 就绪,等待请求
OPcache 与 JIT 的协作
PHP 源代码
↓
OPcache 编译为操作码
↓
【操作码缓存到共享内存】
↓
请求时从缓存读取操作码
↓
JIT 编译器(如果启用)
↓
编译为机器码
↓
执行机器码
关系:
* OPcache:缓存操作码,避免重复编译 PHP 代码
* JIT:将操作码编译为机器码,提升执行速度
* 协作:OPcache 为 JIT 提供稳定的操作码输入
配置示例:
; OPcache 配置
opcache.enable=1
opcache.memory_consumption=256
; JIT 配置(PHP 8.0+)
opcache.jit_buffer_size=256M
opcache.jit=tracing