php static静态属性和静态方法的调用

 更新时间:2016年11月25日 15:31  点击:1972
php static静态属性和静态方法我们以前有介绍过它们的区别在哪里,下面我们来看看关于static静态属性和静态的用法例子。

这里分析了php面向对象中static静态属性和静态方法的调用。关于它们的调用(能不能调用,怎么样调用),需要弄明白了他们在内存中存放位置,这样就非常容易理解了。静态属性、方法(包括静态与非静态)在内存中,只有一个位置(而非静态属性,有多少实例化对象,就有多少个属性)。

实例:

 代码如下 复制代码

<?php
header("content-type:text/html;charset=utf-8");
class Human{
 static public $name = "小妹";
 public $height = 180;
 static public function tell(){
 echo self::$name;//静态方法调用静态属性,使用self关键词
 //echo $this->height;//错。静态方法不能调用非静态属性
//因为 $this代表实例化对象,而这里是类,不知道 $this 代表哪个对象
 }
 public function say(){
 echo self::$name . "我说话了";
 //普通方法调用静态属性,同样使用self关键词
 echo $this->height;
 }
}
$p1 = new Human();
$p1->say();
$p1->tell();//对象可以访问静态方法
echo $p1::$name;//对象访问静态属性。不能这么访问$p1->name
//因为静态属性的内存位置不在对象里
Human::say();//错。say()方法有$this时出错;没有$this时能出结果
//但php5.4以上会提示
?>

静态属性

静态属性也就是说它的值保持其值,比如在类中实例化了N个对象,那么你可以在构造函数中定义一个静态属性来记住对象的个数。类中的静态属性和静态变量差不多,只不过在类中似乎又多了一个些使用上的限制罢了。让我们看看一般的变量吧:

 代码如下 复制代码

1.<?php  
2.function test() {  
3. $n = 1;  
4. echo "The number is:$n<br />";  
5. $n++;  
6.}  
7.test();  
8.test();  
9.test();  
10.test();  
11.test();  
12.test();  
13.?> 

很显然这个函数的结果如下:

The number is:1

The number is:1

The number is:1

The number is:1

The number is:1

The number is:1

但是如果你的程序是这样:

 代码如下 复制代码
1.<?php  
2.function test() {  
3. static $n = 1;  
4. echo "The number is:$n<br />";  
5. $n++;  
6.}  
7.test();  
8.test();  
9.test();  
10.test();  
11.test();  
12.test();  
13.?> 

我们只不过在变量名加了个static关键字而已,结果就大大的不同了:

The number is:1

The number is:2

The number is:3

The number is:4

The number is:5

The number is:6

1.static关键字可以用来修饰变量、方法(静态方法)

2.不经过实例化,就可以直接访问类中static的属性和static的方法。

3.static 的属性和方法,只能访问static的属性和方法,不能访问非静态的属性和方法。因为静态属性和方法被创建时,可能还没有任何这个类的实例可以被调用。

4.在当前类中如果要访问静态成员可以使用self::关键字进行访问。

5.在类中我们不能使用this关键来访问静态属性,因为静态属性在对象可能还没有实例化之前已经存在。

6.在类中静态方法访问静态属性,使用类名::静态属性名即可调用类中的静态属性。

 
静态方法

 代码如下 复制代码
1.<?php  
2.class test {  
3. private static $money = 2000;  
4. public static function getonemon() {  
5.  return test :: $money;  
6. }  
7. public static function gettwomon() {  
8.  self :: $money = self :: $money -1500;  
9.  return self :: $money;  
10. }  
11.}  
12.echo "我现在余额为:";  
13.echo test :: getonemon();  
14.echo "<br>";  
15.echo "消费后,我的余额为:";  
16.echo test :: gettwomon();  
17.?> 

 
在这个示例里我们看到,使用了两种方法来访问静态属性$money的值:一种是前面都提到的类名::属性值的形式,另外一种则是使用了self关键字。当然推荐使用self关键字这种方式,因为如果那天不高兴了,我们修改的类名,那么如果你使用了第一种方式,你是不是还得修改下调用它的方法呢,当然你得在同一个类中,如果你是在子类中想调用父类的静态属性和方法,那就得使用parent::的方式了。

再说一下

1:如果你想在静态方法中调用其它静态方法时,请使用方法是:类名::方法名的形式进行调用,还是那句,如果你在同一个类进行这样的调用,就使用selft关键字进行调用吧。要不然你得的程序可就得报错了。

2:php中静态方法不能调用非静态的属性和非静态方法,也不能使用类名::或者self::调用非静态属性。更不能使用$this->属性名来调用,总之在静态方法中只能调用静态属性或方法,非静态的无法调用。


结论:

(1)、静态属性不需要实例化即可调用。因为静态属性存放的位置是在类里,调用方法为"类名::属性名";
(2)、静态方法不需要实例化即可调用。同上
(3)、静态方法不能调用非静态属性。因为非静态属性需要实例化后,存放在对象里;
(4)、静态方法可以调用非静态方法,使用 self 关键词。php里,一个方法被self:: 后,它就自动转变为静态方法;(会提示strict)

PHP 堆与堆排序这种做法小编开发应用中也没用到多少了,这里来给各位整理一篇关于PHP 堆与堆排序文章,希望对各位有用。

堆排序

堆排序是利用堆的性质进行的一种选择排序。下面先讨论一下堆。

1.堆

  堆实际上是一棵完全二叉树,其任何一非叶节点满足性质:

  Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]

  即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。

  堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。

2.堆排序的思想

   利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。

    其基本思想为(大顶堆):

    1)将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无须区;

    2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n];

    3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

 操作过程如下:

     1)初始化堆:将R[1..n]构造为堆;

     2)将当前无序区的堆顶元素R[1]同该区间的最后一个记录交换,然后将新的无序区调整为新的堆。

    因此对于堆排序,最重要的两个操作就是构造初始堆和调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对所有的非叶节点都进行调整。


堆排序与快速排序,归并排序一样都是时间复杂度为O(N*logN)的几种常见排序方法。学习堆排序前,先讲解下什么是数据结构中的二叉堆。

PHP 堆管理代码如下:

 代码如下 复制代码

<?php
class heep{
 static function add(&$arr,$one){
  $arr[] = $one;
  self::up($arr,count($arr) -1);
 }
 // 下沉
 static function del(&$arr){
  $arr[0] = array_pop($arr);
  self::down($arr,0,count($arr)-1);
 }
 static function swap(&$arr,$i,$p){
  $tmp = $arr[$i];
  $arr[$i] = $arr[$p];
  $arr[$p] = $tmp;
 }
 // 增加元素 上浮
 static function up(&$arr,$i){
  $p = floor(($i-1)/2);
  while( $p >= 0 && $i > 0 && $arr[$p] > $arr[$i] ){
   self::swap($arr,$i,$p);
   $i = $p;
   $p = floor(($i-1)/2);
  }
 }
 // 下沉 $i开始 $n结束
 static function down(&$arr,$i,$n){
  $l = 2*$i + 1;
  while( $l <= $n ){
   if( $l+1 <= $n && $arr[$l+1]<$arr[$l]) $l ++;
   if( $arr[$l] > $arr[$i] ) break;
   self::swap($arr,$i,$l);
   $i = $l;
   $l = 2*$i + 1;
  }
 }
 // 将数组变成堆
 static function make(&$arr){ 
  $n = count( $arr )-1;
     for ($i = $n / 2 - 1; $i >= 0; $i--) 
         self::down($arr,$i,$n);
 }
 // 将堆进行排序
 static function sort(&$arr){ 
  $n = count( $arr )-1;
     for ( $i=$n; $i >= 0; $i--){
      self::swap($arr,0,$i);
         self::down($arr,0,$i-1);
     }
 }
}

$arr = [10,40,30];
$arr = array();

heep::add($arr,40);
heep::add($arr,10);
heep::add($arr,30);
heep::add($arr,15);
heep::add($arr,8);
heep::add($arr,50);
heep::add($arr,20);
heep::add($arr,3);

echo join(',',$arr),'<br>';

heep::del($arr);
heep::del($arr);
heep::del($arr);
echo join(',',$arr),'<br>';

heep::sort($arr);
echo join(',',$arr),'<br>';

$arr = [40,10,30];
heep::make($arr);
echo join(',',$arr),'<br>';

假设n为当前数组的key则,n的父节点为 n>>1 或者 n/2(整除);n的左子节点l= n<<1 或 l=n*2,n的右子节点r=(n<<1)+1 或 r=l+1

 代码如下 复制代码

$arr=array(1,8,7,2,3,4,6,5,9);
数组$arr的原形态结构如下:
1
/
8 7
/ /
2 3 4 6
/
5 9
heapsort($arr);print_r($arr);
排序后生成标准的小顶堆结构如下:
1
/
2 3
/ /
4 5 6 7
/
8 9
既数组:array(1,2,3,4,5,6,7,8,9):
代码如下:
function heapsort(&$arr)
{
//求最后一个元素位
$last=count($arr);
//堆排序中通常忽略$arr[0]
array_unshift($arr,0);
//最后一个非叶子节点
$i=$last>>1;

//整理成大顶堆,最大的数整到堆顶,并将最大数和堆尾交换,并在之后的计算中忽略数组后端的最大数(last),直到堆顶(last=堆顶)
while(true)
{
adjustnode($i,$last,$arr);
if($i>1)
{
//移动节点指针,遍历所有非叶子节点
$i--;
}
else
{
//临界点last=1,既所有排序完成
if($last==1)break;
//当i为1时表示每一次的堆整理都将得到最大数(堆顶,$arr[1]),重复在根节点调整堆
swap($arr[$last],$arr[1]);
//在数组尾部按大小顺序保留最大数,定义临界点last,以免整理堆时重新打乱数组后面已排序好的元素
$last--;
}
}
//弹出第一个数组元素
array_shift($arr);
}

//整理当前树节点($n),临界点$last之后为已排序好的元素
function adjustnode($n,$last,&$arr)
{
$l=$n<<1; //$n的左孩子位
if(!isset($arr[$l])||$l>$last) return ;
$r=$l+1; //$n的右孩子位

//如果右孩子比左孩子大,则让父节点的右孩子比
if($r<=$last&&$arr[$r]>$arr[$l]) $l=$r;
//如果其中子节点$l比父节点$n大,则与父节点$n交换
if($arr[$l]>$arr[$n])
{
//子节点($l)的值与父节点($n)的值交换
swap($arr[$l],$arr[$n]);
//交换后父节点($n)的值($arr[$n])可能还小于原子节点($l)的子节点的值,所以还需对原子节点($l)的子节点进行调整,用递归实现
adjustnode($l,$last,$arr);
}
}

//交换两个值
function swap(&$a,&$b)
{
$a=$a ^ $b;
$b=$a ^ $b;
$a=$a ^ $b;
}

JSON格式的数据我们用到比较多但是日志形式存储用到不多,不过我们这里有一个关于PHP记录和读取JSON格式日志文件例子,下面来看看。

我们有时需要记录用户或者后端的某个操作事件的运行情况,可以使用后端语言如PHP将操作结果记录到日志文件中,方便测试和查找问题。尤其是这些在后端运行的而前端不能直接看到运行结果的,那么就可以用日志文件记录下来,如果你经常跟一些接口开发如支付宝接口、微信卡券接口打交道的话,日志记录就必不可少了。


我们讲的PHP记录日志,就是将日志信息写入到一个日志文件中,区别于内存日志。写入日志的流程是:打开日志文件(如果不存在则新创建),然后将日志内容追加到日志文件的后面,最后关闭日志文件。
本文中,我们将日志内容以json个格式保存,方便必要时直接读取。

PHP写日志文件

PHP写日志文件需要打开、写入和关闭文件等操作,PHP有fopen(),fwrite()和fclose()三个函数与之对应,而另一个函数file_put_contents()它也能字符串写入文件,其实这个函数实现了依次调用 fopen(),fwrite() 以及 fclose()。所以我们使用file_put_contents()非常简洁。值得注意的是,往文件后面追加内容时需要带上参数:FILE_APPEND。
实际运行中,我们有可能会遇到日志文件超大的情况,所以我们设置一个最大值,当日志文件大小超过这个最大值时,将此日志文件备份好,然后重新生成一个新的日志文件来记录新的日志内容。

在写日志前,我们将日志内容进行json格式化,所以需要将内容转化成JSON格式,然后写入文件。当然你也可以不用json,或者换作别的工具程序(如日志分析工具)可以阅读的格式。总之,我们写入的内容是方便必要时可以方便读取。

    function writeLog($filename,$msg){
        $res = array();
        $res['msg'] = $msg;
        $res['logtime'] = date("Y-m-d H:i:s",time());
 
        //如果日志文件超过了指定大小则备份日志文件
        if(file_exists($filename) && (abs(filesize($filename)) > 1024000)){
            $newfilename = dirname($filename).'/'.time().'-'.basename($filename);
            rename($filename, $newfilename);
        }
 
        //如果是新建的日志文件,去掉内容中的第一个字符逗号
        if(file_exists($filename) && abs(filesize($filename))>0){
            $content = ",".json_encode($res);
        }else{
            $content = json_encode($res);
        }
 
        //往日志文件内容后面追加日志内容
        file_put_contents($filename, $content, FILE_APPEND);
    }
PHP读日志文件
必要时,我们会读取日志内容进行分析,同样我们使用PHP的file_get_contents()函数,直接将内容读取,并且转换成json格式,方便调用。
    function readLog($filename){
        if(file_exists($filename)){
            $content = file_get_contents($filename);
            $json = json_decode('['.$content.']',true);
        }else{
            $json = '{"msg":"The file does not exist."}';
        }
        return $json;
    }

日志写入和读取类

写入和读取日志的功能我们经常要用到,所以我将写入和读取功能整理成类,方便调用。
<?php 
/*
 * 日志类
 * 每天生成一个日志文件,当文件超过指定大小则备份日志文件并重新生成新的日志文件
*/
class Log {
 
    private $maxsize = 1024000; //最大文件大小1M
    
    //写入日志
    public function writeLog($filename,$msg){
        $res = array();
        $res['msg'] = $msg;
        $res['logtime'] = date("Y-m-d H:i:s",time());
 
        //如果日志文件超过了指定大小则备份日志文件
        if(file_exists($filename) && (abs(filesize($filename)) > $this->maxsize)){
            $newfilename = dirname($filename).'/'.time().'-'.basename($filename);
            rename($filename, $newfilename);
        }
 
        //如果是新建的日志文件,去掉内容中的第一个字符逗号
        if(file_exists($filename) && abs(filesize($filename))>0){
            $content = ",".json_encode($res);
        }else{
            $content = json_encode($res);
        }
 
        //往日志文件内容后面追加日志内容
        file_put_contents($filename, $content, FILE_APPEND);
    }
 
 
    //读取日志
    public function readLog($filename){
        if(file_exists($filename)){
            $content = file_get_contents($filename);
            $json = json_decode('['.$content.']',true);
        }else{
            $json = '{"msg":"The file does not exist."}';
        }
        return $json;
    }
}
 ?>


使用方法:

$filename = "logs/log_".date("Ymd",time()).".txt";
$msg = '写入了日志';
$Log = new Log(); //实例化
$Log->writeLog($filename,$msg); //写入日志
$loglist = $Log->readLog($filename); //读取日志

下面我们来看一篇关于PHP7 变量在内部的实现详解,希望文章能够让各位理解到php 7变量与老版本有什么区别吧,具体的如下。


要理解本文,你应该对 PHP5 中变量的实现有了一些了解,本文重点在于解释 PHP7 中 zval 的变化。
由于大量的细节描述,本文将会分成两个部分:第一部分主要描述 zval(zend value) 的实现在 PHP5 和 PHP7 中有何不同以及引用的实现。第二部分将会分析单独类型(strings、objects)的细节。
PHP5 中的 zval

PHP5 中 zval 结构体定义如下:
typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;
如上,zval 包含一个 value、一个 type 以及两个 __gc 后缀的字段。value 是个联合体,用于存储不同类型的值:
typedef union _zvalue_value {
    long lval;                 // 用于 bool 类型、整型和资源类型
    double dval;               // 用于浮点类型
    struct {                   // 用于字符串
        char *val;
        int len;
    } str;
    HashTable *ht;             // 用于数组
    zend_object_value obj;     // 用于对象
    zend_ast *ast;             // 用于常量表达式(PHP5.6 才有)
} zvalue_value;
C 语言联合体的特征是一次只有一个成员是有效的并且分配的内存与需要内存最多的成员匹配(也要考虑内存对齐)。所有成员都存储在内存的同一个位置,根据需要存储不同的值。当你需要 lval 的时候,它存储的是有符号整形,需要 dval 时,会存储双精度浮点数。
需要指出的是是联合体中当前存储的数据类型会记录到 type 字段,用一个整型来标记:
#define IS_NULL     0      /* Doesn't use value */
#define IS_LONG     1      /* Uses lval */
#define IS_DOUBLE   2      /* Uses dval */
#define IS_BOOL     3      /* Uses lval with values 0 and 1 */
#define IS_ARRAY    4      /* Uses ht */
#define IS_OBJECT   5      /* Uses obj */
#define IS_STRING   6      /* Uses str */
#define IS_RESOURCE 7      /* Uses lval, which is the resource ID */

/* Special types used for late-binding of constants */
#define IS_CONSTANT 8
#define IS_CONSTANT_AST 9
PHP5 中的引用计数

在PHP5中,zval 的内存是单独从堆(heap)中分配的(有少数例外情况),PHP 需要知道哪些 zval 是正在使用的,哪些是需要释放的。所以这就需要用到引用计数:zval 中 refcount__gc 的值用于保存 zval 本身被引用的次数,比如 $a = $b = 42 语句中,42 被两个变量引用,所以它的引用计数就是 2。如果引用计数变成 0,就意味着这个变量已经没有用了,内存也就可以释放了。
注意这里提及到的引用计数指的不是 PHP 代码中的引用(使用 &),而是变量的使用次数。后面两者需要同时出现时会使用『PHP 引用』和『引用』来区分两个概念,这里先忽略掉 PHP 的部分。
一个和引用计数紧密相关的概念是『写时复制』:对于多个引用来说,zaval 只有在没有变化的情况下才是共享的,一旦其中一个引用改变 zval 的值,就需要复制(”separated”)一份 zval,然后修改复制后的 zval。
下面是一个关于『写时复制』和 zval 的销毁的例子:
$a = 42;   // $a         -> zval_1(type=IS_LONG, value=42, refcount=1)
$b = $a;   // $a, $b     -> zval_1(type=IS_LONG, value=42, refcount=2)
$c = $b;   // $a, $b, $c -> zval_1(type=IS_LONG, value=42, refcount=3)

// 下面几行是关于 zval 分离的
$a += 1;   // $b, $c -> zval_1(type=IS_LONG, value=42, refcount=2)
           // $a     -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($b); // $c -> zval_1(type=IS_LONG, value=42, refcount=1)
           // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($c); // zval_1 is destroyed, because refcount=0
           // $a -> zval_2(type=IS_LONG, value=43, refcount=1)
引用计数有个致命的问题:无法检查并释放循环引用(使用的内存)。为了解决这问题,PHP 使用了循环回收的方法。当一个 zval 的计数减一时,就有可能属于循环的一部分,这时将 zval 写入到『根缓冲区』中。当缓冲区满时,潜在的循环会被打上标记并进行回收。
因为要支持循环回收,实际使用的 zval 的结构实际上如下:
typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;
} zval_gc_info;
zval_gc_info 结构体中嵌入了一个正常的 zval 结构,同时也增加了两个指针参数,但是共属于同一个联合体 u,所以实际使用中只有一个指针是有用的。buffered 指针用于存储 zval 在根缓冲区的引用地址,所以如果在循环回收执行之前 zval 已经被销毁了,这个字段就可能被移除了。next 在回收销毁值的时候使用,这里不会深入。
修改动机

下面说说关于内存使用上的情况,这里说的都是指在 64 位的系统上。首先,由于 str 和 obj 占用的大小一样, zvalue_value 这个联合体占用 16 个字节(bytes)的内存。整个 zval 结构体占用的内存是 24 个字节(考虑到内存对齐),zval_gc_info 的大小是 32 个字节。综上,在堆(相对于栈)分配给 zval 的内存需要额外的 16 个字节,所以每个 zval 在不同的地方一共需要用到 48 个字节(要理解上面的计算方式需要注意每个指针在 64 位的系统上也需要占用 8 个字节)。
在这点上不管从什么方面去考虑都可以认为 zval 的这种设计效率是很低的。比如 zval 在存储整型的时候本身只需要 8 个字节,即使考虑到需要存一些附加信息以及内存对齐,额外 8 个字节应该也是足够的。
在存储整型时本来确实需要 16 个字节,但是实际上还有 16 个字节用于引用计数、16 个字节用于循环回收。所以说 zval 的内存分配和释放都是消耗很大的操作,我们有必要对其进行优化。
从这个角度思考:一个整型数据真的需要存储引用计数、循环回收的信息并且单独在堆上分配内存吗?答案是当然不,这种处理方式一点都不好。
这里总结一下 PHP5 中 zval 实现方式存在的主要问题:
zval 总是单独从堆中分配内存;
zval 总是存储引用计数和循环回收的信息,即使是整型这种可能并不需要此类信息的数据;
在使用对象或者资源时,直接引用会导致两次计数(原因会在下一部分讲);
某些间接访问需要一个更好的处理方式。比如现在访问存储在变量中的对象间接使用了四个指针(指针链的长度为四)。这个问题也放到下一部分讨论;
直接计数也就意味着数值只能在 zval 之间共享。如果想在 zval 和 hashtable key 之间共享一个字符串就不行(除非 hashtable key 也是 zval)。
PHP7 中的 zval

在 PHP7 中 zval 有了新的实现方式。最基础的变化就是 zval 需要的内存不再是单独从堆上分配,不再自己存储引用计数。复杂数据类型(比如字符串、数组和对象)的引用计数由其自身来存储。这种实现方式有以下好处:
简单数据类型不需要单独分配内存,也不需要计数;
不会再有两次计数的情况。在对象中,只有对象自身存储的计数是有效的;
由于现在计数由数值自身存储,所以也就可以和非 zval 结构的数据共享,比如 zval 和 hashtable key 之间;
间接访问需要的指针数减少了。
我们看看现在 zval 结构体的定义(现在在 zend_types.h 文件中):
struct _zval_struct {
    zend_value        value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     var_flags;
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
    } u2;
};
结构体的第一个元素没太大变化,仍然是一个 value 联合体。第二个成员是由一个表示类型信息的整型和一个包含四个字符变量的结构体组成的联合体(可以忽略 ZEND_ENDIAN_LOHI_4 宏,它只是用来解决跨平台大小端问题的)。这个子结构中比较重要的部分是 type(和以前类似)和 type_flags,这个接下来会解释。
上面这个地方也有一点小问题:value 本来应该占 8 个字节,但是由于内存对齐,哪怕只增加一个字节,实际上也是占用 16 个字节(使用一个字节就意味着需要额外的 8 个字节)。但是显然我们并不需要 8 个字节来存储一个 type 字段,所以我们在 u1 的后面增加了了一个名为 u2 的联合体。默认情况下是用不到的,需要使用的时候可以用来存储 4 个字节的数据。这个联合体可以满足不同场景下的需求。
PHP7 中 value 的结构定义如下:
typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;
首先需要注意的是现在 value 联合体需要的内存是 8 个字节而不是 16。它只会直接存储整型(lval)或者浮点型(dval)数据,其他情况下都是指针(上面提到过,指针占用 8 个字节,最下面的结构体由两个 4 字节的无符号整型组成)。上面所有的指针类型(除了特殊标记的)都有一个同样的头(zend_refcounted)用来存储引用计数:
typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;
现在,这个结构体肯定会包含一个存储引用计数的字段。除此之外还有 type、flags 和 gc_info。type 存储的和 zval 中的 type 相同的内容,这样 GC 在不存储 zval 的情况下单独使用引用计数。flags 在不同的数据类型中有不同的用途,这个放到下一部分讲。
gc_info 和 PHP5 中的 buffered 作用相同,不过不再是位于根缓冲区的指针,而是一个索引数字。因为以前根缓冲区的大小是固定的(10000 个元素),所以使用一个 16 位(2 字节)的数字代替 64 位(8 字节)的指针足够了。gc_info 中同样包含一个『颜色』位用于回收时标记结点。
zval 内存管理

上文提到过 zval 需要的内存不再单独从堆上分配。但是显然总要有地方来存储它,所以会存在哪里呢?实际上大多时候它还是位于堆中(所以前文中提到的地方重点不是堆,而是单独分配),只不过是嵌入到其他的数据结构中的,比如 hashtable 和 bucket 现在就会直接有一个 zval 字段而不是指针。所以函数表编译变量和对象属性在存储时会是一个 zval 数组并得到一整块内存而不是散落在各处的 zval 指针。之前的 zval * 现在都变成了 zval。
之前当 zval 在一个新的地方使用时会复制一份 zval * 并增加一次引用计数。现在就直接复制 zval 的值(忽略 u2),某些情况下可能会增加其结构指针指向的引用计数(如果在进行计数)。
那么 PHP 怎么知道 zval 是否正在计数呢?不是所有的数据类型都能知道,因为有些类型(比如字符串或数组)并不是总需要进行引用计数。所以 type_info 字段就是用来记录 zval 是否在进行计数的,这个字段的值有以下几种情况:
#define IS_TYPE_CONSTANT            (1/* special */
#define IS_TYPE_IMMUTABLE           (1/* special */
#define IS_TYPE_REFCOUNTED          (1
#define IS_TYPE_COLLECTABLE         (1
#define IS_TYPE_COPYABLE            (1
#define IS_TYPE_SYMBOLTABLE         (1/* special */
注:在 7.0.0 的正式版本中,上面这一段宏定义的注释这几个宏是供 zval.u1.v.type_flags 使用的。这应该是注释的错误,因为这个上述字段是 zend_uchar 类型。
type_info 的三个主要的属性就是『可计数』(refcounted)、『可回收』(collectable)和『可复制』(copyable)。计数的问题上面已经提过了。『可回收』用于标记 zval 是否参与循环,不如字符串通常是可计数的,但是你却没办法给字符串制造一个循环引用的情况。
是否可复制用于表示在复制时是否需要在复制时制造(原文用的 “duplication” 来表述,用中文表达出来可能不是很好理解)一份一模一样的实体。”duplication” 属于深度复制,比如在复制数组时,不仅仅是简单增加数组的引用计数,而是制造一份全新值一样的数组。但是某些类型(比如对象和资源)即使 “duplication” 也只能是增加引用计数,这种就属于不可复制的类型。这也和对象和资源现有的语义匹配(现有,PHP7 也是这样,不单是 PHP5)。
下面的表格上标明了不同的类型会使用哪些标记(x 标记的都是有的特性)。『简单类型』(simple types)指的是整型或布尔类型这些不使用指针指向一个结构体的类型。下表中也有『不可变』(immutable)的标记,它用来标记不可变数组的,这个在下一部分再详述。
interned string(保留字符)在这之前没有提过,其实就是函数名、变量名等无需计数、不可重复的字符串。
                | refcounted | collectable | copyable | immutable
----------------+------------+-------------+----------+----------
simple types    |            |             |          |
string          |      x     |             |     x    |
interned string |            |             |          |
array           |      x     |      x      |     x    |
immutable array |            |             |          |     x
object          |      x     |      x      |          |
resource        |      x     |             |          |
reference       |      x     |             |          |
要理解这一点,我们可以来看几个例子,这样可以更好的认识 zval 内存管理是怎么工作的。
下面是整数行为模式,在上文中 PHP5 的例子的基础上进行了一些简化 :
$a = 42;   // $a = zval_1(type=IS_LONG, value=42)

$b = $a;   // $a = zval_1(type=IS_LONG, value=42)
           // $b = zval_2(type=IS_LONG, value=42)

$a += 1;   // $a = zval_1(type=IS_LONG, value=43)
           // $b = zval_2(type=IS_LONG, value=42)

unset($a); // $a = zval_1(type=IS_UNDEF)
           // $b = zval_2(type=IS_LONG, value=42)
这个过程其实挺简单的。现在整数不再是共享的,变量直接就会分离成两个单独的 zval,由于现在 zval 是内嵌的所以也不需要单独分配内存,所以这里的注释中使用 = 来表示的而不是指针符号 ->,unset 时变量会被标记为 IS_UNDEF。下面看一下更复杂的情况:
$a = [];   // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

$b = $a;   // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[])
           // $b = zval_2(type=IS_ARRAY) ---^

// zval 分离在这里进行
$a[] = 1   // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1])
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

unset($a); // $a = zval_1(type=IS_UNDEF),   zend_array_2 被销毁
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
这种情况下每个变量变量有一个单独的 zval,但是是指向同一个(有引用计数) zend_array 的结构体。修改其中一个数组的值时才会进行复制。这点和 PHP5 的情况类似。
类型(Types)

我们大概看一下 PHP7 支持哪些类型(zval 使用的类型标记):
/* regular data types */
#define IS_UNDEF                    0
#define IS_NULL                     1
#define IS_FALSE                    2
#define IS_TRUE                     3
#define IS_LONG                     4
#define IS_DOUBLE                   5
#define IS_STRING                   6
#define IS_ARRAY                    7
#define IS_OBJECT                   8
#define IS_RESOURCE                 9
#define IS_REFERENCE                10

/* constant expressions */
#define IS_CONSTANT                 11
#define IS_CONSTANT_AST             12

/* internal types */
#define IS_INDIRECT                 15
#define IS_PTR                      17
这个列表和 PHP5 使用的类似,不过增加了几项:
IS_UNDEF 用来标记之前为 NULL 的 zval 指针(和 IS_NULL 并不冲突)。比如在上面的例子中使用 unset 注销变量;
IS_BOOL 现在分割成了 IS_FALSE 和 IS_TRUE 两项。现在布尔类型的标记是直接记录到 type 中,这么做可以优化类型检查。不过这个变化对用户是透明的,还是只有一个『布尔』类型的数据(PHP 脚本中)。
PHP 引用不再使用 is_ref 来标记,而是使用 IS_REFERENCE 类型。这个也要放到下一部分讲;
IS_INDIRECT 和 IS_PTR 是特殊的内部标记。
实际上上面的列表中应该还存在两个 fake types,这里忽略了。
IS_LONG 类型表示的是一个 zend_long 的值,而不是原生的 C 语言的 long 类型。原因是 Windows 的 64 位系统(LLP64)上的 long 类型只有 32 位的位深度。所以 PHP5 在 Windows 上只能使用 32 位的数字。PHP7 允许你在 64 位的操作系统上使用 64 位的数字,即使是在 Windows 上面也可以。
zend_refcounted 的内容会在下一部分讲。下面看看 PHP 引用的实现。
引用

PHP7 使用了和 PHP5 中完全不同的方法来处理 PHP & 符号引用的问题(这个改动也是 PHP7 开发过程中大量 bug 的根源)。我们先从 PHP5 中 PHP 引用的实现方式说起。
通常情况下, 写时复制原则意味着当你修改一个 zval 之前需要对其进行分离来保证始终修改的只是某一个 PHP 变量的值。这就是传值调用的含义。
但是使用 PHP 引用时这条规则就不适用了。如果一个 PHP 变量是 PHP 引用,就意味着你想要在将多个 PHP 变量指向同一个值。PHP5 中的 is_ref 标记就是用来注明一个 PHP 变量是不是 PHP 引用,在修改时需不需要进行分离的。比如:
$a = [];  // $a     -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b =& $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[])

$b[] = 1; // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[1])
          // 因为 is_ref 的值是 1, 所以 PHP 不会对 zval 进行分离
但是这个设计的一个很大的问题在于它无法在一个 PHP 引用变量和 PHP 非引用变量之间共享同一个值。比如下面这种情况:
$a = [];  // $a         -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b = $a;  // $a, $b     -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
$c = $b   // $a, $b, $c -> zval_1(type=IS_ARRAY, refcount=3, is_ref=0) -> HashTable_1(value=[])

$d =& $c; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
          // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
          // $d 是 $c 的引用, 但却不是 $a 的 $b, 所以这里 zval 还是需要进行复制
          // 这样我们就有了两个 zval, 一个 is_ref 的值是 0, 一个 is_ref 的值是 1.

$d[] = 1; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
          // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1])
          // 因为有两个分离了的 zval, $d[] = 1 的语句就不会修改 $a 和 $b 的值.
这种行为方式也导致在 PHP 中使用引用比普通的值要慢。比如下面这个例子:
$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); //
因为 count() 只接受传值调用,但是 $array 是一个 PHP 引用,所以 count() 在执行之前实际上会有一个对数组进行完整的复制的过程。如果 $array 不是引用,这种情况就不会发生了。
现在我们来看看 PHP7 中 PHP 引用的实现。因为 zval 不再单独分配内存,也就没办法再使用和 PHP5 中相同的实现了。所以增加了一个 IS_REFERENCE 类型,并且专门使用 zend_reference 来存储引用值:
struct _zend_reference {
    zend_refcounted   gc;
    zval              val;
};
本质上 zend_reference 只是增加了引用计数的 zval。所有引用变量都会存储一个 zval 指针并且被标记为 IS_REFERENCE。val 和其他的 zval 的行为一样,尤其是它也可以在共享其所存储的复杂变量的指针,比如数组可以在引用变量和值变量之间共享。
我们还是看例子,这次是 PHP7 中的语义。为了简洁明了这里不再单独写出 zval,只展示它们指向的结构体:
$a = [];  // $a                                     -> zend_array_1(refcount=1, value=[])
$b =& $a; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[])

$b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[1])
上面的例子中进行引用传递时会创建一个 zend_reference,注意它的引用计数是 2(因为有两个变量在使用这个 PHP 引用)。但是值本身的引用计数是 1(因为 zend_reference 只是有一个指针指向它)。下面看看引用和非引用混合的情况:
$a = [];  // $a         -> zend_array_1(refcount=1, value=[])
$b = $a;  // $a, $b,    -> zend_array_1(refcount=2, value=[])
$c = $b   // $a, $b, $c -> zend_array_1(refcount=3, value=[])

$d =& $c; // $a, $b                                 -> zend_array_1(refcount=3, value=[])
          // $c, $d -> zend_reference_1(refcount=2) ---^
          // 注意所有变量共享同一个 zend_array, 即使有的是 PHP 引用有的不是

$d[] = 1; // $a, $b                                 -> zend_array_1(refcount=2, value=[])
          // $c, $d -> zend_reference_1(refcount=2) -> zend_array_2(refcount=1, value=[1])
          // 只有在这时进行赋值的时候才会对 zend_array 进行赋值
这里和 PHP5 最大的不同就是所有的变量都可以共享同一个数组,即使有的是 PHP 引用有的不是。只有当其中某一部分被修改的时候才会对数组进行分离。这也意味着使用 count() 时即使给其传递一个很大的引用数组也是安全的,不会再进行复制。不过引用仍然会比普通的数值慢,因为存在需要为 zend_reference 结构体分配内存(间接)并且引擎本身处理这一块儿也不快的的原因。

 

要理解本文,你应该对 PHP5 中变量的实现有了一些了解,本文重点在于解释 PHP7 中 zval 的变化。
第一部分讲了 PHP5 和 PHP7 中关于变量最基础的实现和变化。这里再重复一下,主要的变化就是 zval 不再单独分配内存,不自己存储引用计数。整型浮点型等简单类型直接存储在 zval 中。复杂类型则通过指针指向一个独立的结构体。
复杂的 zval 数据值有一个共同的头,其结构由 zend_refcounted 定义:
struct _zend_refcounted {
    uint32_t refcount;
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,
                uint16_t      gc_info)
        } v;
        uint32_t type_info;
    } u;
};
这个头存储有 refcount(引用计数),值的类型 type 和循环回收的相关信息 gc_info 以及类型标志位 flags。
接下来会对每种复杂类型的实现单独进行分析并和 PHP5 的实现进行比较。引用虽然也属于复杂类型,但是上一部分已经介绍过了,这里就不再赘述。另外这里也不会讲到资源类型(因为作者觉得资源类型没什么好讲的)。
字符串

PHP7 中定义了一个新的结构体 zend_string 用于存储字符串变量:
struct _zend_string {
    zend_refcounted   gc;
    zend_ulong        h;        /* hash value */
    size_t            len;
    char              val[1];
};
除了引用计数的头以外,字符串还包含哈希缓存 h,字符串长度 len 以及字符串的值 val。哈希缓存的存在是为了防止使用字符串做为 hashtable 的 key 在查找时需要重复计算其哈希值,所以这个在使用之前就对其进行初始化。
如果你对 C 语言了解的不是很深入的话,可能会觉得 val 的定义有些奇怪:这个声明只有一个元素,但是显然我们想存储的字符串偿付肯定大于一个字符的长度。这里其实使用的是结构体的一个『黑』方法:在声明数组时只定义一个元素,但是实际创建 zend_string 时再分配足够的内存来存储整个字符串。这样我们还是可以通过 val 访问完整的字符串。
当然这属于非常规的实现手段,因为我们实际的读和写的内容都超过了单字符数组的边界。但是 C 语言编译器却不知道你是这么做的。虽然 C99 也曾明确规定过支持『柔性数组』,但是感谢我们的好朋友微软,没人能在不同的平台上保证 C99 的一致性(所以这种手段是为了解决 Windows 平台下柔性数组的支持问题)。
新的字符串类型的结构比原生的 C 字符串更方便使用:第一是因为直接存储了字符串的长度,这样就不用每次使用时都去计算。第二是字符串也有引用计数的头,这样也就可以在不同的地方共享字符串本身而无需使用 zval。一个经常使用的地方就是共享 hashtable 的 key。
但是新的字符串类型也有一个很不好的地方:虽然可以很方便的从 zend_string 中取出 C 字符串(使用str->val 即可),但反过来,如果将 C 字符串变成 zend_string 就需要先分配 zend_string 需要的内存,再将字符串复制到 zend_string 中。这在实际使用的过程中并不是很方便。
字符串也有一些特有的标志(存储在 GC 的标志位中的):
#define IS_STR_PERSISTENT           (1/* allocated using malloc */
#define IS_STR_INTERNED             (1/* interned string */
#define IS_STR_PERMANENT            (1/* interned string surviving request boundary */
持久化的字符串需要的内存直接从系统本身分配而不是 zend 内存管理器(ZMM),这样它就可以一直存在而不是只在单次请求中有效。给这种特殊的分配打上标记便于 zval 使用持久化字符串。在 PHP5 中并不是这样处理的,是在使用前复制一份到 ZMM 中。
保留字符(interned strings)有点特殊,它会一直存在直到请求结束时才销毁,所以也就无需进行引用计数。保留字符串也不可重复(duplicate),所以在创建新的保留字符时也会先检查是否有同样字符的已经存在。所有 PHP 源码中不可变的字符串都是保留字符(包括字符串常量、变量名函数名等)。持久化字符串也是请求开始之前已经创建好的保留字符。但普通的保留字符在请求结束后会销毁,持久化字符串却始终存在。
如果使用了 opcache 的话保留字符会被存储在共享内存(SHM)中这样就可以在所有 PHP 进程质检共享。这种情况下持久化字符串也就没有存在的意义了,因为保留字符也是不会被销毁的。
数组

因为之前的文章有讲过新的数组实现,所以这里就不再详细描述了。虽然最近有些变化导致之前的描述不是十分准确了,但是基本的概念还是一致的。
这里要说的是之前的文章中没有提到的数组相关的概念:不可变数组。其本质上和保留字符类似:没有引用计数且在请求结束之前一直存在(也可能在请求结束之后还存在)。
因为某些内存管理方便的原因,不可变数组只会在开启 opcache 时会使用到。我们来看看实际使用的例子,先看以下的脚本:
for ($i = 0; $i  1000000; ++$i) {
    $array[] = ['foo'];
}
var_dump(memory_get_usage());
开启 opcache 时,以上代码会使用 32MB 的内存,不开启的情况下因为 $array 每个元素都会复制一份 ['foo'] ,所以需要 390MB。这里会进行完整的复制而不是增加引用计数值的原因是防止 zend 虚拟机操作符执行的时候出现共享内存出错的情况。我希望不使用 opcache 时内存暴增的问题以后能得到改善。
PHP5 中的对象

在了解 PHP7 中的对象实现直线我们先看一下 PHP5 的并且看一下有什么效率上的问题。PHP5 中的 zval 会存储一个 zend_object_value 结构,其定义如下:
typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;
handle 是对象的唯一 ID,可以用于查找对象数据。handles 是保存对象各种属性方法的虚函数表指针。通常情况下 PHP 对象都有着同样的 handler 表,但是 PHP 扩展创建的对象也可以通过操作符重载等方式对其行为自定义。
对象句柄(handler)是作为索引用于『对象存储』,对象存储本身是一个存储容器(bucket)的数组,bucket 定义如下:
typedef struct _zend_object_store_bucket {
    zend_bool destructor_called;
    zend_bool valid;
    zend_uchar apply_count;
    union _store_bucket {
        struct _store_object {
            void *object;
            zend_objects_store_dtor_t dtor;
            zend_objects_free_object_storage_t free_storage;
            zend_objects_store_clone_t clone;
            const zend_object_handlers *handlers;
            zend_uint refcount;
            gc_root_buffer *buffered;
        } obj;
        struct {
            int next;
        } free_list;
    } bucket;
} zend_object_store_bucket;
这个结构体包含了很多东西。前三个成员只是些普通的元数据(对象的析构函数是否被调用过、bucke 是否被使用过以及对象被递归调用过多少次)。接下来的联合体用于区分 bucket 是处于使用中的状态还是空闲状态。上面的结构中最重要的是 struct _store_object 子结构体:
第一个成员 object 是指向实际对象(也就是对象最终存储的位置)的指针。对象实际并不是直接嵌入到对象存储的 bucket 中的,因为对象不是定长的。对象指针下面是三个用于管理对象销毁、释放与克隆的操作句柄(handler)。这里要注意的是 PHP 销毁和释放对象是不同的步骤,前者在某些情况下有可能会被跳过(不完全释放)。克隆操作实际上几乎几乎不会被用到,因为这里包含的操作不是普通对象本身的一部分,所以(任何时候)他们在每个对象中他们都会被单独复制(duplicate)一份而不是共享。
这些对象存储操作句柄后面是一个普通的对象 handlers 指针。存储这几个数据是因为有时候可能会在 zval 未知的情况下销毁对象(通常情况下这些操作都是针对 zval 进行的)。
bucket 也包含了 refcount 的字段,不过这种行为在 PHP5 中显得有些奇怪,因为 zval 本身已经存储了引用计数。为什么还需要一个多余的计数呢?问题在于虽然通常情况下 zval 的『复制』行为都是简单的增加引用计数即可,但是偶尔也会有深度复制的情况出现,比如创建一个全新的 zval 但是保存同样的 zend_object_value。这种情况下两个不同的 zval 就用到了同一个对象存储的 bucket,所以 bucket 自身也需要进行引用计数。这种『双重计数』的方式是 PHP5 的实现内在的问题。GC 根缓冲区中的buffered 指针也是由于同样的原因才需要进行完全复制(duplicate)。
现在看看对象存储中指针指向的实际的 object 的结构,通常情况下用户层面的对象定义如下:
typedef struct _zend_object {
    zend_class_entry *ce;
    HashTable *properties;
    zval **properties_table;
    HashTable *guards;
} zend_object;
zend_class_entry 指针指向的是对象实现的类原型。接下来的两个元素是使用不同的方式存储对象属性。动态属性(运行时添加的而不是在类中定义的)全部存在 properties 中,不过只是属性名和值的简单匹配。
不过这里有针对已经声明的属性的一个优化:编译期间每个属性都会被指定一个索引并且属性本身是存储在 properties_table 的索引中。属性名称和索引的匹配存储在类原型的 hashtable 中。这样就可以防止每个对象使用的内存超过 hashtable 的上限,并且属性的索引会在运行时有多处缓存。
guards 的哈希表是用于实现魔术方法的递归行为的,比如 __get,这里我们不深入讨论。
除了上文提到过的双重计数的问题,这种实现还有一个问题是一个最小的只有一个属性的对象也需要 136 个字节的内存(这还不算 zval 需要的内存)。而且中间存在很多间接访问动作:比如要从对象 zval 中取出一个元素,先需要取出对象存储 bucket,然后是 zend object,然后才能通过指针找到对象属性表和 zval。这样这里至少就有 4 层间接访问(并且实际使用中可能最少需要七层)。
PHP7 中的对象

PHP7 的实现中试图解决上面这些问题,包括去掉双重引用计数、减少内存使用以及间接访问。新的 zend_object 结构体如下:
struct _zend_object {
    zend_refcounted   gc;
    uint32_t          handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};
可以看到现在这个结构体几乎就是一个对象的全部内容了:zend_object_value 已经被替换成一个直接指向对象和对象存储的指针,虽然没有完全移除,但已经是很大的提升了。
除了 PHP7 中惯用的 zend_refcounted 头以外,handle 和 对象的 handlers 现在也被放到了 zend_object 中。这里的 properties_table 同样用到了 C 结构体的小技巧,这样 zend_object 和属性表就会得到一整块内存。当然,现在属性表是直接嵌入到 zval 中的而不是指针。
现在对象结构体中没有了 guards 表,现在如果需要的话这个字段的值会被存储在 properties_table 的第一位中,也就是使用 __get 等方法的时候。不过如果没有使用魔术方法的话,guards 表会被省略。
dtor、free_storage 和 clone 三个操作句柄之前是存储在对象操作 bucket 中,现在直接存在handlers 表中,其结构体定义如下:
struct _zend_object_handlers {
    /* offset of real object header (usually zero) */
    int                                     offset;
    /* general object functions */
    zend_object_free_obj_t                  free_obj;
    zend_object_dtor_obj_t                  dtor_obj;
    zend_object_clone_obj_t                 clone_obj;
    /* individual object functions */
    // ... rest is about the same in PHP 5
};
handler 表的第一个成员是 offset,很显然这不是一个操作句柄。这个 offset 是现在的实现中必须存在的,因为虽然内部的对象总是嵌入到标准的 zend_object 中,但是也总会有添加一些成员进去的需求。在 PHP5 中解决这个问题的方法是添加一些内容到标准的对象后面:
struct custom_object {
    zend_object std;
    uint32_t something;
    // ...
};
这样如果你可以轻易的将 zend_object* 添加到 struct custom_object* 中。这也是 C 语言中常用的结构体继承的做法。但是在 PHP7 中这种实现会有一个问题:因为 zend_object 在存储属性表时用了结构体 hack 的技巧,zend_object 尾部存储的 PHP 属性会覆盖掉后续添加进去的内部成员。所以 PHP7 的实现中会把自己添加的成员添加到标准对象结构的前面:
struct custom_object {
    uint32_t something;
    // ...
    zend_object std;
};
不过这样也就意味着现在无法直接在 zend_object* 和 struct custom_object* 进行简单的转换了,因为两者都一个偏移分割开了。所以这个偏移量就需要被存储在对象 handler 表中的第一个元素中,这样在编译时通过 offsetof() 宏就能确定具体的偏移值。
也许你会好奇既然现在已经直接(在 zend_value 中)存储了 zend_object 的指针,那现在就不需要再到对象存储中去查找对象了,为什么 PHP7 的对象者还保留着 handle 字段呢?
这是因为现在对象存储仍然存在,虽然得到了极大的简化,所以保留 handle 仍然是有必要的。现在它只是一个指向对象的指针数组。当对象被创建时,会有一个指针插入到对象存储中并且其索引会保存在handle 中,当对象被释放时,索引也会被移除。
那么为什么现在还需要对象存储呢?因为在请求结束的阶段会在存在某个节点,在这之后再去执行用户代码并且取指针数据时就不安全了。为了避免这种情况出现 PHP 会在更早的节点上执行所有对象的析构函数并且之后就不再有此类操作,所以就需要一个活跃对象的列表。
并且 handle 对于调试也是很有用的,它让每个对象都有了一个唯一的 ID,这样就很容易区分两个对象是同一个还是只是有相同的内容。虽然 HHVM 没有对象存储的概念,但它也存了对象的 handle。
和 PHP5 相比,现在的实现中只有一个引用计数(zval 自身不计数),并且内存的使用量有了很大的缩减:40 个字节用于基础对象,每个属性需要 16 个字节,并且这还是算了 zval 之后的。间接访问的情况也有了显著的改善,因为现在中间层的结构体要么被去掉了,要么就是直接嵌入的,所以现在读取一个属性只有一层访问而不再是四层。
间接 zval

到现在我们已经基本提到过了所有正常的 zval 类型,但是也有一对特殊类型用于某些特定的情况的,其中之一就是 PHP7 新添加的 IS_INDIRECT。
间接 zval 指的就是其真正的值是存储在其他地方的。注意这个 IS_REFERENCE 类型是不同的,间接 zval 是直接指向另外一个 zval 而不是像 zend_reference 结构体一样嵌入 zval。
为了理解在什么时候会出现这种情况,我们来看一下 PHP 中变量的实现(实际上对象属性的存储也是一样的情况)。
所有在编译过程中已知的变量都会被指定一个索引并且其值会被存在编译变量(CV)表的相应位置中。但是 PHP 也允许你动态的引用变量,不管是局部变量还是全局变量(比如 $GLOBALS),只要出现这种情况,PHP 就会为脚本或者函数创建一个符号表,这其中包含了变量名和它们的值之间的映射关系。
但是问题在于:怎么样才能实现两个表的同时访问呢?我们需要在 CV 表中能够访问普通变量,也需要能在符号表中访问编译变量。在 PHP5 中 CV 表用了双重指针 zval**,通常这些指针指向中间的 zval* 的表,zval* 最终指向的才是实际的 zval:
+------ CV_ptr_ptr[0]
| +---- CV_ptr_ptr[1]
| | +-- CV_ptr_ptr[2]
| | |
| | +-> CV_ptr[0] --> some zval
| +---> CV_ptr[1] --> some zval
+-----> CV_ptr[2] --> some zval
当需要使用符号表时存储 zval* 的中间表其实是没有用到的而 zval** 指针会被更新到 hashtable buckets 的响应位置中。我们假定有 $a、$b 和 $c 三个变量,下面是简单的示意图:
CV_ptr_ptr[0] --> SymbolTable["a"].pDataPtr --> some zval
CV_ptr_ptr[1] --> SymbolTable["b"].pDataPtr --> some zval
CV_ptr_ptr[2] --> SymbolTable["c"].pDataPtr --> some zval
但是 PHP7 的用法中已经没有这个问题了,因为 PHP7 中的 hashtable 大小发生变化时 hashtable bucket 就失效了。所以 PHP7 用了一个相反的策略:为了访问 CV 表中存储的变量,符号表中存储 INDIRECT 来指向 CV 表。CV 表在符号表的生命周期内不会重新分配,所以也就不会存在有无效指针的问题了。
所以加入你有一个函数并且在 CV 表中有 $a、$b 和 $c,同时还有一个动态分配的变量 $d,符号表的结构看起来大概就是这个样子:
SymbolTable["a"].value = INDIRECT --> CV[0] = LONG 42
SymbolTable["b"].value = INDIRECT --> CV[1] = DOUBLE 42.0
SymbolTable["c"].value = INDIRECT --> CV[2] = STRING --> zend_string("42")
SymbolTable["d"].value = ARRAY --> zend_array([4, 2])
间接 zval 也可以是一个指向 IS_UNDEF 类型 zval 的指针,当 hashtable 没有和它关联的 key 时就会出现这种情况。所以当使用 unset($a) 将 CV[0] 的类型标记为 UNDEF 时就会判定符号表不存在键值为 a 的数据。
常量和 AST

还有两个需要说一下的在 PHP5 和 PHP7 中都存在的特殊类型 IS_CONSTANT 和 IS_CONSTANT_AST。要了解他们我们还是先看以下的例子:
function test($a = ANSWER,
              $b = ANSWER * ANSWER) {
    return $a + $b;
}

define('ANSWER', 42);
var_dump(test()); // int(42 + 42 * 42)·
test() 函数的两个参数的默认值都是由常量 ANSWER构成,但是函数声明时常量的值尚未定义。常量的具体值只有通过 define() 定义时才知道。
由于以上问题的存在,参数和属性的默认值、常量以及其他接受『静态表达式』的东西都支持『延时绑定』直到首次使用时。
常量(或者类的静态属性)这些需要『延时绑定』的数据就是最常需要用到 IS_CONSTANT 类型 zval 的地方。如果这个值是表达式,就会使用 IS_CONSTANT_AST 类型的 zval 指向表达式的抽象语法树(AST)。
到这里我们就结束了对 PHP7 中变量实现的分析。后面我可能还会写两篇文章来介绍一些虚拟机优化、新的命名约定以及一些编译器基础结构的优化的内容(这是作者原话)。

下面我们来看一篇关于PHP7错误处理机制详解介绍,对于php7新特性我们有介绍过不小的教程,希望文章能够帮助到各位朋友。

HP7实现了一个全局的throwable接口,原来的Exception和部分Error都实现了这个接口(interface), 以接口的方式定义了异常的继承结构。于是,PHP7中更多的Error变为可捕获的Exception返回给开发者,如果不进行捕获则为Error,如果捕获就变为一个可在程序内处理的Exception。这些可被捕获的Error通常都是不会对程序造成致命伤害的Error,例如函数不存。

一、现在有两个异常类:Exception and Error.

PHP7现在有两个异常类,Exception and Error。这两个类都实现了一个新的接口:Throwable。在您的异常处理代码中,类型暗示可能需要调整下。

<?php

try {
    not_exists_func();
} catch (EngineException $e) {
    var_dump($e->getMessage());
}

output:
string(44) "Call to undefined function not_exists_func()"

二、一些致命错误和可恢复致命错误改为抛出Error对象。

有一些致命错误和可恢复致命错误现在改为报出Error对象。Error对象是和Exception独立的,它们无法被常规的try/catch扑获。编者按:需要注册错误处理函数,请参考下面的RFC。
对于这些已经转为异常的可恢复致命错误,已经无法通过error handler静默的忽略掉。尤其是无法忽略类型暗示错误。

三、语法错误会抛出一个ParseError对象

语法错误会抛出一个ParseError对象,该对象继承自Error对象。之前处理eval()的时候,对于潜在可能错误的代码除了检查返回值或者error_get_last()之外,还应该捕获ParseError对象。

四、内部对象的构造方法如果失败的时候总会抛出异常

内部对象的构造方法如果失败的时候总会报出异常。之前的有一些构造方法会返回NULL或者一个无法使用的对象。

五、一些E_STRICT错误的级别调整了。

[!--infotagslink--]

相关文章

  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • php 中file_get_contents超时问题的解决方法

    file_get_contents超时我知道最多的原因就是你机器访问远程机器过慢,导致php脚本超时了,但也有其它很多原因,下面我来总结file_get_contents超时问题的解决方法总结。...2016-11-25
  • HTTP 408错误是什么 HTTP 408错误解决方法

    相信很多站长都遇到过这样一个问题,访问页面时出现408错误,下面一聚教程网将为大家介绍408错误出现的原因以及408错误的解决办法。 HTTP 408错误出现原因: HTT...2017-01-22
  • php抓取网站图片并保存的实现方法

    php如何实现抓取网页图片,相较于手动的粘贴复制,使用小程序要方便快捷多了,喜欢编程的人总会喜欢制作一些简单有用的小软件,最近就参考了网上一个php抓取图片代码,封装了一个php远程抓取图片的类,测试了一下,效果还不错分享...2015-10-30
  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • ps把文字背景变透明的操作方法

    ps软件是现在非常受大家喜欢的一款软件,有着非常不错的使用功能。这次文章就给大家介绍下ps把文字背景变透明的操作方法,喜欢的一起来看看。 1、使用Photoshop软件...2017-07-06
  • intellij idea快速查看当前类中的所有方法(推荐)

    这篇文章主要介绍了intellij idea快速查看当前类中的所有方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-09-02
  • Mysql select语句设置默认值的方法

    1.在没有设置默认值的情况下: 复制代码 代码如下:SELECT userinfo.id, user_name, role, adm_regionid, region_name , create_timeFROM userinfoLEFT JOIN region ON userinfo.adm_regionid = region.id 结果:...2014-05-31
  • js导出table数据到excel即导出为EXCEL文档的方法

    复制代码 代码如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta ht...2013-10-13
  • mysql 批量更新与批量更新多条记录的不同值实现方法

    批量更新mysql更新语句很简单,更新一条数据的某个字段,一般这样写:复制代码 代码如下:UPDATE mytable SET myfield = 'value' WHERE other_field = 'other_value';如果更新同一字段为同一个值,mysql也很简单,修改下where即...2013-10-04
  • js基础知识(公有方法、私有方法、特权方法)

    本文涉及的主题虽然很基础,在许多人看来属于小伎俩,但在JavaScript基础知识中属于一个综合性的话题。这里会涉及到对象属性的封装、原型、构造函数、闭包以及立即执行表达式等知识。公有方法 公有方法就是能被外部访问...2015-11-08
  • ps怎么制作倒影 ps设计倒影的方法

    ps软件是一款非常不错的图片处理软件,有着非常不错的使用效果。这次文章要给大家介绍的是ps怎么制作倒影,一起来看看设计倒影的方法。 用ps怎么做倒影最终效果&#819...2017-07-06
  • PHP 验证码不显示只有一个小红叉的解决方法

    最近想自学PHP ,做了个验证码,但不知道怎么搞的,总出现一个如下图的小红叉,但验证码就是显示不出来,原因如下 未修改之前,出现如下错误; (1)修改步骤如下,原因如下,原因是apache权限没开, (2)点击打开php.int., 搜索extension=ph...2013-10-04
  • c#中分割字符串的几种方法

    单个字符分割 string s="abcdeabcdeabcde"; string[] sArray=s.Split('c'); foreach(string i in sArray) Console.WriteLine(i.ToString()); 输出下面的结果: ab de...2020-06-25
  • js修改input的type属性问题探讨

    js修改input的type属性有些限制。当input元素还未插入文档流之前,是可以修改它的值的,在ie和ff下都没问题。但如果input已经存在于页面,其type属性在ie下就成了只读属性了,不可以修改。...2013-10-19
  • 安卓手机wifi打不开修复教程,安卓手机wifi打不开解决方法

    手机wifi打不开?让小编来告诉你如何解决。还不知道的朋友快来看看。 手机wifi是现在生活中最常用的手机功能,但是遇到手机wifi打不开的情况该怎么办呢?如果手机wifi...2016-12-21
  • js控制页面控件隐藏显示的两种方法介绍

    javascript控制页面控件隐藏显示的两种方法,方法的不同之处在于控件隐藏后是否还在页面上占位 方法一: 复制代码 代码如下: document.all["panelsms"].style.visibility="hidden"; document.all["panelsms"].style.visi...2013-10-13
  • 连接MySql速度慢的解决方法(skip-name-resolve)

    最近在Linux服务器上安装MySql5后,本地使用客户端连MySql速度超慢,本地程序连接也超慢。 解决方法:在配置文件my.cnf的[mysqld]下加入skip-name-resolve。原因是默认安装的MySql开启了DNS的反向解析。如果禁用的话就不能...2015-10-21
  • C#方法的总结详解

    本篇文章是对C#方法进行了详细的总结与介绍,需要的朋友参考下...2020-06-25
  • Zend studio文件注释模板设置方法

    步骤:Window -> PHP -> Editor -> Templates,这里可以设置(增、删、改、导入等)管理你的模板。新建文件注释、函数注释、代码块等模板的实例新建模板,分别输入Name、Description、Patterna)文件注释Name: 3cfileDescriptio...2013-10-04