PHP カーネルで探索される変数 (1) Zval

WBOY
リリース: 2016-06-23 13:44:28
オリジナル
948 人が閲覧しました

データの入れ物として、数値、配列、文​​字列、オブジェクトなどの変数を扱う必要があることが多いため、変数は言語にとって不可欠な基盤であると言えます。この記事は、PHP カーネルで検討される変数に関する最初の記事であり、主に次の側面を含む zval の基本的な知識を紹介します:

  1. Zval の基本構造
  2. zval の表示方法: debug_zval_dump と xdebug
  3. Zvalの原理、COWなど

急いで書いているため、必然的に間違いがあるかもしれません、ご指摘は歓迎です。

1. Zval の基本構造

Zval は、PHP で最も重要なデータ構造の 1 つです (もう 1 つの重要なデータ構造は、PHP の変数の値と型に関する情報が含まれています)。これは構造体であり、基本的な構造は次のとおりです:

struct _zval_struct {    zvalue_value value;     /* value */    zend_uint refcount__gc;  /* variable ref count */    zend_uchar type;          /* active type */    zend_uchar is_ref__gc;    /* if it is a ref variable */};typedef struct _zval_struct zval;
ログイン後にコピー

そのうち:

1. zval_value value

変数の実際の値、特に zvalue_value の結合:

typedef union _zvalue_value {    long lval;                  /* long value */    double dval;                /* double value */    struct {                    /* string */        char *val;        int len;    } str;    HashTable *ht;              /* hash table value,used for array */    zend_object_value obj;      /* object */} zvalue_value;
ログイン後にコピー

2. zen d_uint refcount__gc

この値は実際には、変数 (またはシンボル、シンボル) の数を保存するために使用されるカウンターです。すべてのシンボルはシンボル テーブル (シンボル テーブル) に保存されます。これについては後で説明します。 ) は zval を指します。変数が生成されると、その refcount=1 になります。$a = $b などの一般的な代入操作では zval の refcount が 1 ずつ増加し、それに応じて unset 操作によって 1 ずつ減少します。 PHP5.3 より前では、GC の実装に参照カウント メカニズムが使用されていました。zval の refcount が 0 未満の場合、Zend エンジンは zval を指す変数が存在しないと判断し、その結果、zval が占有しているメモリ空間を解放していました。ズヴァル。しかし、物事はそれほど単純ではない場合もあります。 zval を指す変数が設定されていない場合でも、単純な参照カウント メカニズムでは循環参照される zval を GC できず、その結果メモリ リーク (メモリ リーク) が発生することが後でわかります。

3. zend_uchar type

このフィールドは、変数の実際の型を示すために使用されます。 PHP の学習を開始したとき、PHP の変数には 4 つのスカラー型 (bool、int、float、string)、2 つの複合型 (配列、オブジェクト)、および 2 つの特殊な型 (リソースと NULL) が含まれていることはすでに知っていました。 zend 内では、これらの型は次のマクロ (コードの場所 phpsrc/Zend/zend.h) に対応します:

#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#define IS_CALLABLE 10
ログイン後にコピー

4. is_ref__gc

このフィールドは、変数が参照変数であるかどうかをマークするために使用されます。通常の変数の場合は値が 0、参照変数の場合は値が 1 になります。この変数は、zval の共有、分離などに影響します。これについては後で説明します。

名前が示すように、ref_count__gc と is_ref__gc は、PHP の GC メカニズムに必要な 2 つの非常に重要なフィールドであり、これら 2 つのフィールドの値は、xdebug などのデバッグ ツールを通じて表示できます。

2. xdebug のインストールと設定

xdebug は、オープンソースの PHP パフォーマンス分析およびデバッグ ツールです。一般的なプログラムのデバッグには、var_dump、echo、print、debug_backtrace などの一般的なデバッグ ツールで基本的に十分ですが、一部の複雑なデバッグやパフォーマンス テストには、xdebug が間違いなく優れたヘルパーです (Xhprof などの他のツールも優れています)。

この記事の基本環境:

xdebug をインストールする基本的なプロセスは次のとおりです (実際にソース コードから拡張機能をコンパイルします):

1. ソース コード パッケージをダウンロードします

ダウンロード アドレスは次のとおりです。 : http://www .xDebug.org/DOCS/Install

この記事でダウンロードしたバージョンは: xdebug-2.6.tar.gz

2 ./configure Configuration

5。 make install

これにより、xdebug/modules にある xdebug.so 拡張ファイル (zend_extension) が生成されます

6. xdebug 拡張ファイルを php.ini にロードします

tar xvzf xdebug-2.6.tar.gz
ログイン後にコピー

7。

zend_extension=your-xdebug-path/xdebug.so
ログイン後にコピー

各設定項目の意味については、ここでは詳しく紹介しません。詳細については、http://www.xdebug.org/docs/all を参照してください。 さて、PHP には、Xdebug の拡張情報がすでに用意されているはずです。 (php ?m、または phpinfo()):

これで、スクリプト内で、xdebug_debug_zval を通じて Zval 情報を出力できます。

xdebug.profiler_enable = onxdebug.default_enable = onxdebug.trace_output_dir="/tmp/xdebug"xdebug.trace_output_name = trace.%c.%pxdebug.profiler_output_dir="/tmp/xdebug"xdebug.profiler_output_name="cachegrind.out.%s"
ログイン後にコピー

3 Zval のその他の原則。

(注、この部分の主な参考資料は http://derickrethans.nl/collecting-garbage-phps-take-on-variables.html です。著者の Derick Rethans は優れた PHP カーネルの専門家であり、世界中で多くのレポートを提供しています。ここ (http://derickrethans.nl/talks.html) には、著者の各スピーチの記録があり、その多くは私たちの綿密な調査と研究に値します)


私たちはすでに述べました。その前に、PHP は変数を保存するために Zval 構造体を使用します。ここでは、zval の詳細を追跡していきます。

1.       创建变量时,会创建一个zval.

$str = "test zval";xdebug_debug_zval('str');
ログイン後にコピー

输出结果:

str: (refcount=1, is_ref=0)='test zval'
ログイン後にコピー

当使用$str="test zval";来创建变量时,会在当前作用域的符号表中插入新的符号(str),由于该变量是一个普通的变量,因此会生成一个refcount=1且is_ref=0的zval容器。也就是说,实际上是这样的:

2.       变量赋值给另外一个变量时,会增加zval的refcount值。

$str  = "test zval";$str2 = $str;xdebug_debug_zval('str');xdebug_debug_zval('str2');
ログイン後にコピー

输出结果:      

str: (refcount=2, is_ref=0)='test zval'str2: (refcount=2, is_ref=0)='test zval'
ログイン後にコピー

同时我们看到,str和是str2这两个symbol的zval结构是一样的。这里其实是PHP所做的一个优化,由于str和str2都是普通变量,因而它们指向了同一个zval,而没有为str2开辟单独的zval。这么做,可以在一定程度上节省内存。这时的str,str2与zval的对应关系是这样的:

 

3.       使用unset时,对减少相应zval的refcount值

$str  = "test zval";$str3 = $str2 = $str;xdebug_debug_zval('str');unset($str2,$str3)xdebug_debug_zval('str');
ログイン後にコピー

结果为:

str: (refcount=3, is_ref=0)='test zval'str: (refcount=1, is_ref=0)='test zval'
ログイン後にコピー

由于unset($str2,$str3)会将str2和str3从符号表中删除,因此,在unset之后,只有str指向该zval,如下图所示:

 

现在如果执行unset($str),则由于zval的refcount会减少到0,该zval会从内存中清理。这当然是最理想的情况。

但是事情并不总是那么乐观。

4.       数组变量与普通变量生成的zval非常类似,但也有很大不同

与标量这些普通变量不同,数组和对象这类复合型的变量在生成zval时,会为每个item项生成一个zval容器。例如:

1

2

3

4

$ar = array (

     'id'   => 38,

     'name' => 'shine'

);
xdebug_debug_zval( 'ar' );

打印出zval的结构是:

ar: (refcount=1, is_ref=0)=array (    'id' => (refcount=1, is_ref=0)=38,     'name' => (refcount=1, is_ref=0)='shine')
ログイン後にコピー

如下图所示:

 

可以看出,变量$ar生成的过程中,共生成了3个zval容器(红色部分标注)。对于每个zval而言,refcount的增减规则与普通变量的相同。例如,我们在数组中添加另外一个元素,并把$ar['name']的值赋给它:

$ar = array(    'id'   => 38,    'name' => 'shine'); $ar['test'] = $ar['name'];xdebug_debug_zval('ar');
ログイン後にコピー

则打印出的zval为:

ar: (refcount=1, is_ref=0)=array (    'id' => (refcount=1, is_ref=0)=38,    'name' => (refcount=2, is_ref=0)='shine',    'test' => (refcount=2, is_ref=0)='shine')
ログイン後にコピー

如同普通变量一样,这时候,name和test这两个symbol指向同一个zval:

 

同样的,从数组中移除元素时,会从符号表中删除相应的符号,同时减少对应zval的refcount值。同样,如果zval的refcount值减少到0,那么就会从内存中删除该zval:

$ar = array(    'id'   => 38,    'name' => 'shine'); $ar['test'] = $ar['name'];unset($ar['test'],$ar['name']);xdebug_debug_zval('ar');
ログイン後にコピー

输出结果为:

ar: (refcount=1, is_ref=0)=array ('id' => (refcount=1, is_ref=0)=38)
ログイン後にコピー

5.       引用的出现,会令zval的规则变得复杂

在加入引用之后,情况会变的稍微复杂一点。例如,在数组中添加对本身的引用:

$a = $array('one');$a[] = &$a;xdebug_debug_zval('a');
ログイン後にコピー

输出的结果:

a: (refcount=2, is_ref=1)=array (    0 => (refcount=1, is_ref=0)='one',     1 => (refcount=2, is_ref=1)=...)
ログイン後にコピー

上述输出中,…表示指向原始数组,因而这是一个循环的引用。如下图所示:

 

现在,我们对$a执行unset操作,这会在symbol table中删除相应的symbol,同时,zval的refcount减1(之前为2),也就是说,现在的zval应该是这样的结构:

(refcount=1, is_ref=1)=array (    0 => (refcount=1, is_ref=0)='one',     1 => (refcount=1, is_ref=1)=...)
ログイン後にコピー

也就是下图所示的结构:

 

  这时,不幸的事情发生了!

  Unset之后,虽然没有变量指向该zval,但是该zval却不能被GC(指PHP5.3之前的单纯引用计数机制的GC)清理掉,因为zval的refcount均大于0。这样,这些zval实际上会一直存在内存中,直到请求结束(参考SAPI的生命周期)。在此之前,这些zval占据的内存不能被使用,便白白浪费了,换句话说,无法释放的内存导致了内存泄露。

  如果这种内存泄露仅仅发生了一次或者少数几次,倒也还好,但如果是成千上万次的内存泄露,便是很大的问题了。尤其在长时间运行的脚本中(例如守护程序,一直在后台执行不会中断),由于无法回收内存,最终会导致系统“再无内存可用”。

6.       zval分离(Copy on write和change on write)

前面我们已经介绍过,在变量赋值的过程中例如$b = $a,为了节省空间,并不会为$a和$b都开辟单独的zval,而是使用共享zval的形式:

        

那么问题来了:如果其中一个变量发生变化时,如何处理zval的共享问题?

对于这样的代码:

$a = "a simple test";$b = $a; echo "before write:".PHP_EOL;xdebug_debug_zval('a');xdebug_debug_zval('b'); $b = "thss";echo "after write:".PHP_EOL;xdebug_debug_zval('a');xdebug_debug_zval('b');
ログイン後にコピー

打印的结果是:

before write:a: (refcount=2, is_ref=0)='a simple test'b: (refcount=2, is_ref=0)='a simple test'after write:a: (refcount=1, is_ref=0)='a simple test'b: (refcount=1, is_ref=0)='thss'
ログイン後にコピー

起初,符号表中a和b指向了同一个zval(这么做的原因是节省内存),而后$b发生了变化,Zend会检查b指向的zval的refcount是否为1,如果是1,那么说明只有一个符号指向该zval,则直接更改zval。否则,说明这是一个共享的zval,需要将该zval分离出去,以保证单独变化互不影响,这种机制叫做COW ?Copy on write。在很多场景下,COW都是一种比较高效的策略。

那么对于引用变量呢?

$a = 'test';$b = &$a;<br>echo "before change:".PHP_EOL;xdebug_debug_zval('a');xdebug_debug_zval('b');<br>$b = 12;echo "after change:".PHP_EOL;xdebug_debug_zval('a');xdebug_debug_zval('b');<br>unset($b);echo "after unset:".PHP_EOL;xdebug_debug_zval('a');xdebug_debug_zval('b');
ログイン後にコピー

输出的结果为:

before change:a: (refcount=2, is_ref=1)='test'b: (refcount=2, is_ref=1)='test'after change:a: (refcount=2, is_ref=1)=12b: (refcount=2, is_ref=1)=12after unset:a: (refcount=1, is_ref=0)=12
ログイン後にコピー

可以看出,在改变了$b的值之后,Zend会检查zval的is_ref检查是否是引用变量,如果是引用变量,则直接更改即可,否则,需要执行刚刚提到的zval分离。由于$a 和 $b是引用变量,因而更改共享的zval实际上也间接更改了$a的值。而在unset($b)之后,变量$b从符号表中删除了。

这里也说明一个问题,unset并不是清除zval,而只是从符号表中删除相应的symbol。这样一来,之前很多的关于引用的疑问也可以理解了(下一节我们将深入探索PHP的引用)。

この記事の参考文献:

  1. Brother Bird の詳細な変数参照/分離 http://www.laruence.com/2008/09/19/520.html
  2. この記事の主な参考文献 http://derickrethans. nl/collection-garbage-phps-take-on-variables.html
  3. http://blog.csdn.net/phpkernel/article/details/5732784
  4. http://www.jb51.net/article/50080。 html
  5. http://www.nowmagic.net/librarys/veda/detail/1442
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート