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

做者:王澍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

1、基础变量

PHP中zval是全部变量的基础。(zend_type.h 121行)ubuntu

clipboard.png

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

clipboard.png

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

整个zval结构体,占用16字节,就支持了php全部类型。数组

clipboard.png

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

2、字符串变量

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

经过GDB调试能够看到:性能

clipboard.png

type = 6,对照类型的定义,能够看到类型是IS_STRING (zend_type.h 303行)编码

clipboard.png

因为咱们的字符串长度不必定,因此单靠zval的16个字节是没法直接存储的,因而经过value中的str指向真正存储字符串的内存地址。经过打印咱们能够看到,这个地址的类型是zend_stringspa

clipboard.png

一、zend_string结构体

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

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的对齐后是什么结构?总体占多大?)

二、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

3、字符串的引用计数

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

4、字符串变量特殊值

<?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。
相关文章
相关标签/搜索