对PHP变量的实现方式以及内存管理的梳理

Posted by onceme on Sunday, January 20, 2019

TOC

对PHP变量的实现方式以及内存管理的梳理

变量

  • 局部变量 PHP中局部变量分配在zend_execute_data结构上,每次执行zend_op_array都会生成一个新的zend_execute_data 局部变量通过编译时确定的编号进行读写操作

  • 静态变量 静态变量只会在编译时初始化,保存在zend_op_array->static_variables 这个哈希表中 静态变量通过哈希表保存,这就使得能像普通变量那样有一个固定的编号 编译时先判断zend_op_array->static_variables 是否已创建,然后将静态变量插入哈希表

    //zend_compile_static_var_common():
    if (!CG(active_op_array)->static_variables) {
    ALLOC_HASHTABLE(CG(active_op_array)->static_variables);
    zend_hash_init(CG(active_op_array)->static_variables, 8, NULL, ZVAL_P
    TR_DTOR, 0);
    }
    //插入静态变量
    zend_hash_update(CG(active_op_array)->static_variables, Z_STR(var_node.u.
    constant), value);
    

    下面的代码生成了两条opcode: * ZEND_FETCH_W: 这条opcode对应的操作是创建一个IS_INDIRECT类型的zval,指向static_variables中对应静态变量的zval * ZEND_ASSIGN_REF: 它的操作是引用赋值,即将一个引用赋值给CV变量 通过上面两条opcode可以确定静态变量的读写过程: 首先根据变量名在static_variables中取出对应的zval,然后将它修改为引用类型并赋值给局部变量, 也就是说static $count = 4; 包含了两个操作,严格的讲 $count 并不是真正的静态变量,它只是一个指向静态变量的局部变量,执行时实际操作是: $count = & static_variables[“count”]; 。

    //生成一条ZEND_FETCH_W的opcode
    opline = zend_emit_op(&result, by_ref ? ZEND_FETCH_W : ZEND_FETCH_R, &var
    _node, NULL);
    opline->extended_value = ZEND_FETCH_STATIC;
    if (by_ref) {
    zend_ast *fetch_ast = zend_ast_create(ZEND_AST_VAR, var_ast);
    //生成一条ZEND_ASSIGN_REF的opcode
    zend_emit_assign_ref_znode(fetch_ast, &result);
    }
    

    上面例子$count与 static_variables[“count”]间的关系如图所示

  • 全局变量 PHP中在函数、类之外直接定义的变量可以在函数、类成员方法中通过global关键词引入使 用,这些变量称为:全局变量

全局变量在整个请求执行期间始终存在,它们保存在 EG(symbol_table) 中,也就是全局 变量符号表,与静态变量的存储一样,这也是一个哈希表,主脚本(或include、require)在 zend_execute_ex 执行开始之前会把当前作用域下的所有局部变量添加到 EG(symbol_table) 中

与静态变量的访问一样,全局变量也是将原来的值转换为引用,然后在global导入的作用域 内创建一个局部变量指向该引用

  • 超全局变量 超全局变量实际是PHP内核定义的一些全局变量:$GLOBALS、$_SERVER、$_REQUEST、 $_POST、$_GET、$_FILES、$_ENV、$_COOKIE、$_SESSION、argv、argc
  • 常量 在内核中常量存储在 EG(zend_constant) 哈希表 常量的数据结构

    typedef struct _zend_constant {
            zval value; //常量值
            zend_string *name; //常量名
            int flags; //常量标识位
            int module_number; //所属扩展、模块
    } zend_constant;
    

    这里主要介绍下flags,它的值可以是以下三个中任意组合:

    #define CONST_CS (1<<0) //大小写敏感
    #define CONST_PERSISTENT (1<<1) //持久化的
    #define CONST_CT_SUBST (1<<2) 
    

    介绍下三种flag代表的含义:

    • CONST_CS: 大小写敏感,默认是开启的,用户通过define()定义的始终是区分大小 写的,通过扩展定义的可以自由选择
    • CONST_PERSISTENT: 持久化的,只有通过扩展、内核定义的才支持,这种常量不 会在request结束时清理掉
    • CONST_CT_SUBST: 允许编译时替换,编译时如果发现有地方在读取常量的值,那么编 译器会尝试直接替换为常量值,而不是在执行时再去读取,目前这个flag只有TRUE、 FALSE、NULL三个常量在使用

这里需要特别注意的一个是,__CONST_PERSISTENT这个持久化状态,持久化常量是在 php_module_shutdown() 阶段销毁的

  • 资源型变量 在实际使用PHP中,开启对mysql,redis的长连接是很常见的优化措施, 之前也思考过每次请求结束都会进行gc销毁变量的php-fpm是如何维持跨请求的长连接的, 这里就要另外介绍php内核对资源型数据的存储

    • 非持久化的资源型变量保存在EG(regular_list)
    • 持久化的资源型变量保存在EG(persistent_list)中
  • php数组的实现

普通的hashtable是无序的,而php实现的数组功能却是有序的(保持插入数组的顺序),这是因为php内核在实现数组功能时,根据key计算的哈希值索引定位到的是一个中间散列表,而不是直接定位到数组元素组成的数组中

向数组添加元素的时候,会先顺序的吧新元素加入到数组中,而根据key进行寻址查找元素时,则是先定位到中间散列表的某个下标值,而这个值就是元素在数组中的下标

内存管理

zend 对内存的操作做了一层封装,提供的emalloc、efree、estrdup的操作就是在zend的内存池上,而不是直接操作内存

  • 内存池是内核中最底层的内存操作,定义了三种粒度的内存块:chunk、page、slot,每个 chunk的大小为2M,page大小为4KB,一个chunk被切割为512个page,而一个或若干个 page被切割为多个slot,所以申请内存时按照不同的申请大小决定具体的分配策略:
  • 内存池在php_module_startup阶段初始化,start_memory_manager

    ZEND_API void start_memory_manager(void)
    {
    #ifdef ZTS
    ts_allocate_id(&alloc_globals_id, sizeof(zend_alloc_globals), (ts_all
    ocate_ctor) alloc_globals_ctor, (ts_allocate_dtor) alloc_globals_dtor);
    #else
    alloc_globals_ctor(&alloc_globals);
    #endif
    }
    static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
    {
    #ifdef MAP_HUGETLB
    tmp = getenv("USE_ZEND_ALLOC_HUGE_PAGES");
    if (tmp && zend_atoi(tmp, 0)) {
    zend_mm_use_huge_pages = 1;
    }
    #endif
    ZEND_TSRMLS_CACHE_UPDATE();
    alloc_globals->mm_heap = zend_mm_init();
    }
    
    

alloc_globals 是一个全局变量,即 AG宏 ,它只有一个成员:mm_heap,保存着整个内 存池的信息,所有内存的分配都是基于这个值,多线程模式下(ZTS)会有多个heap,也就是 说每个线程都有一个独立的内存池,(php多线程的线程安全实现,就是简单粗暴的复制出独立内存池)

  • 内存的分配

  • 垃圾回收 一个是引用计数这个早期就有的基本机制,refcount减到0时,释放变量 这里同时也介绍下一个比较通用的写时复制机制,

    $a = 1;
    $b = $a;
    // 这里变量$a 与变量$b 持有的是同一个zend_val
    $a = 2;
    // 这个时候变量$a的值发生了改变,而显然,让$b的值也发生同样的改变是不符合预期的
    //所以这个时候就会发生zend_val的复制
    
    //另外一种情况
    $a = 1;
    $b = &$a;
    //当$b只有的是对$a的引用时,这两个变量始终共用同一个zend_val
    $a = 2; //这时$b的值也为2
    

引用计数机制有一个缺陷,就是碰到循环引用时,refcount无法减到0,导致变量无法释放,具体来说就是变量内部的成员引用了变量本身,比如数组中的某个元素指向了数组

    $a = [1];
    $a[] = &$a;
    unset($a);
针对这种情况,php引入了垃圾回收器来处理
变量是否加入垃圾检查buffer并不是根据zval的类型判断的,而是与前面介绍的是否用到引用计数一样通过 zval.u1.type_flag 记录的,只有包含 IS_TYPE_COLLECTABLE 的变量才会被GC收集

目前垃圾只会出现在array、object两种类型中,只有这两种类型的变量会出现成员引用自身的情况

如果当变量的refcount减少后大于0,PHP并不会立即进行对这个变量进行垃圾鉴定,而是放

入一个缓冲buffer中,等这个buffer满了以后(10000个值)再统一进行处理,加入buffer的是 变量zend_value的 zend_refcounted_h

一个变量只能加入一次buffer,为了防止重复加入,变量加入后会把
zend_refcounted_h.gc_info 置为 GC_PURPLE ,即标为紫色,下次refcount减少时
如果发现已经加入过了则不再重复插入。
垃圾缓存区是一个双向链表,等到缓存区满了以后则启动垃圾检查过程:遍历缓存区,再对当前变量的所有成员进行遍历,然后把成员的refcount减1(如果成员还包含子成员则也进行递归遍历,其实就是深度优先的遍历),最后再
检查当前变量的引用,如果减为了0则为垃圾
这个算法的原理很简单,垃圾是由于成员引用自身导致的,那么就对所有的成员减一遍引用,结果如果发现变量本身refcount变为了0则就表明其引用全部来自自身成员。

PHP对象在内存堆栈中的分配

这里要注意内存中的堆栈与数据结构中的堆栈不是一回事 内存中的堆栈: 栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。

对象在PHP里面和整型、浮点型一样,也是一种数据类,都是存储不同类型数据用的, 在运行的时候都要加载到内存中去用,那么对象在内存里面是怎么体现的呢?内存从逻辑上说大体上是分为4段,栈空间段、堆空间段、代码段、初始化静态段,程序里面不同的声明放在不同的内存段里面。

数据段(data segment)通常是指用来存放程序中已初始化且不为0的全局变量如:静态变量和常量

代码段(code segment / text segment)通常是指用来存放程序执行代码的一块内存区域,比如函数和方法

栈空间段是存储占用相同空间长度并且占用空间小的数据类型的地方,比如说整型1,10,100,1000,10000,100000 等等,在内存里面占用空间是等长的,都是64 位4 个字节。

(heap)数据长度不定长,而且占有空间很大的数据类型的数据放在堆内存里面的。

栈内存是可以直接存取的,而堆内存是 不可以直接存取的内存。对于我们的对象来数就是一种大的数据类型而且是占用空间不定长的类型,所以说对象是放在堆里面的,但对象名称是放在栈里面的,这样通过对象名称就可 以使用对象了。

  • PHP脚本运行的时候,那些变量被放到了栈内存,那些被保存到了堆内存?

在PHP5的Zend Engine的实现中,所有的值都是在堆上分配空间,并且通过引用计数和垃圾收集来管理. PHP5的Zend Engine主要使用指向zval结构的指针来操作值,在很多地方甚至通过zval的二级指针来操作.

而在PHP7的Zend Engine实现中,值是通过zval结构本身来操作(非指针). 新的zval结构直接被存放在VM[虚拟机?]的栈上,HashTable的桶里,以及属性槽里. 这样大大减少了在堆上分配和释放内存的操作,还避免了对简单值的引用计数和垃圾收集.

引用:

https://www.cnblogs.com/web21/p/6197980.html

《PHP7内核剖析》


comments powered by Disqus
Ï