【PHP7源码分析】初探PHP字符串类型中的引用计数

作者:王澍

背景介绍

  • 字符串类型也是我们平时常用的类型,由于字符串的特性,为了节省内存通常相同字符串变量会共用一块内存空间,通过引用计数来标记有多变量使用这块内存。
  • 但是,经过GDB追踪发现,并不是所有字符串都是正常在操作引用计数,有正常累加的,有时候为0,又有时候为1。为了一探究竟,于是简单分析了一下各种赋值情况。

环境情况

  • 系统版本:Ubuntu 16.04.3 LTS
  • PHP版本:PHP 7.1.0
  • gdb版本:GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1

一、基础变量

PHP中zval是所有变量的基础。(zend_type.h 121行)

clipboard.png

其中zend_value存储了具体数据,结构如图: (zend_type.h 101行)

clipboard.png

  • zend_value为一个联合体,整体占用8字节。
  • u1为一个联合体,存储了类型所需要的必要数据,占用4字节。
  • u2位一个联合体,存储了一些额外数据,比如hash碰撞时的next,占用4字节。

整个zval结构体,占用16字节,就支持了php所有类型。

clipboard.png

PHP7中用如此简单而巧妙的zval存储了所有类型数据,那么一个不确定长度的字符串又如何能存储在一个16字节的zval中呢?

二、字符串变量

<?php
$a = "hello world";
echo $a;

通过GDB调试可以看到:

clipboard.png

type = 6,对照类型的定义,可以看到类型是IS_STRING (zend_type.h 303行)

clipboard.png

由于我们的字符串长度不一定,所以单靠zval的16个字节是无法直接存储的,于是通过value中的str指向真正存储字符串的内存地址。通过打印我们可以看到,这个地址的类型是zend_string

clipboard.png

1、zend_string结构体

先看一下它的数据结构,如图 (zend_type.h 169行)

clipboard.png

zend_string结构体中的gc
头部先是gc,可以看一下其他复杂类型,头部都有一个gc,它的作用是什么?
看看gc的数据结构,如图:

clipboard.png

  • 第一个是refcount,记录了被引用的次数。
  • 第二个u是一个联合体,可以看到与zval的u1很像,关键是记录了type。

那么作用就比较好猜测了,在程序执行gc或其他操作的时候,对于任意一个复杂类型,指针头部就是gc,里面不光有引用计数,并且能通过u.v.type确定该复杂类型的真正类型。

zend_string结构体中的h
从名字可以猜测,这是字符串的hash,空间换时间的思想,把计算好的hash保存下来,提高性能。

zend_string中的len
比较明显,它存储了字符串的长度。

zend_string中的val[1]
这种写法是c里面的柔性数组,这里存储了整个字符串,通过这个方式保证字符串所在的内存地址是与该结构体内存地址紧密相连的,减少了去另外一个块内存取值的时间。
(ps:留个小疑问,gdb就可以追踪到。柔性数组是否占内存空间?zend_string的对齐后是什么结构?整体占多大?)

2、zend_string实际内容

了解过结构本身,可以打印一下内容来看看,如图

clipboard.png

该地址内存储的确实是hello world,为什么gc中的refcount是0呢?

原因是这样的:

  • 常量字符串,在PHP代码中的固定字符串,在编译阶段存储在全局变量表中,又叫做字面量表,请求结束后才会被销毁,所以refcount一直为0。
  • 临时字符串,发生在虚拟机执行opcode时计算出来的字符串,存储在临时变量区,有正常的refcount。

修改一下代码,看一下临时字符串

<?php
$a = "hello world".time();
echo $a;

打印一下这个变量的zval,refcount为1,如图

clipboard.png

三、字符串的引用计数

1.临时字符串直接赋值

对于临时字符串,应该是每有一个被赋值的变量时,该zend_string中的引用计数+1,并且在引用计数为0时,释放这块内存。
<?php

$a = "hello world".time();
echo $a;
$b = $a;
echo $b;
当为$a赋值完成时,$a,在栈上第一个位置,类型为6,IS_STRING,取value中的str,地址为:**0x7ffff4402c30**,可以看到内容,zend_string引用计数为1。

clipboard.png

当为$b赋值完成时,$b在栈上第二个位置,类型为6,IS_STRING,取value中的str同样地址为:**0x7ffff4402c30**,zend_string引用计数为2。

clipboard.png

大致引用情况可以画出:

clipboard.png

2.引用赋值

对于变量直接赋值,上面已经画出了引用关系,那么如果是引用类型呢?
<?php

$a = "hello world".time();
echo $a;
$b = &$a;
echo $b;
当为$a赋值完成时,$a,在栈上第一个位置,类型为6,IS_STRING,取value中的str,地址为:**0x7ffff4402c30**,可以看到内容,zend_string引用计数为1。

clipboard.png

当$b赋值为引用类型时,$b在栈上第二个位置,类型为10 , IS_REFERENCE,取value中的ref可以看到内容。

clipboard.png

这时$a的类型是否发生改变?是否还是字符串类型?直接打印$a看一下。这时$a的类型变成了10,IS_REFERENCE,打印value中的ref,地址与$b的ref相同!

clipboard.png

在$b引用$a的时候,$a与$b都变成引用类型,该引用类型指向了中有一个zval,类型为6,IS_STRING,value中的str指向了一个zend_string,并且zend_string引用计数为1.

clipboard.png

大致引用情况如图:

clipboard.png

四、字符串变量特殊值

<?php
$a = “string”;
$b = “double”;
 
echo $a;
echo $b;

以我们上面的结论,$a与$b都属于常量字符串。

打印$a的zend_string,如图

clipboard.png

打印$b的zend_string,如图

clipboard.png

可见$b符合预期,但是$a颠覆了以上的理论。
那问题出在了哪?

经过GDB的追踪,可以看到a和b都在栈上,并且都是string类型。
但是,其中value中的str地址有很大不一样。
首先看变量a
在栈的第一个位置,str的值是 0x11522c0

clipboard.png

其次看变量b
在栈上第二个位置,str的值是 0x7ffff4401880

clipboard.png

了解PHP的内存分配的话,可以看出b的字符串,分配在了 0x7ffff440000 这个chunk上,属于第一个page页,0x7ffff4401000

而a的字符串很显然不是这个规则,他没有分配在chunk上,而是很特殊的一个地址。
所以string这个字符串,不是_emalloc分配的。

那么采用个笨办法,我把 0x11522c0强转 (zend_string *)0x11522c0 ,然后看里的值什么时候放进去的。

PHP版本 7.1.0
第一个节点: php_cli.c中的 1345行

sapi_module->startup(sapi_module)

第二个节点: php_cli.c 中的424 行

php_module_startup(sapi_module, NULL, 0)

第三个节点: main.c 中的 2123行

zend_startup(&zuf, NULL);

第四个节点: zend.c 中的768行

zend_interned_strings_init();

很接近了哦
第五个节点: zend_string.c中的103

zend_intern_known_strings(known_strings, (sizeof(known_strings)

在这里打印一下,know_strings,这里可以看到,file,line,function,class,object等等,以及string在这里就初始化了!

clipboard.png

对应声明的地址在 zend_string.h 383行

clipboard.png

这里还没有初始化字面量,于是这些字符串与字面量的情况有些不一样。

小结

同样是字符串在PHP中有很多种不同情况。

  • 1.代码中直接硬编码的字符串,在字面量表中,引用计数一直为0,直到整个脚本执行完毕后,才会销毁。
  • 2.在执行阶段计算出来的字符串,临时字符串,引用计数正常计算,每个引用都会加1。在引用计数为0时回收内存。
  • 3.引用类型的字符串,多个变量引用计数计算在引用类型(zend_reference)上。字符串被zend_reference引用,引用计数为1。
  • 4.特殊的字符串,在PHP初始化时创建的,整个脚本执行完毕后才会销毁,引用计数一直为1。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章