内容简介:baiyan全部视频:
baiyan
全部视频: https://segmentfault.com/a/11...
垃圾回收触发条件
- 我们知道,在 PHP 中,如果一个变量的引用计数减少到0(没有任何地方在使用这个变量),它所占用的内存就会被PHP虚拟机自动回收, 并不会被当做垃圾 。垃圾回收的触发条件是当一个变量的引用计数的值减少1之后,仍不为0(还有某个地方在使用这个变量),才 有可能 是垃圾。需要让我们人工去对其进行进一步的检验,看它是否真的是垃圾,然后再做后续的操作。一个典型的例子就是在我们使用 数组 与 对象 的过程中可能存在的 循环引用 问题。它会让某个变量自己引用自己。看下面一个例子:
<?php $a = ['time' => time()]; $a[] = &$a; //循环引用 unset($a);
- 我们可以知道,unset($a)之后,$a的type类型变成了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-19 PHP引用
- 那么如何解决循环引用带来的内存泄漏问题呢?我们的 垃圾回收 就要派上用场了。
- 在PHP7中,垃圾回收分为 垃圾回收器 和 垃圾回收算法 两大部分
- 在这篇笔记中只讲解第一部分:垃圾回收器
垃圾回收器
- 在PHP7中,如果检测到refcount减1后仍大于0的变量,会首先把它放入一个 双向链表 中,它就是我们的垃圾回收器。这个垃圾回收器相当于一个 缓冲区 的作用,待缓冲区满了之后,等待垃圾回收算法进行后续的标记与清除操作。
- 垃圾回收算法的启动时机并不是简单的有一个疑似垃圾到来,就要运行一次,而是待缓冲区存满了之后(规定10001个存储单元),然后垃圾回收算法才会启动,对缓冲区中的疑似垃圾进行最终的标记和清除。这个垃圾回收器缓冲区的作用就是减少垃圾回收算法运行的频率,减少对操作系统资源的占用以及对正在运行的服务端代码的影响,下面我们通过代码来详细讲解。
垃圾回收器存储结构
- 垃圾回收器的结构如下:
typedef struct _gc_root_buffer { zend_refcounted *ref; struct _gc_root_buffer *next; //双向链表,指向下一个缓冲区单元 struct _gc_root_buffer *prev; //双向链表,指向上一个缓冲区单元 uint32_t refcount; } gc_root_buffer;
- 垃圾回收器是一个双向链表,那么如何维护这个双向链表首尾指针的信息,还有缓冲区的使用情况等额外信息呢,现在就需要使用我们的全局变量zend_gc_globals了:
typedef struct _zend_gc_globals { zend_bool gc_enabled; //是否启用gc zend_bool gc_active; //当前是否正在运行gc zend_bool gc_full; //缓冲区是否满了 gc_root_buffer *buf; /*指向缓冲区头部 */ gc_root_buffer roots; /*当前处理的垃圾缓冲区单元,注意这里不是指针*/ gc_root_buffer *unused; /*指向未使用的缓冲区单元链表开头(用于串联缓冲区碎片)*/ gc_root_buffer *first_unused; /*指向第一个未使用的缓冲区单元*/ gc_root_buffer *last_unused; /*指向最后一个未使用的缓冲区单元 */ gc_root_buffer to_free; gc_root_buffer *next_to_free; ... } zend_gc_globals;
垃圾回收器初始化
- 那么现在,我们需要为垃圾回收器分配内存空间,以存储接下来可能到来的可疑垃圾,我们通过gc_init()函数实现空间的分配:
ZEND_API void gc_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:
#define GC_ROOT_BUFFER_MAX_ENTRIES 10001
- 那么现在我们得到了大小为10001的缓冲区(第1个单元不用),并把它的步长置为gc_root_buffer类型,随后将它的last_unused指针指向缓冲区的末尾,然后通过gc_reset()做一些初始化操作:
ZEND_API void gc_reset(void) { GC_G(gc_runs) = 0; GC_G(collected) = 0; GC_G(gc_full) = 0; ... GC_G(roots).next = &GC_G(roots); GC_G(roots).prev = &GC_G(roots); 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; //没有缓冲区碎片,置指针为NULL GC_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_API void ZEND_FASTCALL gc_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(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK)); ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref))); GC_BENCH_INC(zval_possible_root); newRoot = GC_G(unused); if (newRoot) { GC_G(unused) = newRoot->prev; } else if (GC_G(first_unused) != GC_G(last_unused)) { newRoot = GC_G(first_unused); GC_G(first_unused)++; } else { if (!GC_G(gc_enabled)) { return; } GC_REFCOUNT(ref)++; gc_collect_cycles(); GC_REFCOUNT(ref)--; if (UNEXPECTED(GC_REFCOUNT(ref)) == 0) { zval_dtor_func(ref); return; } if (UNEXPECTED(GC_INFO(ref))) { return; } newRoot = GC_G(unused); if (!newRoot) { #if ZEND_GC_DEBUG if (!GC_G(gc_full)) { fprintf(stderr, "GC: no space to record new root candidate\n"); GC_G(gc_full) = 1; } #endif return; } GC_G(unused) = newRoot->prev; } GC_TRACE_SET_COLOR(ref, GC_PURPLE); GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE; 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); }
- 代码有点长不要紧,我们逐行分析。首先又声明了一个指向缓冲区的指针newRoot。接下来判断如果垃圾回收器已经运行,那么本次就不再执行了。然后将zend_gc_globals全局变量上的unused指针字段赋值给newRoot指针,然而unused指针为NULL(因为没有缓冲区碎片),所以newRoot此时也为NULL。故接下来进入else if分支:
newRoot = GC_G(first_unused); GC_G(first_unused)++;
- 首先将newRoot指向第一个未使用的缓冲区单元,所以下一行需要将第一个未使用的缓冲区单元往后挪一个单元,方便下一次的使用,很好理解,跳过这个长长的else分支往下继续执行:
GC_TRACE_SET_COLOR(ref, GC_PURPLE); GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE; 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_PURPLE;我们看到这里有一个GC_PURPLE,也就是颜色的概念。在PHP垃圾回收中,用到了4种颜色:
#define GC_BLACK 0x0000 #define GC_WHITE 0x8000 #define GC_GREY 0x4000 #define GC_PURPLE 0xc000
- 源码中对它们的解释如下:
* BLACK (GC_BLACK) - In use or free. * GREY (GC_GREY) - Possible member of cycle. * WHITE (GC_WHITE) - Member of garbage cycle. * PURPLE (GC_PURPLE) - Possible root of cycle.
- 这里我们先不对每一种颜色做详细解释。我们用(newRoot - GC_G(buf)) | GC_PURPLE的意思是:newRoot - GC_G(buf)(缓冲区起始地址)代表当前使用的缓冲区的偏移量,再与0xc000做或运算,结果拼装到变量的gc_info字段中,这个字段是一个uint16类型,所以可以利用前2位把它标记成紫色,同时利用后14位存储偏移量。最终字段按位拆开的情况如图:
- 第三行:将当前引用赋值到当前缓冲区中
- 接下来是双向链表的指针操作:
newRoot->next = GC_G(roots).next; newRoot->prev = &GC_G(roots); GC_G(roots).next->prev = newRoot; GC_G(roots).next = newRoot;
- 其目的是将当前缓冲区的prev和next指针指向全局变量中的root字段,同时将全局变量中的root字段的prev与next指针指向当前使用的缓冲区。
- 至此,我们就可以将所有疑似垃圾的变量都放到缓冲区中,一直存下去,待存满缓冲区10000个存储单元之后,垃圾回收算法就会启动,对缓冲区中的所有疑似垃圾进行标记与清除,垃圾回收算法的过程会在下一篇笔记进行讲解。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- JVM 源码分析之 JDK 8 下的僵尸(无法回收)类加载器
- 垃圾回收算法(7)-分代回收算法
- JAVA 垃圾回收机制(二) --- GC回收具体实现
- 对象回收判定与垃圾回收算法-JVM学习笔记(1)
- 必知必会JVM垃圾回收——对象搜索算法与回收算法
- Go 语言的垃圾回收演化历程:垃圾回收和运行时问题
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。