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的特点:

  1. 内存池:ZendMM会预先向系统申请一大块内存,然后在这块内存上进行分配,减少系统调用的次数。
  2. 生命周期管理:PHP的内存,大部分是请求级别的。也就是说,一次请求结束后,所有在这次请求中分配的内存,都会被一次性释放。ZendMM会跟踪所有分配的内存,请求结束时统一释放,不需要手动free,也不会有内存泄漏。
  3. 内存对齐:ZendMM分配的内存是对齐的,提高访问效率。
  4. 调试支持: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的工作原理:

  1. 根缓冲区:当一个zval的引用计数增加,并且可能形成循环引用的时候,会被放入根缓冲区。
  2. 标记阶段:当根缓冲区满了(默认10000个),或者手动触发gccollectcycles()的时候,GC会启动。GC会遍历根缓冲区中的每个zval,模拟删除它,然后看引用计数是否变成0。如果变成0,说明这个zval是循环引用的一部分,可以被回收。
  3. 清除阶段:标记完成后,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] 现在是3

4. 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=2

2. 避免不必要的数组复制

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的底层机制。如果你有不同的观点或者补充,欢迎在评论区留言,我们一起交流。