PHP字符串深入理解
无论在什么语言中字符串是常见的类型,在PHP中字符串定义更为简洁。列:$txt="Hello world!";
这段代码就可以表示一个字符串,你以为就这简单?大家想个问题 如果一个包含大量字符的一个字符串,同样赋值给一个变量所占用的空间一样吗?文字描述可能不方便理解,对程序员来说还是直接上代码:
1:var_dump(memory_get_usage()); //先打印出当前内存情况
2:$arr = array_fill (0, 100000, 'tioncico' );
3:var_dump(memory_get_usage());
4:$arr_copy = $arr ;
5:var_dump(memory_get_usage());
6:$arr_copy[1]='a';
7:var_dump(memory_get_usage());
以上代码你们觉得直接结果如何呢?大家可以复制执行实际操作下
分析下上面代码:
- 步骤一只是打印当前占用内存使用情况.这里假设当前内存为:
100
- 步骤二使用
array_fill
填充一个占有10万
个KEY的数组 - 步骤三打印当前内存使用情况。假设内存增加到
150
- 步骤四做一个赋值操作,相信这里都可以看懂.`
- 重点来了步骤五在此打印发现内存并没有增加,内存使用情况还是和步骤三相同。内存还是
150
.看到这里是不是感觉很奇怪了? - 步骤六对新的变量改变其中一个key的值
- 步骤七这个是否发现内存才是真正的增加。假设内存增加到
200
正主来了
其实对于上面的步骤,并不是感觉很奇怪 这是因为php采取的一个叫写实复制
的方案,仔细看上面代码可以发现执行到步骤四$arr_copy
和$arr
这两个变量的值其实是相同,对于这种相同的值不同的变量进行分配的时候是没必要在进行额外空间的占用.来看下定义:
写时复制(Copy on Write,也缩写为COW)的应用场景非常多, 比如Linux中对进程复制中内存使用的优化,在各种编程语言中,如C++的STL等等中均有类似的应用。 COW是常用的优化手段,可以归类于:资源延迟分配。只有在真正需要使用资源时才占用资源, 写时复制通常能减少资源的占用
想信看到这里还是很迷,没事继续往下看,下面使用php源码进行解析:
简单了解下zend_string结构体
//string zval结构
struct _zend_string {
zend_refcounted_h gc; 计数器
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
解析zstring结构:
- zend_refcounted_h 这个字段也就是内存管理使用到的,如果有别的变量进行相同的引用就会加一,下文通过gdb分析就可以看到.
- zend_ulong 对 zend_string 进行哈希处理时会用到
- size_t 字符串长度
- char 存储字符串
GDB分析PHP代码:
$b='hell word';
$a=$b;
echo $a; 此时zendstr地址等于 0x7ffff1c6f410
echo $b; 此时zendstr地址也等于 0x7ffff1c6f410 引用变量在和被引用变量一致时zendstr指向的是一个
$a='hello word a';
echo $a; 此时zendstr地址也等于 0x7ffff1c6f438 引用字符串发生改变的时候才会真正的进行复制一份
对应GDB分析代码重点:
步骤一:
(gdb) p z
$2 = (zval *) 0x7ffff1c1e090
(gdb) p *z
$3 = {value = {lval = 140737249735696, dval = 6.9533440184587449e-310, counted = 0x7ffff1c6f410, str = 0x7ffff1c6f410, arr = 0x7ffff1c6f410, obj = 0x7ffff1c6f410, res = 0x7ffff1c6f410, ref = 0x7ffff1c6f410, ast = 0x7ffff1c6f410,
zv = 0x7ffff1c6f410, ptr = 0x7ffff1c6f410, ce = 0x7ffff1c6f410, func = 0x7ffff1c6f410, ww = {w1 = 4056347664, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {call_info = 0, extra = 0}}, type_info = 6}, u2 = {
next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
(gdb) p $3.value.str
$4 = (zend_string *) 0x7ffff1c6f410
(gdb) p *$3.value.str
$5 = {gc = {refcount = 1, u = {type_info = 70}}, h = 9473262949128236454, len = 9, val = "h"}
(gdb) p *$3.value.str.val@9
$6 = "hell word"
步骤二:
(gdb) c
Continuing.
hell word
Breakpoint 1, ZEND_ECHO_SPEC_CV_HANDLER () at /www/server/php/73/src/Zend/zend_vm_execute.h:36660
36660 /www/server/php/73/src/Zend/zend_vm_execute.h: 没有那个文件或目录.
(gdb) n
36661 in /www/server/php/73/src/Zend/zend_vm_execute.h
(gdb) n
411 /www/server/php/73/src/Zend/zend_types.h: 没有那个文件或目录.
(gdb) p *z
$7 = {value = {lval = 140737249735696, dval = 6.9533440184587449e-310, counted = 0x7ffff1c6f410, str = 0x7ffff1c6f410, arr = 0x7ffff1c6f410, obj = 0x7ffff1c6f410, res = 0x7ffff1c6f410, ref = 0x7ffff1c6f410, ast = 0x7ffff1c6f410,
zv = 0x7ffff1c6f410, ptr = 0x7ffff1c6f410, ce = 0x7ffff1c6f410, func = 0x7ffff1c6f410, ww = {w1 = 4056347664, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {call_info = 0, extra = 0}}, type_info = 6}, u2 = {
next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
(gdb) p *$7.value.str
$8 = {gc = {refcount = 1, u = {type_info = 70}}, h = 9473262949128236454, len = 9, val = "h"}
(gdb) p *$7.value.str.val.@9
A syntax error in expression, near `@9'.
(gdb) p *$7.value.str.val@9
$9 = "hell word"
步骤三:
(gdb) c
Continuing.
hell word
Breakpoint 1, ZEND_ECHO_SPEC_CV_HANDLER () at /www/server/php/73/src/Zend/zend_vm_execute.h:36660
36660 /www/server/php/73/src/Zend/zend_vm_execute.h: 没有那个文件或目录.
(gdb) n
36661 in /www/server/php/73/src/Zend/zend_vm_execute.h
(gdb) n
411 /www/server/php/73/src/Zend/zend_types.h: 没有那个文件或目录.
(gdb) p *z
$10 = {value = {lval = 140737249735736, dval = 6.9533440184607211e-310, counted = 0x7ffff1c6f438, str = 0x7ffff1c6f438, arr = 0x7ffff1c6f438, obj = 0x7ffff1c6f438, res = 0x7ffff1c6f438, ref = 0x7ffff1c6f438, ast = 0x7ffff1c6f438,
zv = 0x7ffff1c6f438, ptr = 0x7ffff1c6f438, ce = 0x7ffff1c6f438, func = 0x7ffff1c6f438, ww = {w1 = 4056347704, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {call_info = 0, extra = 0}}, type_info = 6}, u2 = {
next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
(gdb) p *$10.value.str
$11 = {gc = {refcount = 1, u = {type_info = 70}}, h = 15212097803322570358, len = 12, val = "h"}
(gdb) p *$10.value.str.val@12
$12 = "hello word a"
分析GDB代码,如果看不懂上面GDB打印没有关系,看下面讲解你会豁然开朗,下面也是只会讲解重点来说:
先看两个名词zval
结构:
{value = {lval = 140737249735736, dval = 6.9533440184607211e-310, counted = 0x7ffff1c6f438, str = 0x7ffff1c6f438, arr = 0x7ffff1c6f438, obj = 0x7ffff1c6f438, res = 0x7ffff1c6f438, ref = 0x7ffff1c6f438, ast = 0x7ffff1c6f438,
zv = 0x7ffff1c6f438, ptr = 0x7ffff1c6f438, ce = 0x7ffff1c6f438, func = 0x7ffff1c6f438, ww = {w1 = 4056347704, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {call_info = 0, extra = 0}}, type_info = 6}, u2 = {
next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
zstring
结构:
{gc = {refcount = 1, u = {type_info = 70}}, h = 9473262949128236454, len = 9, val = "h"}
- 首先对上面代码分为步骤一 二 三来说对应的就是PHP代码:
echo $a; echo $b; echo $a;
对应的三个输出. - 在第一个步骤中我们可以看到
zval
里面的str =0x7ffff1c6f410
这个代表的就是zstring
的地址 - 再看步骤二,注意在步骤二之前是有赋值操作可以看下上面GDB对应的php代码,这个时候发现步骤二zstring也还是指向
str=0x7ffff1c6f410
指向的是同一个地址,看到这里是不是明白了什么?对的相同的数据指向的是同一个zstring,所以并不会增加额外的空间. - 步骤三 通过改变
$a
的值发现,zvale
里面的str
地址也发生了改变,这就说明只有在值真正发生改变的时候才会真正的占用额外空间来存储zstring
题外话redis中的COW:
不仅是php中使用到写时复制,在简单了解redis
中的写实复制技术:
Redis中使用RDB进行内存快照的时候是不是不允许当前的数据发生变化?但是不允许发生变化此时有人更新redis中数据怎么办呢?如果允许变化快照中数据肯定就会发生错乱,这个时候redis
也采用写实复制解决.
redis 生成rdb 时候会fork 子进程。此时的读写操作:
读:主线程和bgsave子进程互不影响。
写:被修改的数据会被复制一份为副本,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据写入 RDB 文件。
以上就是php的写实复制,有兴趣的也可以动手试试.是不是又可以向同事露一手呢?好了,最好不要装逼,万一人家早就知道呢。反正多学点对自己还是以后面试都是有帮助的,下期看点php引用
,看段代码,可以先自己猜一下:
$b='hell word';
$a=&$b;
echo $a;
echo $b;
unset($a);
echo $a;
echo $b
问题:删除$a $b还能打印出来吗?这也是比较常见的面试题。很多人应该都知道答案,可是为什么呢?我们要做到知其然,知其所以然。