PHP 的一些底层知识
本篇内容比较干涩,请自备矿泉水
文章分6个主题进行讲解
- PHP运行机制和原理
- PHP底层变量数据结构
- PHP传值赋值中的COW特性
- PHP垃圾回收机制
- PHP中数组底层分析
- PHP数组函数分类
PHP运行机制和原理
扫描 -> 解析 -> 编译 -> 执行 -> 输出
执行步骤
- 扫描
对代码进行词法和语法分析,将内容切割成一个个片段 (token)
- 解析
将代码片段筛掉空格注释等,将剩下的token 转成有意义的表达式
- 编译
将表达式编译成中间码 (opcode)
- 执行
将中间码一条条执行
- 输出
将执行结果输出到缓冲区
代码切割
$code = <<<EOF
<?php
echo 'hello world'l;
$data = 1+1;
echo $data;
EOF;
print_r(token_get_all($code));
执行结果
Array
(
[0] => Array
(
[0] => 376
[1] => <?php
[2] => 1
)
[1] => Array
(
[0] => 319
[1] => echo
[2] => 2
)
[2] => Array
(
[0] => 379
[1] =>
[2] => 2
)
[3] => Array
(
[0] => 318
[1] => 'hello world'
[2] => 2
)
[4] => Array
(
[0] => 310
[1] => l
[2] => 2
)
[5] => ;
[6] => Array
(
[0] => 379
[1] =>
[2] => 2
)
[7] => =
[8] => Array
(
[0] => 379
[1] =>
[2] => 3
)
[9] => Array
(
[0] => 308
[1] => 1
[2] => 3
)
[10] => +
[11] => Array
(
[0] => 308
[1] => 1
[2] => 3
)
[12] => ;
[13] => Array
(
[0] => 379
[1] =>
[2] => 3
)
[14] => Array
(
[0] => 319
[1] => echo
[2] => 4
)
[15] => Array
(
[0] => 379
[1] =>
[2] => 4
)
[16] => ;
)
观察上面可以得到三个信息
- Token id 例如空格回车都是 379
- token 字符串
- 行号
Token id 是Zend内部token对应码, 定义于zend_language_parser.h
提高PHP执行效率
- 压缩代码,去除无用注释和空白字符 (jquery.min.js)
- 尽量使用PHP内置函数或扩展函数
- 用 apc/xcache/opcache 等缓存PHP的opcode
- 缓存复杂和耗时的运算结果
- 能异步处理的不要同步处理,如发送邮件
- HHVM 为何速度快
通过虚拟机(类似java) 直接将PHP转换成二进制字节码运行,执行时不用每次都去解析。
PHP底层变量数据结构
使用 zval 结构体保存,下面代码在 Zend/zend.h
定义
typedef union _zvalue_value
{
/* 下面定义描述了PHP的8大数据类型 */
long lval; // 长整型 布尔型
double dval; // 浮点型
struct { // 字符串型
char *val;
int len; // strlen 返回这个值
} str; // NULL 类型表示本身为空
HashTable *ht; // 数组使用哈希表实现
zend_object_value obj; // 对象类型
} zvalue_value;
struct _zval_struct
{
zvalue_value value; /* 变量的值 */
zend_uint refcount__gc;
zend_uchar type; /* 变量的类型 */
zend_uchar is_ref__gc
};
typedef struct _zval_struct zval;
变量类型的定义,下面代码在 Zend/zend_types.h
定义
typedef unsigned int zend_uint;
typedef unsigned char zend_uchar;
PHP数据8大类型统一通过 zvalue_value
联合体存储
联合体自身为空 描述 null
long 描述 int bool
double 描述 float
str 描述 string
HashTable 描述 数字数组和关联数组
zend_object_value 描述 对象和资源
PHP变量类型描述使用 zend_uchar type
描述
#define IS_NULL 0
#define IS_LONG 1
#define IS_DOUBLE 2
#define IS_BOOL 3
#define IS_ARRAY 4
#define IS_OBJECT 5
#define IS_STRING 6
#define IS_RESOURCE 7
#define IS_CONSTANT 8
#define IS_CONSTANT_ARRAY 9
例如 $a=3
结构体如下(伪代码)
struct {
zvalue_value = 3;
refcount__gc = 1;
type = IS_LONG;
is_ref__gc = 0;
}
$a
就像指针一样指向上面的结构体
PHP传值赋值中的COW特性
在 _zval_struct
数据结构中还有下面两个成员
- zend_uint refcount__gc 表示被引用多少次,每次引用+1
- zend_uchar is_ref__gc 表示普通变量还是引用变量
下面通过编写代码了解引用机制
此处我使用的是 php5.4,需要安装 xdebug 来查看变量引用
注意使用 php7.2 测试的时候引用数会一直为0
编译生成 xdebug.so
yum -y install php-devel
tar xf xdebug-2.8.0alpha1.tgz
cd xdebug-2.8.0alpha1
phpize
find /usr/ -name "php-config"
./configure --with-php-config=/usr/bin/php-config
make && make install
ls /usr/lib64/php/modules/
配置 xdebug
php --ini
echo 'zend_extension=/usr/lib64/php/modules/xdebug.so' >> /etc/php.ini
systemctl restart php72-php-fpm.service
php -m | grep xdebug
编写测试代码
$a = 3;
xdebug_debug_zval('a');
输出
a: (refcount=1, is_ref=0)=3
refcount
引用数为1is_ref
为0表示普通变量=3
表示值为3
开始引用
$a = 3;
$b = $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
输出
a: (refcount=2, is_ref=0)=3
b: (refcount=2, is_ref=0)=3
赋予新值
$a = 3;
$b = $a;
$b = 5;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
输出
a: (refcount=1, is_ref=0)=3
b: (refcount=1, is_ref=0)=5
传递地址
$a = 3;
$b = &$a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
输出
a: (refcount=2, is_ref=1)=3
b: (refcount=2, is_ref=1)=3
is_ref 该变量从普通变量转成引用变量
赋予新值
$a = 3;
$b = &$a;
$c = $a;
$b = 5;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
a: (refcount=2, is_ref=1)=5
b: (refcount=2, is_ref=1)=5
c: (refcount=1, is_ref=0)=3
总结
变量之间传值是通过引用赋值形式,无需开辟新的空间,节省资源
当一个变量的值发生改变时,会复制一份来存新的值,取消引用,称为 copy on write (COW)
引用变量不会触发COW
PHP垃圾回收机制
什么是垃圾
上海人: 你算什么垃圾?
如果一个zval 没有任何变量引用它,那它就是垃圾
?: (refcount=0, is_ref=0)=5
为啥要清理垃圾?
有人说php线程结束时会销毁所有变量,关闭所有句柄资源,不是自动的嘛,为啥要清理
- 如果php 短时间内处理多个大文件时(如1G的电影),处理完不回收继续处理下一个,会造成内存溢出
- 如果php 是个守护进程或者长时间运行的脚本,不回收垃圾,慢慢积累会造成内存溢出
如何清理垃圾
- 找垃圾
- 清除
- 找垃圾
通过 get_defined_vars
查看所有已定义变量
底层代码 zend_globals.h
定义了存储所有变量的两个哈希表
struct _zend_executor_globals {
...
HashTable *active_symbol_table; //局部变量符号表
HashTable symbol_table; //全局变量符号表
...
}
找到所有已定义的变量后,寻找哪些变量引用数为0
struct _zval_struct{
...
zend_uint refcount__gc;
zend_uchar is_ref__gc;
...
}
- 清理垃圾
如上面将 refcount__gc 为0的变量清除,这个思路是 PHP5.2版本之前的做法了
PHP5.3后用 引用计数系统中同步周期回收
算法来清除
其实新算法也是基于 refcount__gc 来回收,那么为什么要用新算法呢?
我们知道 refcount__gc 为0的一定是垃圾
但是并不是所有的垃圾 refcount__gc 都为0
也有 refcount__gc 不为0 的垃圾,如下实验可以产生不为0的垃圾
一个例子
$a = ['a'];
$a[] = &$a; //引用自己
xdebug_debug_zval('a');
输出
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='a',
1 => (refcount=2, is_ref=1)=...
)
第二元素: ... 代表递归,引用数2,是一个指针引用变量
官方提供的一张图
此时删掉 $a
$a = ['a'];
$a[] = &$a;
unset($a);
xdebug_debug_zval('a');
输出
a: no such symbol
因为 $a 被删了,所以xdebug打印不出来,那么此时理论结构如下
(refcount=1, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='a',
1 => (refcount=1, is_ref=1)=...
)
此时这个 zval 已经没有符号 (symbol) 引用了,但是它因为自己引用自己 refcount 为1,所以它是一个奇葩的垃圾
对于此情况php脚本结束时,会自动清理,当结束前会占用空间
因此 5.2 版本之前的垃圾清理思路不能覆盖这种情况
引用计数系统中同步周期回收算法 (Concurrent Cycle Collection in Reference Counted System)
继续以上面代码为例进行说明
新算法说明:
将 $a
作为疑似垃圾变量,进行模拟删除 (refcount--),然后模拟恢复,恢复条件是有其他变量引用该值时才进行模拟恢复 (refcount++)
这样没能恢复成功的就是垃圾了,把它删除即可。
例如上面的奇葩垃圾:
(refcount=1, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='a',
1 => (refcount=1, is_ref=1)=...
)
模拟删除后变成:
(refcount=0, is_ref=1)=array (
0 => (refcount=0, is_ref=0)='a',
1 => (refcount=0, is_ref=1)=...
)
然后模拟恢复:
因为没有类似 $a 这种 symbol 取指向该zval,所以恢复不来
何时清除
通过上面的算法疑似垃圾会存放到一个区域(垃圾站),只有垃圾站满了才会立刻清除。 注意前提是开启垃圾回收
开启垃圾回收两种方式
php.ini 下的
zend.enable_gc = On
默认开启通过
gc_enable()
和gc_disable()
来打开或关闭垃圾回收
可以直接使用 gc_collect_cycles()
函数强制执行周期回收
最后说了那么多,其实只需要了解其中的原理,整个过程不需要PHP开发人员参与,只需要调用 gc_enable() 或 gc_collect_cycles() 即可实现自动回收
PHP中数组底层分析
先复习一下数组特性
PHP 数组键的特性
$arr = [
1 => 'a',
'1' => 'b',
1.5 => 'c',
true => 'd',
];
print_r($arr);
Array
(
[1] => d
)
key 可以是 integer 或 string
value 可以是任意类型
key 有如下特性
- 数字字符串会被转成整型 '1' => 1
- 浮点型和布尔型转成整型 1.3 =》 1
- null会被当做空字符串 null => ''
- 键名不可以使用对象和数组
- 相同键名后面覆盖前面
访问数组元素
- $arr[key]
- $arr{key}
5.4 版本后可以使用如下
function getArr(){ return [1,2,3,4]; }
echo getArr()[2];
删除数组元素
$a = [1,2,3,4];
foreach ($a as $k => $v) {
unset($a[$k]);
}
$a[] = 5;
print_r($a);
Array
(
[4] => 5
)
- 删除不会重置索引
数组遍历
- for
- foreach
- array_walk
- array_map
- current 和 next
数组内部实现
实现使用两个结构 HashTable
和 bucket
- 什么是 HashTable
哈希表,通过关键字直接访问内存存储位置的数据结构。
通过把关键字进行哈希函数计算,得到映射到表中的位置使得: 查找,插入,修改,删除均在O(1)完成
下面代码在 Zend/zend_types.h
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar nApplyCount,
zend_uchar nIteratorsCount,
zend_uchar consistency)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
Bucket *arData;
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
旧版结构体
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer;
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
unsigned char nApplyCount;
};
成员 | 说明 |
---|---|
nTableSize | Bucket大小,最小为8,以2x增长 |
nTableMask | 索引优化 nTableSize-1 |
nNumOfElements | 元素个数 使用count()函数直接返回这个 |
nNextFreeElement | 下一个索引位置 foreach使用 |
pInternalPointer | 当前遍历的指针,foreach比for快的原因,reset current函数使用 |
pListHead | 存储数组头部指针 |
pListTail | 存储数组尾部指针 |
arBuckets | 实际存储容器 |
arData | Bucket数据 |
nApplyCount | 记录被递归次数,防止死循环递归 |
typedef struct bucket
{
ulong h;
uint nKeyLength;
void *pData;
void *pDataPtr;
struct bucket *pListNext;
struct bucket *pListLast;
struct bucket *pNext;
struct bucket *pLast;
const char *arKey;
};
成员 | 说明 |
---|---|
h | 对char *key进行hash后的值,或是用户指定数字索引值 |
nKeyLength | 哈希关键字长度,若为索引数字则为0 |
pData | 指向value 一般是用户数据的副本,若为指针数据则指向指针 |
pDataPtr | 如果是指针数据,指针会指向真正value,上面指向此 |
pListNext | 整个hash表下个元素 |
pListLast | 整个hash表上个元素 |
pNext | 同一个hash的下一个元素 |
pLast | 同一个hash的上一个元素 |
arKey | 保存当前key对应的字符串 |
foreach 遍历先从 HashTable 的 pListHead -> pListNext
pNext
和 pLast
用于hash冲突同一个hash不同个bucket之间指针
PHP数组函数分类
建议体验一下下面的函数,不用记住,只是留个印象,当你需要用的时候会联想起来的,而不用自己去实现
遍历
- prev
- next
- current
- end
- reset
- each
排序
- sort
- rsort
- asort
- ksort
- krsort
- uasort
- uksort
查找
- in_array
- array_search
- array_key_exists
分合
- array_slice
- array_splice
- implode
- explode
- array_combine
- array_chunk
- array_keys
- array_values
- array_columns
集合
- array_merge
- array_diff
- array_diff_*
- array_intersect
- array_intersect_*
队列/栈
- array_push
- array_pop
- array_shift
其他
- array_fill
- array_flip
- array_sum
- array_reverse
转载请指明出处 https://www.cnblogs.com/demonxian3/p/11327522.html
PHP 的一些底层知识的更多相关文章
- 写给Android App开发人员看的Android底层知识(1)
这个系列的文章一共8篇,我酝酿了很多年,参考了很多资源,查看了很多源码,直到今天把它写出来,也是战战兢兢,生怕什么地方写错了,贻笑大方. (一)引言 早在我还是Android菜鸟的时候,有很多技术我都 ...
- 写给Android App开发人员看的Android底层知识(5)
(十)Service Service有两套流程,一套是启动流程,另一套是绑定流程.我们做App开发的同学都应该知道. 1)在新进程启动Service 我们先看Service启动过程,假设要启动的Ser ...
- JS底层知识理解之执行上下文篇
JS底层知识理解之执行上下文篇 一.什么是执行上下文(Execution Context) 执行上下文可以理解为当前代码的执行环境,它会形成一个作用域. 二.JavaScript引擎会以什么方式去处理 ...
- Java底层知识学习:Bytecode and JMM
最近在跟着耗子哥的程序员练级指南学习Java底层知识,结合<深入理解Java虚拟机>这本书在看,写笔记,看资料,成长中…… 目前看完了第二章JMM和各内存区OOM的情况 一篇图文并茂介绍字 ...
- 极客时间-左耳听风-程序员攻略-Java底层知识
Java 字节码相关 字节码编程,也就是动态修改或是动态生成 Java 字节码.Java 的字节码相当于汇编,其中的一些细节. Java Zone: Introduction to Java Byte ...
- 写给Android App开发人员看的Android底层知识(2)
(五)AMS 如果站在四大组件的角度来看,AMS就是Binder中的Server. AMS全称是ActivityManagerService,看字面意思是管理Activity的,但其实四大组件都归它管 ...
- 写给Android App开发人员看的Android底层知识(7)
(十二)ContentProvider (1)ContentProvider是什么? ContentProvider,简称CP. 做App开发的同学,尤其是电商类App,对CP并不熟悉,对这个概念的最 ...
- 写给Android App开发人员看的Android底层知识(8)
(十)PMS及App安装过程 PMS,全称PackageManagerService,是用来获取Apk包的信息的. 在前面分析四大组件与AMS通信的时候,我们介绍过,AMS总是会使用PMS加载包的信息 ...
- Redis数据结构底层知识总结
Redis数据结构底层总结 本篇文章是基于作者黄建宏写的书Redis设计与实现而做的笔记 数据结构与对象 Redis中数据结构的底层实现包括以下对象: 对象 解释 简单动态字符串 字符串的底层实现 链 ...
- 计算机底层知识拾遗(九)深入理解内存映射mmap
内存映射mmap是Linux内核的一个重要机制,它和虚拟内存管理以及文件IO都有直接的关系,这篇细说一下mmap的一些要点. 修改(2015-11-12):Linux的虚拟内存管理是基于mmap来实现 ...
随机推荐
- filebeat直连elasticsearch利用pipeline提取message中的字段
这里使用filebeat直连elasticsearch的形式完成数据传输,由于没有logstash,所有对于原始数据的过滤略显尴尬(logstash的filter非常强大). 但是由于业务需求,还是需 ...
- 设计模式-工厂方法模式(FactoryMethod)
工厂方法模式又称多态工厂模式.工厂方法模式是定义一个创建产品对象的接口(FruitFactory),将具体创建工作给具体的实现类(AppFactory,BananaFactory,PearFactor ...
- django基础知识之HttpReqeust对象:
HttpReqeust对象 服务器接收到http协议的请求后,会根据报文创建HttpRequest对象 视图函数的第一个参数是HttpRequest对象 在django.http模块中定义了HttpR ...
- 西门子 S7-300 PLC 从入门到精通的100个经典问题
1:使用CPU 315F和ET 200S时应如何避免出现“通讯故障”消息? 使用CPU S7 315F, ET 200S以及故障安全DI/DO模块,那么您将调用OB35 的故障安全程序.而且,您已 ...
- 剑指offer第二版-8.二叉树的下一个节点
描述:给定一棵二叉树和其中的一个节点,找出中序遍历序列的下一个节点.树中应定义指向左节点.右节点.父节点的三个变量. 思路: 1.如果输入的当前节点有右孩子,则它的下一个节点即为该右孩子为根节点的子树 ...
- cogs.12运输问题2题解
乍一看貌似和运输问题1没有任何区别,但本题有一个有意思的东西叫做下限,我个人称之为非强制下限,因为本题中要求的实际是我走这条边这条边才至少走下限的流,虽然出题人没说,但从样例来看确实是这样的,而强制下 ...
- 求1-2/3+3/5-4/7+......49/97和(C语言实现)
一.功能需求 求1 - 2/3 + 3/5 - 4/7 + ......49/97的和 C语言等级考试中也有涉及到类似的需求. 二.代码分析 仔细查看功能需求,可以发现这个等式的三个规律: 1.从每一 ...
- python+selenium 批量执行时出现随机报错问题【已解决】
出现场景:用discover方法批量执行py文件,出现随机性的报错(有时a.py报错,有时b.py报错...),共同特点:均是打开新窗口后,切换最新窗口,但定位不到新窗口的元素,超时报错.由于个人项目 ...
- c++小游戏——五子棋
#include<iostream> #include<iomanip> #include<cstring> using namespace std; const ...
- java多线程总结-同步容器与并发容器的对比与介绍
1 容器集简单介绍 java.util包下面的容器集主要有两种,一种是Collection接口下面的List和Set,一种是Map, 大致结构如下: Collection List LinkedLis ...