当前位置: 首页 > 后端技术 > PHP

【PHP7源码学习】2019-04-01PHP垃圾回收1

时间:2023-03-29 19:37:23 PHP

baiyan完整视频:https://segmentfault.com/a/11...垃圾回收的触发条件我们知道在PHP中,如果一个变量时引用计数减为0(这个变量没有被用到任何地方),它占用的内存会被PHP虚拟机自动回收,不会被当作垃圾处理。垃圾回收的触发条件是当一个变量的引用计数的值减1时,仍然不为0(该变量还在某处被使用),可能是垃圾。我们需要进一步手动检查它是否真的是垃圾,然后再进行后续操作。一个典型的例子就是我们在使用数组和对象时可能存在的循环引用问题。它使变量引用自身。看下面的例子:time()];$a[]=&$a;//循环引用unset($a);我们可以知道,unset($a)之后,$a的类型变为0(IS_UNDEF),它指向的zend_reference结构体的refcount变为1(因为$a数组中的元素仍然引用它),我们画一张图来表示当前的内存情况:那么问题来了,\$a是unset的,但是因为原来zend_array中的元素还是指向zend_reference结构体,所以zend_reference的refcount是1,不是预期的0.这样,zend_reference和zend_array这两个结构在unset($a)之后仍然存在于内存中,如果什么都不做,就会造成内存泄漏。以上详细解释请看:【PHP源码学习】2019-03-19PHP参考那么如何解决循环引用导致的内存泄漏问题呢?我们的垃圾回收就要派上用场了。在PHP7中,垃圾回收分为垃圾回收器和垃圾回收算法两部分。在这篇笔记中,只解释了第一部分:垃圾收集器,首先会把它放到一个双向链表中,也就是我们的垃圾收集器。这个垃圾收集器相当于一个缓冲区。缓冲区满后,等待垃圾回收算法进行后续的标记和清除操作。启动垃圾回收算法的时机并不是简单的当一个可疑的垃圾到达时必须运行一次,而是在缓冲区满后(指定10001个存储单元),然后启动垃圾回收算法。疑似垃圾最终被标记并清除。这个垃圾收集器缓冲区的作用是减少垃圾收集算法运行的频率,减少操作系统资源的占用和对正在运行的服务器代码的影响。下面通过代码来详细解释一下。垃圾收集器存储结构垃圾收集器的结构如下:typedefstruct_gc_root_buffer{zend_refcounted*ref;结构_gc_root_buffer*下一个;//双向链表,指向下一个缓冲单元struct_gc_root_buffer*prev;//双向链表,指向上一个缓冲区单元uint32_trefcount;}gc_root_buffer;垃圾收集器是一个双向链表,那么如何维护这个双向链表的头指针和尾指针的信息,以及缓冲区的使用等附加信息,现在就需要用到我们的全局变量zend_gc_globals:typedefstruct_zend_gc_globals{zend_boolgc_enabled;//是否开启gczend_boolgc_active;//gc当前是否在运行zend_boolgc_full;//缓冲区是否满gc_root_buffer*buf;/*指向缓冲区头*/gc_root_bufferroots;/*当前处理的垃圾缓冲单元,注意这不是指针*/gc_root_buffer*unused;/*指向未使用的缓冲区单元链表的开头(用于连接缓冲区片段)*/gc_root_buffer*first_unused;/*指向第一个未使用的缓冲单元*/gc_root_buffer*last_unused;/*指向最后一个未使用的缓冲单元*/gc_root_bufferto_free;gc_root_buffer*next_to_free;...}zend_gc_globals;垃圾收集器初始化现在,我们需要为垃圾收集器分配内存空间,用于存放接下来可能出现的可疑垃圾。我们使用gc_init()函数来分配空间:ZEND_APIvoidgc_init(void){if(GC_G(buf)==NULL&&GC_G(gc_enabled)){GC_G(buf)=(gc_root_buffer*)malloc(sizeof(gc_root_buffer)*GC_ROOT_BUFFER_MAX_ENTRIES);GC_G(last_unused)=&GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];gc_reset()}GC_G这个宏就是获取上面zend_gc_globals结构体中的变量。我们还没有生成缓冲区,所以进入这个if分支。通过系统调用malloc分配一块内存,这块内存的大小是单个缓冲区结构的大小*10001:#defineGC_ROOT_BUFFER_MAX_ENTRIES10001现在我们有一个大小为10001的缓冲区(第一个单元没有用到),并将指针的步长设置为gc_root_buffer类型,然后将其last_unused指针设置为缓冲区的末尾,然后通过gc_reset()做一些初始化操作:ZEND_APIvoidgc_reset(void){GC_G(gc_runs)=0;GC_G(收集)=0;GC_G(gc_full)=0;...GC_G(根).next=&GC_G(根);GC_G(根).prev=&GC_G(根);GC_G(to_free).next=&GC_G(to_free);GC_G(to_free).prev=&GC_G(to_free);if(GC_G(buf)){//因为我们之前分配了缓冲区,所以这里输入GC_G(unused)=NULL;//没有缓冲区碎片,设置指针为NULLGC_G(first_unused)=GC_G(buf)+1;//将指向第一个未使用空间的指针向后移动1个单位的长度}else{GC_G(unused)=NULL;GC_G(first_unused)=NULL;GC_G(last_unused)=NULL;}GC_G(additional_buffer)=NULL;}根据这个函数的内容,我们可以画出当前的内存结构图:将疑似垃圾存放在垃圾收集器中。这样,我们的垃圾收集器缓冲区就初始化好了,现在等待zend虚拟机把可能是垃圾的变量收集起来,存放到这些缓冲区中。这一步是通过gc_possible_root(zend_refcounted*ref)函数完成的:ZEND_AP我无效ZEND_FASTCALLgc_possible_root(zend_refcounted*ref){gc_root_buffer*newRoot;if(UNEXPECTED(CG(unclean_shutdown))||UNEXPECTED(GC_G(gc_active))){return;}ZEND_ASSERT(GC_TYPE(ref)==IS_ARRAY||GC_TYPE(ref)==IS_OBJECT);ZEND_ASSERT(预期(GC_REF_GET_COLOR(ref)==GC_BLACK));ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref)));GC_BENCH_INC(zval_possible_root);newRoot=GC_G(未使用);if(newRoot){GC_G(unused)=newRoot->prev;}elseif(GC_G(first_unused)!=GC_G(last_unused)){newRoot=GC_G(first_unused);GC_G(first_unused)++;}else{if(!GC_G(gc_enabled)){返回;}GC_REFCOUNT(ref)++;gc_collect_cycles();GC_REFCOUNT(参考)--;if(UNEXPECTED(GC_REFCOUNT(ref))==0){zval_dtor_func(ref);返回;}if(UNEXPECTED(GC_INFO(ref))){return;}newRoot=GC_G(未使用);if(!newRoot){#ifZEND_GC_DEBUGif(!GC_G(gc_full)){fprintf(stderr,"GC:没有空间记录新的候选根\n");GC_G(gc_full)=1;}#endif返回;}GC_G(未使用)=newRoot->prev;}GC_TRACE_SET_COLOR(参考,GC_PURPLE);GC_INFO(ref)=(newRoot-GC_G(buf))|GC_紫色;newRoot->ref=ref;newRoot->next=GC_G(roots).next;newRoot->prev=&GC_G(roots);GC_G(roots).next->prev=newRoot;GC_G(roots).next=newRoot;GC_BENCH_INC(zval_buffered);GC_BENCH_INC(root_buf_length);GC_BENCH_PEAK(root_buf_peak,root_buf_length);}代码有点长没关系,我们一行一行分析,先申明一个指向buffer的指针newRoot。接下来判断,如果垃圾收集器已经在运行,那么这次就不执行了。然后将zend_gc_globals全局变量上未使用的指针字段赋值给newRoot指针,但是未使用的指针为NULL(因为没有缓冲区碎片),所以此时newRoot也为NULL。于是再进入elseif分支:newRoot=GC_G(first_unused);GC_G(first_unused)++;首先把newRoot指向第一个未使用的缓冲单元,所以下一行需要将第一个未使用的缓冲单元往后移一个单元,方便下次使用,很容易理解,跳过这个长长的else分支,继续执行:GC_TRACE_SET_COLOR(参考,GC_PURPLE);GC_INFO(ref)=(newRoot-GC_G(buf))|GC_紫色;newRoot->ref=ref;newRoot->next=GC_G(roots).next;newRoot->prev=&GC_G(roots);GC_G(roots).next->prev=newRoot;GC_G(roots).next=newRoot;第一行宏GC_TRACE用于打印相关的DEBUG信息,我们跳过这一行。第二行执行GC_INFO(ref)=(newRoot-GC_G(buf))|GC_紫色;我们看到这里有一个GC_PURPLE,就是颜色的概念。在PHP垃圾回收中,使用了4种颜色:#defineGC_BLACK0x0000#defineGC_WHITE0x8000#defineGC_GREY0x4000#defineGC_PURPLE0xc000它们在源代码中解释如下:*BLACK(GC_BLACK)-Inuseorfree。*GREY(GC_GREY)-循环的可能成员。*WHITE(GC_WHITE)-垃圾循环成员。*PURPLE(GC_PURPLE)-可能的循环根。这里我们不会详细解释每种颜色。我们使用(newRoot-GC_G(buf))|GC_PURPLE的意思是:newRoot-GC_G(buf)(bufferstartaddress)表示当前使用的buffer的偏移量,然后与0xc000进行OR运算,结果拼装到变量的gc_info字段中,该字段为uint16类型,所以你可以用前2位来标记它是紫色的,用后14位来存储偏移量。逐位拆final字段的情况如图:第3行:将当前引用赋值给当前缓冲区接下来是双向链表的指针操作:newRoot->next=GC_G(roots).next;newRoot->prev=&GC_G(roots);GC_G(roots).next->prev=newRoot;GC_G(roots).next=newRoot;目的是将当前buffer的prev和next指针指向全局变量中的root域,同时设置全局变量root域中的prev和next指针指向当前使用的buffer。至此,我们就可以将疑似垃圾变量全部放入缓冲区,永久保存。当缓冲区满10000个存储单元后,垃圾回收算法就会开始对缓冲区中所有可疑的垃圾进行标记和清除,垃圾回收算法的过程将在下一篇笔记中解释。