PHP,是世界上使用最广泛的Web编程语言之一。从1995年诞生到现在,PHP已经经历了二十多年的发展。
2015年12月,PHP 7.0正式发布,带来了性能的巨大提升——相比PHP 5.6,性能提升了将近一倍,内存消耗也大幅降低。PHP 7的发布,是PHP发展史上的一个重要里程碑。
2017年11月,PHP 7.2正式发布,带来了更多的新特性和性能优化。而PHP 7.3,也在紧锣密鼓地开发中,预计2018年底发布。
我做PHP开发已经有很多年了,从PHP 5.3用到PHP 7.2,见证了PHP的不断进化。但是,很长一段时间里,我对PHP的底层机制了解得不够深入,只知道怎么用,不知道为什么这么用。后来,我花了一些时间,研究了PHP的源码和底层原理,对PHP有了更深的理解。
今天,我想分享一下我对PHP 7.2/7.3底层机制的理解,包括Zend引擎的执行流程、内存管理、垃圾回收、Opcode编译、以及PHP 7.2/7.3的新特性和性能优化。希望能帮助你深入理解PHP的底层原理,写出更高性能的PHP代码。
一、PHP的执行流程
在深入底层机制之前,先搞清楚一个基本问题:一段PHP代码,是怎么被执行的?
PHP是一门解释型语言,但是它不是直接解释执行源代码,而是先把源代码编译成Opcode(中间字节码),然后由Zend虚拟机执行Opcode。
PHP的执行流程,大致分为以下几个阶段:
1. 词法分析(Lexing)
首先,PHP的源代码会被词法分析器(基于re2c生成)扫描,把源代码转换成一个个的Token(标记)。比如,echo "hello";会被转换成TECHO、TCONSTANTENCAPSEDSTRING、';'等Token。
2. 语法分析(Parsing)
然后,语法分析器(基于Bison生成)会把Token流,按照PHP的语法规则,转换成抽象语法树(AST,Abstract Syntax Tree)。AST是源代码的树形表示,每个节点代表一个语法结构。
PHP 7.0之前,语法分析的结果直接生成Opcode,没有AST这一步。PHP 7.0引入了AST,让语法分析和Opcode生成分离,代码更清晰,也更容易维护和扩展。
3. 编译(Compilation)
接下来,编译器会遍历AST,把每个节点编译成对应的Opcode。Opcode是Zend虚拟机可以执行的中间指令,类似于Java的字节码。
比如,echo "hello";会被编译成ECHO opcode。$a = 1 + 2;会被编译成ADD、ASSIGN等opcode。
4. 执行(Execution)
最后,Zend虚拟机会执行编译好的Opcode。Zend虚拟机是一个基于栈的虚拟机,执行Opcode的时候,会在一个执行栈上进行操作。
为了提高性能,PHP会把编译好的Opcode缓存起来(比如用OPcache扩展),这样下次执行同一个脚本的时候,就不需要重新编译了,直接执行缓存的Opcode,性能会好很多。
二、Zend虚拟机和Opcode
Zend虚拟机,是PHP的核心,负责执行Opcode。
Zend虚拟机是一个基于栈的虚拟机。执行Opcode的时候,会用到两个重要的栈:
- 执行栈(Execution Stack):用于存放操作数和临时变量。
- 调用栈(Call Stack):用于函数调用,存放函数的调用帧(Stack Frame)。
每个Opcode,对应Zend虚拟机中的一个处理函数。执行的时候,Zend虚拟机会取出下一个Opcode,调用对应的处理函数,然后继续执行下一个Opcode,直到程序结束。
PHP的Opcode有很多种,大概有100多个,涵盖了变量赋值、算术运算、逻辑运算、函数调用、控制流等各种操作。
你可以用vld扩展,来查看一段PHP代码编译后的Opcode。比如:
<?php
$a = 1;
$b = 2;
$c = $a + $b;
echo $c;用vld查看,编译后的Opcode大概是这样的:
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > ASSIGN !0, 1
4 1 ASSIGN !1, 2
5 2 ADD ~2 !0, !1
3 ASSIGN !2, ~2
6 4 ECHO !2
5 > RETURN 1通过查看Opcode,你可以更深入地理解PHP代码的执行过程,也能发现一些性能问题。比如,有些写法看起来简洁,但是编译后的Opcode更多,执行更慢。
三、PHP的变量实现:zval
在PHP中,变量是用zval(Zend value)结构来表示的。zval是PHP中最基础的数据结构,所有的变量、常量、函数参数、返回值,都是用zval来表示的。
zval的结构,大致是这样的(简化版):
struct _zval_struct {
zend_value value; // 值
union {
struct {
zend_uchar type; // 类型
zend_uchar type_flags;
zend_uchar const_flags;
zend_uchar reserved;
} v;
uint32_t type_info;
} u1;
union {
uint32_t var_flags;
uint32_t next; // 哈希表冲突链
uint32_t cache_slot; // 运行时缓存
uint32_t lineno;
uint32_t num_args;
uint32_t fe_pos;
uint32_t fe_iter_idx;
} u2;
};zval的核心是value和type。value是一个联合体,可以存放不同类型的值(整数、浮点数、字符串、数组、对象等),type表示这个zval的类型。
PHP 7对zval做了重要的优化。在PHP 5中,zval是单独在堆上分配的,而且总是通过指针引用,这导致了很多内存分配和引用计数的开销。PHP 7中,zval可以直接嵌入到其他结构中(比如哈希表的bucket),不需要单独分配内存,而且很多简单类型(整数、浮点数)不需要引用计数,直接拷贝就行。这大大提高了性能,减少了内存消耗。
PHP的变量类型,包括:
- IS_NULL:空值
- ISFALSE / ISTRUE:布尔值
- IS_LONG:整数
- IS_DOUBLE:浮点数
- IS_STRING:字符串
- IS_ARRAY:数组
- IS_OBJECT:对象
- IS_RESOURCE:资源
- IS_REFERENCE:引用
对于简单类型(NULL、布尔、整数、浮点数),zval直接存放值,拷贝的时候直接拷贝值,不需要引用计数。对于复杂类型(字符串、数组、对象、资源),zval存放的是一个指针,指向实际的数据结构,拷贝的时候默认是引用拷贝(共享同一份数据,引用计数加1),修改的时候才会复制(写时复制,Copy on Write)。
四、内存管理
PHP的内存管理,是分层的。最底层是系统的内存分配(malloc/free),上面是Zend的内存管理器(Zend Memory Manager,简称ZendMM),再上面是PHP的各种缓存和分配器。
Zend内存管理器(ZendMM)
ZendMM是PHP的内存管理核心,它在系统malloc的基础上,做了一层封装,提供了更高效的内存分配和释放。
ZendMM的特点:
- 内存池:ZendMM会预先向系统申请一大块内存,然后在这块内存上进行分配,减少系统调用的次数。
- 生命周期管理:PHP的内存,大部分是请求级别的。也就是说,一次请求结束后,所有在这次请求中分配的内存,都会被一次性释放。ZendMM会跟踪所有分配的内存,请求结束时统一释放,不需要手动free,也不会有内存泄漏。
- 内存对齐:ZendMM分配的内存是对齐的,提高访问效率。
- 调试支持:ZendMM支持内存泄漏检测、越界检测等调试功能。
在PHP中,你一般不需要手动管理内存,ZendMM会帮你处理。但是,了解ZendMM的原理,能帮助你写出更高效的代码,避免一些内存问题。
写时复制(Copy on Write)
PHP的复杂类型(字符串、数组、对象),默认使用写时复制(COW)机制。也就是说,当你把一个变量赋值给另一个变量的时候,不会立即复制数据,而是两个变量共享同一份数据,引用计数加1。只有当其中一个变量被修改的时候,才会真正复制一份数据出来。
比如:
$a = [1, 2, 3]; // 数组的引用计数是1
$b = $a; // 数组的引用计数变成2,$a和$b共享同一份数据
$b[] = 4; // $b被修改,这时候才复制一份数据出来,$a的引用计数变回1,$b指向新的副本COW机制,能大大减少内存的使用,因为很多时候,变量只是被读取,不会被修改,共享数据就够了。只有在修改的时候,才需要复制。
五、垃圾回收(GC)
PHP的垃圾回收,主要是用来处理循环引用的问题。
在PHP 5.3之前,PHP的引用计数机制,无法处理循环引用。比如:
$a = [];
$a[] = &$a; // $a引用了自己,形成循环引用
unset($a); // 虽然unset了,但是因为循环引用,引用计数不会变成0,内存无法释放这种循环引用,会导致内存泄漏。PHP 5.3引入了新的垃圾回收机制(GC),专门用来处理循环引用。
PHP的GC算法,是基于引用计数的,但是增加了一个"根缓冲区"(root buffer)和周期性的回收算法。
GC的工作原理:
- 根缓冲区:当一个zval的引用计数增加,并且可能形成循环引用的时候,会被放入根缓冲区。
- 标记阶段:当根缓冲区满了(默认10000个),或者手动触发gccollectcycles()的时候,GC会启动。GC会遍历根缓冲区中的每个zval,模拟删除它,然后看引用计数是否变成0。如果变成0,说明这个zval是循环引用的一部分,可以被回收。
- 清除阶段:标记完成后,GC会把所有可以回收的循环引用,真正删除,释放内存。
PHP的GC,默认是开启的。你可以通过php.ini中的zend.enablegc来开启或关闭,也可以在代码中用gcenable()和gc_disable()来控制。
需要注意的是,GC只处理循环引用,普通的引用计数还是由Zend引擎自己处理。而且,GC的回收是有开销的,所以PHP不会每次都回收,而是等根缓冲区满了才回收。
在PHP 7中,GC的性能有了很大的提升。PHP 7.2还对GC做了进一步的优化,减少了GC的开销。
六、PHP 7.2的新特性
PHP 7.2在2017年11月正式发布,带来了很多新特性和改进。下面介绍几个重要的:
1. 对象类型(object type)
PHP 7.2新增了object类型,可以用来声明参数和返回值的类型为对象。
function foo(object $obj): object {
return $obj;
}在PHP 7.2之前,如果你想限制参数必须是对象,只能用某个具体的类名,或者用stdClass。现在有了object类型,可以更通用地表示任何对象。
2. 抽象方法重写(Abstract Method Override)
PHP 7.2允许抽象类继承另一个抽象类的时候,重写父类的抽象方法。
abstract class A {
abstract function foo(string $s);
}
abstract class B extends A {
abstract function foo($s); // 可以去掉类型声明,或者放宽限制
}在PHP 7.2之前,这样写会报错。现在允许了,只要子类的方法签名比父类更宽松就行。
3. 参数类型扩展(Parameter Type Widening)
PHP 7.2允许子类在重写父类方法的时候,去掉参数的类型声明(也就是放宽参数类型)。
class A {
public function foo(string $s) {}
}
class B extends A {
public function foo($s) {} // 可以去掉string类型声明
}这符合里氏替换原则(LSP),子类的参数类型可以比父类更宽松。
4. 密码哈希算法扩展
PHP 7.2新增了对Argon2密码哈希算法的支持。Argon2是2015年密码哈希竞赛的获胜者,比bcrypt更安全。
password_hash("password", PASSWORD_ARGON2I);需要注意的是,Argon2需要PHP在编译的时候加上--with-password-argon2选项,并且系统需要安装libargon2库。
5. 废弃的特性
PHP 7.2废弃了一些旧的特性:
- __autoload()方法被废弃,推荐用splautoloadregister()。
- create_function()被废弃,推荐用匿名函数。
- parse_str()不带第二个参数的用法被废弃。
- assert()的字符串参数被废弃。
这些废弃的特性,在PHP 7.2中会发出E_DEPRECATED警告,在未来的版本中会被移除。
七、PHP 7.3的展望
PHP 7.3目前还在开发中,预计2018年底发布。从目前的开发进度来看,PHP 7.3会带来以下一些新特性和改进:
1. 灵活的Heredoc和Nowdoc语法
PHP 7.3改进了Heredoc和Nowdoc的语法,允许结束标记有缩进,并且结束标记后面不需要换行。
// PHP 7.3之前,结束标记必须在行首,后面必须换行
$str = <<<CODE
hello
world
CODE;
// PHP 7.3,结束标记可以有缩进
$str = <<<CODE
hello
world
CODE;这个改进,让Heredoc/Nowdoc在缩进的代码中更美观。
2. JSONTHROWON_ERROR
PHP 7.3新增了JSONTHROWONERROR选项,让jsonencode和jsondecode在出错的时候抛出异常,而不是返回false然后用jsonlast_error()检查。
try {
json_decode("invalid json", true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo $e->getMessage();
}这个改进,让JSON错误处理更现代化,也更符合PHP 7的异常处理风格。
3. list()中的引用赋值
PHP 7.3允许在list()(或者[]解构)中使用引用赋值。
$arr = [1, 2];
list($a, &$b) = $arr; // PHP 7.3允许
$b = 3;
// $arr[1] 现在是34. is_countable()函数
PHP 7.3新增了is_countable()函数,用来检查一个变量是否是可数的(数组或者实现了Countable接口的对象)。
if (is_countable($var)) {
count($var);
}在PHP 7.2中,count()一个不可数的变量会发出警告。有了is_countable(),可以更方便地检查。
5. 性能改进
PHP 7.3还会带来一些性能改进,包括:
- 优化了字符串处理函数。
- 改进了GC的性能。
- 优化了Opcode的编译和执行。
- 减少了内存分配的开销。
虽然PHP 7.3的性能提升可能不像PHP 7.0那么巨大,但是还是会有一定的提升。
八、性能优化建议
了解了PHP的底层机制之后,我们来看看一些性能优化的建议:
1. 使用OPcache
这是最重要、最简单的性能优化。OPcache会把PHP编译后的Opcode缓存起来,避免每次请求都重新编译。开启OPcache,通常能带来2-3倍的性能提升。
在php.ini中配置:
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=22. 避免不必要的数组复制
PHP的数组使用写时复制,但是如果你在循环中不断修改数组,可能会触发多次复制。尽量在循环外初始化数组,避免在循环中频繁修改大数组。
3. 使用引用传递大数组
如果你需要在函数中处理大数组,并且不需要保留原数组,可以用引用传递,避免复制。
function process(array &$arr) {
// 处理$arr
}但是要注意,引用传递会改变原数组,使用的时候要小心。
4. 预定义字符串长度
在拼接大量字符串的时候,预先分配好长度,可以减少内存重新分配的次数。
5. 避免在循环中做重复的计算
把循环中不变的计算提到循环外面。比如:
// 不好的写法
for ($i = 0; $i < count($arr); $i++) {
// ...
}
// 好的写法
$count = count($arr);
for ($i = 0; $i < $count; $i++) {
// ...
}6. 使用PHP 7的标量类型声明和返回值类型声明
类型声明不仅能让代码更安全,还能帮助PHP引擎做一些优化。
7. 尽量用PHP 7.2或更高版本
每个PHP版本的发布,都会带来性能改进。尽量使用最新的稳定版本,能获得更好的性能和安全性。
九、写在最后
PHP 7的发布,是PHP发展史上的一次重大变革。它不仅带来了性能的巨大提升,还引入了很多现代语言特性,让PHP变得更严谨、更高效、更现代化。
PHP 7.2和即将到来的PHP 7.3,在PHP 7.0的基础上,继续改进和完善,带来了更多的新特性和性能优化。作为PHP开发者,我们应该跟上PHP的发展步伐,了解新版本的特性,写出更高质量、更高性能的PHP代码。
深入理解PHP的底层机制,包括Zend引擎、Opcode、zval、内存管理、垃圾回收等,能帮助我们更好地使用PHP,写出更高效的代码,也能在遇到问题的时候,更快地定位和解决。
当然,PHP的底层机制很复杂,一篇文章不可能讲得很全面。这篇文章只是一个入门,如果你想深入学习,建议阅读PHP的源码,或者参考一些深入的书籍和文章。
最后,用一句话来结束这篇文章:"知其然,更要知其所以然。了解PHP的底层机制,能让你从一个只会用PHP的开发者,变成一个真正懂PHP的专家。"
希望这篇文章,能帮助你深入理解PHP 7.2/7.3的底层机制。如果你有不同的观点或者补充,欢迎在评论区留言,我们一起交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录