Heim >php教程 >php手册 >PHP命令执行PHP脚本,结束之前,内存会回收吗?

PHP命令执行PHP脚本,结束之前,内存会回收吗?

WBOY
WBOYOriginal
2016-06-06 19:57:08961Durchsuche

在往下看之前,如果你不明白什么是GC (Garbage Collection) 的话,那一定要先去了解GC,不然你根本不知道我在说什么! Links: http://www.php.net/manual/en/features.gc.php http://blog.csdn.net/phpkernel/article/details/5734743 =========================

在往下看之前,如果你不明白什么是GC (Garbage Collection) 的话,那一定要先去了解GC,不然你根本不知道我在说什么!

Links:

http://www.php.net/manual/en/features.gc.php

http://blog.csdn.net/phpkernel/article/details/5734743

===================================================================

再详细说下问题:

unix下,用php命令来执行php脚本,在php结束之前,内存有没有机会被回收?新的GC算法有没有机会被调用?

出现这个问题,是因为线上有个 离线数据导入脚本,需要把几千万行数据筛选入库,发现,在执行过程中,到达一定程度,就会抛出 内存使用超过最大值。

1

Fatal error: Allowed memory size of 293601280 bytes exhausted

那第一想到的就是程序是不是有什么bug,造成内存超出,看了半天没有发现问题,于是,突然出现了这个疑问。 那要解决这个疑问,最好的办法就去翻源码吧。

在之前我这么说:

都知道,PHP5.3有了新的垃圾回收机制:GC,它不是重点,不谈它的原理。

经过翻阅PHP源码,发现,调用这个的时机是在 main/main.c ::php_request_shutdown这个函数中,

1

2

/* 7. Shutdown scanner/executor/compiler and restore ini entries */

        zend_deactivate(TSRMLS_C);

php_request_shutdown,通过名字就能看出,它是在php请求结束的时候执行的,在这里会执行 gc_collect_cycles 来清理内存。


其实这句话是没错,但它只针对于SAPI接口(之前我就错在这个地方。),在用PHP命令执行php脚本的时候,是不会执行这个php_request_shutdown的。

那回到最初的问题,过程中到底有没有执行GC呢?

为了更直观有效的知道答案,我选择了最BT,最暴力的方法,拦截gc_collect_cycles,输出error_log到文件,只要执行了,

那肯定会输出log来。

重新编译PHP后,果不其然,符合官方的说法,只要buckets满超过默认值1000,就会启动GC来清理没用的内存,防止内存泄露。

那问 “什么时间 触发的GC呢?”,答 “buckets超过1000的时候啊”,这不屁话嘛,要的是真真正正的执行流程,so。。不断的debug,

不断的grep,不断的step,不断的C+T,终于搞清楚了。下面就来根据官方的说法详细谈谈,PHP到底是怎么触发的。

有一点要注意,PHP的命令入口 和 sapi接口的入口 是不同的,我就载在这个地方,以为都公用一个。

测试代码以官方文档为例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<?php

class Foo

{

    public $var = '3.1415962654';

}

 

for ( <code>$i = 0; <code>$i <code>$i++ )

{

    $a = new Foo;

    $a->self = $a;

}

 

echo memory_get_peak_usage(), "\n";

?>

这样的代码,在PHP5.3之前,肯定会造成大量的 内存泄露,不过,谁在开发时又能开发出这么变态的代码来?除非这个人很变态。^.*

那PHP的命令入口是什么?流程又是什么?

主要函数流程如下:

入口main函数(sapi/cli/php_cli.c) ==》php_execute_script(main/main.c)==>zend_execute_scripts(Zend/zend.c)==>execute(Zend/zend_vm_execute.h)

调用GC的地方在execute里。

简单描述下这个过程,

main 是入口,它的作用是根据我们传递的参数做不同的设置,最后会把我们的php脚本作为一个zend_file_handle指针传递给

php_execute_script函数,zend_file_handle其实就是把FILE*做了一下封装,保存了一些其他的文件信息。

php_execute_script会做一些文件检查工作,把php脚本加到 哈希表included_files中。

php_execute_scripts 会执行 zend_compile_file函数来解释我们写的PHP代码,最后执行execute。

应该都知道 Zend把脚本解析完会生成op代码保存到 哈希表:active_op_array中,execute会逐个执行每个op,

op基本上都对应一个ZEND_ASSIGN_*_HANDLER这样的一个宏,它就保存在active_op_array->opline->handlers中。

在进入到 execute之后:

首先初始化execute_data,它保存了很多重要信息,上下文信息,然后调用 ZEND_VM_SET_OPCODE宏,

把execute_data->opline的指针指向active_op_array->opline->handlers。

之后,execute会执行一个while循环,逐条执行opline:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

        while (1) {

        int ret;

#ifdef ZEND_WIN32

                if (EG(timed_out)) {

                        zend_timeout(0);

                }

#endif

 

                if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {

                        switch (ret) {

                                case 1:

                                        EG(in_execution) = original_in_execution;

                                        return;

                                case 2:

                                        op_array = EG(active_op_array);

                                        goto zend_vm_enter;

                                case 3:

                                        execute_data = EG(current_execute_data);

                                default:

                                        break;

                        }

                }

 

        }

每个handlers都会执行一个宏:ZEND_VM_NEXT_OPCODE(),它意思就是跳到下一个Opline,这样就能逐条执行了。

最后跟踪 上面的PHP代码会执行 ZEND_ASSIGN_SPEC_CV_VAR_HANDLER这个宏,它是干嘛的?他就是 变量赋值

下面代码执行的操作:

1

2

3

4

class A{

 

}

$a=new A();

这里就会执行 这个宏。

在这个宏里有段代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

    static int ZEND_FASTCALL  ZEND_ASSIGN_SPEC_CV_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

{

        zend_op *opline = EX(opline);

        zend_free_op free_op2;

        zval *value = _get_zval_ptr_var(&opline->op2, EX(Ts), &free_op2 TSRMLS_CC);

        zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);

 

        if (IS_CV == IS_VAR && !variable_ptr_ptr) {

                if (zend_assign_to_string_offset(&EX_T(opline->op1.u.var), value, IS_VAR TSRMLS_CC)) {

                        if (!RETURN_VALUE_UNUSED(&opline->result)) {

                                EX_T(opline->result.u.var).var.ptr_ptr = &EX_T(opline->result.u.var).var.ptr;

                                ALLOC_ZVAL(EX_T(opline->result.u.var).var.ptr);

                                INIT_PZVAL(EX_T(opline->result.u.var).var.ptr);

                                ZVAL_STRINGL(EX_T(opline->result.u.var).var.ptr, Z_STRVAL_P(EX_T(opline->op1.u.var).str_offset.str)+EX_T(opline->op1.u.var).str_offset.offset, 1, 1);

                        }

                } else if (!RETURN_VALUE_UNUSED(&opline->result)) {

                        AI_SET_PTR(EX_T(opline->result.u.var).var, EG(uninitialized_zval_ptr));                        PZVAL_LOCK(EG(uninitialized_zval_ptr));

                }

        } else {

                value = zend_assign_to_variable(variable_ptr_ptr, value, 0 TSRMLS_CC);

                if (!RETURN_VALUE_UNUSED(&opline->result)) {

                        AI_SET_PTR(EX_T(opline->result.u.var).var, value);

                        PZVAL_LOCK(value);

                }

        }

 

        /* zend_assign_to_variable() always takes care of op2, never free it! */

        if (free_op2.var) {zval_ptr_dtor(&free_op2.var);};

 

        ZEND_VM_NEXT_OPCODE();

}

free_op2.var保存的是 new A的对象.

free_op2.var这个是哪儿来的呢?

在整个execute期间,维持一个 execute_data结构,里面有个 Ts指针

1

union _temp_variable *Ts;

它用来保存一些临时的变量信息,比如 new A(),这个会保存到Ts链表里,

opline->op2.u.var这个里面保存了此临时变量所在的位置,然后Ts+这个值是一个zval*指针,它就保存了new A产生的对象.

在代码中

1

if (free_op2.var) {zval_ptr_dtor(&free_op2.var);};

zval_ptr_dtor会根据free_op2.var的值执行到 Zend/zend_execute_API.c::_zval_ptr_dtor函数中,

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

ZEND_API void _zval_ptr_dtor(zval **zval_ptr ZEND_FILE_LINE_DC) /* {{{ */

{

        zval *zv = *zval_ptr;

 

#if DEBUG_ZEND>=2

        printf("Reducing refcount for %x (%x): %d->%d\n", *zval_ptr, zval_ptr, Z_REFCOUNT_PP(zval_ptr), Z_REFCOUNT_PP(zval_ptr) - 1);

#endif

        Z_DELREF_P(zv);

        if (Z_REFCOUNT_P(zv) == 0) {

                TSRMLS_FETCH();

 

                if (zv != &EG(uninitialized_zval)) {

                        GC_REMOVE_ZVAL_FROM_BUFFER(zv);

                        zval_dtor(zv);

                        efree_rel(zv);

                }

        } else {

                TSRMLS_FETCH();

 

                if (Z_REFCOUNT_P(zv) == 1) {

                        Z_UNSET_ISREF_P(zv);

                }

 

                GC_ZVAL_CHECK_POSSIBLE_ROOT(zv);

        }

}

GC_ZVAL_CHECK_POSSIBLE_ROOT(zv);

它就是最终GC算法执行的地方.

gc_collect_cycles就在这个宏中执行了..

所以..

回到上面的问题,

php无论在SAPI接口或命令端,都会执行 GC算法来进行垃圾内存回收.

看样子,内存泄露的问题还得从程序里面找了.

====================================================================

如果 您发现哪里错了,一定要帮我指出来。3Q!

http://www.imsiren.com/archives/704

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn