PHP 변수 구현의 기본 구조는 zval
,各种类型的实现均基于此结构实现,是PHP中最基础的一个结构,每个PHP变量都对应一个zval
입니다. 이 구조와 PHP 변수의 메모리 관리 메커니즘을 살펴보겠습니다.
zval 구조
관련 학습 권장사항: PHP 엔트리부터 마스터까지 프로그래밍
zval结构比较简单,内嵌一个union类型的zend_value保存具体变量类型的值或指针,zval中还有两个union:u1、u2:
u1:그 의미는 비교적 직관적입니다. 변수의 유형은 u1.type
으로 구분되며, 다른 값은 type_flags입니다.
변수의 메모리 관리와 gc 메커니즘에 사용되는 유형 마스크입니다. 이는 세 번째 부분에서 자세히 분석됩니다. 예약됨
, 지금은 그대로 두세요u1.type
区分,另外一个值type_flags
为类型掩码,在变量的内存管理、gc机制中会用到,第三部分会详细分析,至于后面两个const_flags
、reserved
暂且不管
u2:这个值纯粹是个辅助值,假如zval
只有:value
、u1
两个值,整个zval的大小也会对齐到16byte,既然不管有没有u2大小都是16byte,把多余的4byte拿出来用于一些特殊用途还是很划算的,比如next在哈希表解决哈希冲突时会用到,还有fe_pos在foreach会用到......
从zend_value
可以看出,除long
、double
类型直接存储值外,其它类型都为指针,指向各自的结构。
类型
zval.u1.type
u2:
이 값은 순전히 보조 값입니다.zval
에 두 개의 값만 있는 경우: value
와 u1
를 사용하면 전체 zval 크기도 16byte로 정렬되므로 u2 유무에 관계없이 크기가 16byte이므로 여전히 사용하는 것이 비용 효율적입니다. 예를 들어, next는 해시 충돌을 해결하기 위해 해시 테이블에서 사용되며 foreach에서 사용되는 fe_pos가 있습니다...🎜🎜보시다시피 zend_value
에서(long
및 double 제외) 값을 직접 저장하는
유형을 제외하고 다른 유형은 해당 구조를 가리키는 포인터입니다. 🎜🎜🎜zval.u1.type
유형: 🎜🎜🎜🎜가장 간단한 유형은 true, false, long, double, null입니다. True, false, null은 값이 없으며 유형별로 직접 구분되는 반면, long 및 double의 값은 값에 직접 저장됩니다. : zend_long, double, 즉, 스칼라 유형에는 추가 값 포인터가 필요하지 않습니다.
Strings
PHP의 문자열은 다음과 같이 표현됩니다. zend_string
:
gc: 현재 값에 대한 참조 수와 같은 변수 참조 정보, 참조 카운팅을 사용하는 모든 변수 유형에는 이 구조는 3.1절에서 자세히 분석하겠습니다.
h: 배열에서 인덱스를 계산할 때 사용되는 해시 값
len: 문자열 길이, 이 값을 통해 바이너리 보안이 보장됩니다.
val : 문자열 내용, 가변 길이 구조체, 할당 시 len 길이에 따라 메모리 적용
실제로 문자열은 여러 범주로 나눌 수 있습니다: IS_STR_PERSISTENT(malloc을 통해 할당), IS_STR_INTERNED(PHP 코드로 작성된 일부 리터럴) 함수 이름, 변수 값 등의 수량, IS_STR_PERMANENT(영구 값, 수명 주기가 요청보다 큼), IS_STR_CONSTANT(상수), IS_STR_CONSTANT_UNQUALIFIED, 이 정보는 플래그 zval.value->gc.u.flags를 통해 저장됩니다. 나중에 사용할 내용입니다. 나중에 자세히 분석하겠습니다.
Array
array는 PHP의 매우 강력한 데이터 구조입니다. 기본 구현은 일반적인 정렬된 HashTable입니다. 다음 섹션에서는 배열 구현을 간략하게 살펴보겠습니다.
Object/Resource
리소스는 tcp 연결, 파일 핸들 등과 같은 유형을 참조합니다. 이 유형은 원하는 대로 구조체를 정의할 수 있습니다. 이 유형은 나중에 따로 분석할 것이므로 여기서는 더 언급하지 않겠습니다.
Reference
Reference는 실제로 다른 PHP 변수를 가리킵니다. 이를 수정하면 가리키는 실제 zval이 직접 변경됩니다. PHP의 포인터입니다. &
연산자를 통해 참조 변수를 생성합니다. 즉, 이전 유형이 무엇이든 &
는 먼저 새 zval을 생성하고 유형은 IS_REFERENCE를 가리킨 다음 val 값을 원래 zval 값으로 바꿉니다. &
操作符产生一个引用变量,也就是说不管以前的类型是什么,&
首先会将新生成一个zval,类型为IS_REFERENCE,然后将val的value指向原来zval的value。
结构非常简单,除了公共部分zend_refcounted_h
外只有一个val
구조는 매우 간단합니다. 공개 부분인 zend_refcounted_h
를 제외하면 val
이 하나만 있습니다. . 자세한 내용을 보려면 예를 들어 보겠습니다. 구조적 관계:
최종 결과는 그림과 같습니다.
🎜🎜🎜참고: 참조는 &
&
产生,无法通过赋值传递,比如:
$b = &$a
这时候$a
、$b
的类型是引用,但是$c = $b
并不会直接将$b
赋值给$c
,而是把$b
实际指向的zval赋值给$c
,如果想要$c
也是一个引用则需要这么操作:
这个也表示PHP中的引用只可能有一层,不会出现一个引用指向另外一个引用的情况,也就是没有C语言中指针的指针
를 통해서만 생성할 수 있으며
🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜
$b = &$a
이때 $a
및 $b
는 참조이지만 $c = $b
는 $b
를 $c
에 직접 할당하지 않지만 $b
실제로 가리키는 zval이 $c
에 할당됩니다. $c
를 참조로 사용하려면 이렇게 해야 합니다. : 🎜🎜🎜🎜🎜 🎜🎜🎜이것은 또한 한 가지 참조 수준만 있을 수 있다는 뜻이기도 합니다🎜,다음과 같은 상황은 없습니다. 하나의 참조는 다른 참조🎜를 가리킵니다. 즉, C 언어에는 가 없습니다. 포인터의 개념
입니다. 🎜🎜🎜메모리 관리🎜🎜🎜 다음으로 변수의 할당과 소멸을 분석합니다. 🎜在分析变量内存管理之前我们先自己想一下可能的实现方案,最简单的处理方式:定义变量时alloc一个zval及对应的value结构(ref/arr/str/res...),赋值、函数传参时硬拷贝一个副本,这样各变量最终的值完全都是独立的,不会出现多个变量同时共用一个value的情况,在执行完以后直接将各变量及value结构free掉。
这种方式是可行的,而且内存管理也很简单,但是,硬拷贝带来的一个问题是效率低,比如我们定义了一个变量然后赋值给另外一个变量,可能后面都只是只读操作,假如硬拷贝的话就会有多余的一份数据,这个问题的解决方案是:引用计数+写时复制。PHP变量的管理正是基于这两点实现的。
引用计数
引用计数是指在value中增加一个字段refcount
记录指向当前value的数量,变量复制、函数传参时并不直接硬拷贝一份value数据,而是将refcount++
,变量销毁时将refcount--
,等到refcount
减为0时表示已经没有变量引用这个value,将它销毁即可。
引用计数的信息位于给具体value结构的gc中:
从上面的zend_value结构可以看出并不是所有的数据类型都会用到引用计数,long
、double
直接都是硬拷贝,只有value是指针的那几种类型才可能会用到引用计数。
下面再看一个例子:
$a = "hi~";$b = $a;
猜测一下变量$a/$b
的引用情况。
这个不跟上面的例子一样吗?字符串"hi~"
有$a/$b
两个引用,所以zend_string1(refcount=2)
。但是这是错的,gdb调试发现上面例子zend_string的引用计数为0。这是为什么呢?
$a,$b -> zend_string_1(refcount=0,val="hi~")
事实上并不是所有的PHP变量都会用到引用计数,标量:true/false/double/long/null是硬拷贝自然不需要这种机制,但是除了这几个还有两个特殊的类型也不会用到:interned string(内部字符串,就是上面提到的字符串flag:IS_STR_INTERNED)、immutable array,它们的type是IS_STRING
、IS_ARRAY
,与普通string、array类型相同,那怎么区分一个value是否支持引用计数呢?还记得zval.u1
中那个类型掩码type_flag
吗?正是通过这个字段标识的,这个字段除了标识value是否支持引用计数外还有其它几个标识位,按位分割,注意:type_flag
与zval.value->gc.u.flag
不是一个值。
支持引用计数的value类型其zval.u1.type_flag
包含(注意是&,不是等于)IS_TYPE_REFCOUNTED
:
#define IS_TYPE_REFCOUNTED (1<<2)
下面具体列下哪些类型会有这个标识:
| type | refcounted | +----------------+------------+ |simple types | | |string | Y | |interned string | | |array | Y | |immutable array | | |object | Y | |resource | Y | |reference | Y |
simple types很显然用不到,不再解释,string、array、object、resource、reference有引用计数机制也很容易理解,下面具体解释下另外两个特殊的类型:
interned string:内部字符串,这是种什么类型?我们在PHP中写的所有字符都可以认为是这种类型,比如function name、class name、variable name、静态字符串等等,我们这样定义:$a = "hi~;"
后面的字符串内容是唯一不变的,这些字符串等同于C语言中定义在静态变量区的字符串:char *a = "hi~";
,这些字符串的生命周期为request期间,request完成后会统一销毁释放,自然也就无需在运行期间通过引用计数管理内存。
immutable array:只有在用opcache的时候才会用到这种类型,不清楚具体实现,暂时忽略。
写时复制
上一小节介绍了引用计数,多个变量可能指向同一个value,然后通过refcount统计引用数,这时候如果其中一个变量试图更改value的内容则会重新拷贝一份value修改,同时断开旧的指向,写时复制的机制在计算机系统中有非常广的应用,它只有在必要的时候(写)才会发生硬拷贝,可以很好的提高效率,下面从示例看下:
$a = array(1,2);$b = &$a;$c = $a;//发生分离$b[] = 3;
最终的结果:
不是所有类型都可以copy的,比如对象、资源,实时上只有string、array两种支持,与引用计数相同,也是通过zval.u1.type_flag
标识value是否可复制的:
#define IS_TYPE_COLLECTABLE (1<<3)
| type | copyable | +----------------+------------+ |simple types | | |string | Y | |interned string | | |array | Y | |immutable array | | |object | | |resource | | |reference | |
copyable的意思是当value发生duplication时是否需要copy,这个具体有两种情形下会发生:
a.从literal变量区复制到局部变量区,比如:$a = [];
实际会有两个数组,而$a = "hi~";//interned string
则只有一个string
b.局部变量区分离时(写时复制):如改变变量内容时引用计数大于1则需要分离,$a = [];$b = $a; $b[] = 1;
这里会分离,类型是array所以可以复制,如果是对象:$a = new user;$b = $a;$a->name = "dd";这种情况是不会复制object的,$a、$b指向的对象还是同一个<p style="color:rgb(51,51,51);clear:both;min-height:1em;font-family:'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;font-size:16px;">具体literal、局部变量区变量的初始化、赋值后面编译、执行两篇文章会具体分析,这里知道变量有个<code>copyable
的属性就行了。
变量回收
PHP变量的回收主要有两种:主动销毁、自动销毁。主动销毁指的就是unset,而自动销毁就是PHP的自动管理机制,在return时减掉局部变量的refcount,即使没有显式的return,PHP也会自动给加上这个操作。
垃圾回收
PHP变量的回收是根据refcount实现的,当unset、return时会将变量的引用计数减掉,如果refcount减到0则直接释放value,这是变量的简单gc过程,但是实际过程中出现gc无法回收导致内存泄漏的bug,先看下一个例子:
$a = [1];$a[] = &amp;amp;amp;amp;amp;$a;unset($a);
unset($a)
之前引用关系:
unset($a)
之后:
可以看到,unset($a)
之后由于数组中有子元素指向$a
,所以refcount > 0
,无法通过简单的gc机制回收,这种变量就是垃圾,垃圾回收器要处理的就是这种情况,目前垃圾只会出现在array、object两种类型中,所以只会针对这两种情况作特殊处理:当销毁一个变量时,如果发现减掉refcount后仍然大于0,且类型是IS_ARRAY、IS_OBJECT则将此value放入gc可能垃圾双向链表中,等这个链表达到一定数量后启动检查程序将所有变量检查一遍,如果确定是垃圾则销毁释放。
标识变量是否需要回收也是通过u1.type_flag
区分的:
#define IS_TYPE_COLLECTABLE
| type | collectable | +----------------+-------------+ |simple types | | |string | | |interned string | | |array | Y | |immutable array | | |object | Y | |resource | | |reference | |
具体的垃圾回收过程这里不再介绍。
위 내용은 PHP7 커널에서 변수의 내부 구현을 분석합니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!