现在的位置: 首页 > 综合 > 正文

simple_html_dom使用不当导致的内存泄漏

2014年09月24日 ⁄ 综合 ⁄ 共 2750字 ⁄ 字号 评论关闭

转自:http://wbean.3sbstudio.com/?p=77

今天用simple_html_dom写一个简单的网页爬虫(php5.2.3),运行很慢,并且很快超出默认的8m内存,后面把内存改成memory_limit = 128M才运行成功。这显然不满足我的要求。然后我用memory_get_usage()函数在每次循环结尾输出一下所用内存,循环每执行一次,内存上涨1m多,最后60次循环用了80多m的内存。

细细检查,在脚本用到的simple_html_dom对象和simple_html_dom_node对象的析构函数中添加了输出,运行发现在循环的过程中,这两个对象没有一次也没有被GC回收,我用了unset($var),$var=null等操作仍然没有效果,都是在脚本结束之后,释放了全部对象。
后面又试了试在php5.3下面运行这个脚本,内存占用就大大减少了,每次循环结束,就会输出call destruct等信息,说明每次循环结束,对象就被回收了。
后面发现simple_html_dom对象有一个clear方法,于是我在每次使用完simple_html_dom之后,调用其clear方法,这样无论在5.2还是5.3的版本中,都不会占用大量内存了。

另附文章一篇:刚好解决了我今天的问题,早些搜到就不用摸索这么久了
0.导言

你写了一个php脚本,一般你都不用考虑内存泄露和垃圾回收的问题,因为一般情况下你的脚本很快就执行完退出了;只有当你写一个需要处理很多数据或者运行很长时间的php脚本的时候,才需要考虑这个问题。

我的一位同事就遇到了这个问题;他需要抓取并分析几千个页面;处理页面的时候,它使用了 simple_html_dom 这个开源工具,每个页面处理时都新建一个 simple_html_dom 对象;然后他发现,程序运行一段时间后,php脚本就占用了过多内存,然后就报错(PHP Fatal error: Allowed memory size of 134217728 bytes exhausted)退出了。一般来说,每个页面处理结束,新建的simple_html_dom对象就应该被销毁了——但是实际上没有,很明显,内存泄露发生了。

1.PHP的垃圾回收机制

php 5.3之前使用的垃圾回收机制是单纯的“引用计数”,也就是每个内存对象都分配一个计数器,当内存对象被变量引用时,计数器+1;当变量引用撤掉后,计数器-1;当计数器=0时,表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成。

“引用计数”存在问题,就是当两个或多个对象互相引用形成环状后,内存对象的计数器则不会消减为0;这时候,这一组内存对象已经没用了,但是不能回收,从而导致内存泄露;

php5.3开始,使用了新的垃圾回收机制,在引用计数基础上,实现了一种复杂的算法,来检测内存对象中引用环的存在,以避免内存泄露。

该算法可以参考下面这篇文章,这是这篇小总结的主要参考文献:) :浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

2.查看内存是否泄露

看是否有该释放的内存没有被释放,可以简单的通过 调用 memory_get_usage 函数查看内存使用情况来判断;memory_get_usage 函数返回的内存使用数据据说不是很准确,可以使用 php 的 xdebug 扩展来获得更准确翔实的内存使用情况。

class A{
    private $b;
    function __construct(){
        $this->b = new B($this);
    }
    function __destruct(){
        //echo "A destruct\n";
    }
}

class B{
    private $a;
    function __construct($a){
        $this->a = $a;
    }
    function __destruct(){
        //echo "B descturct\n";
    }
}

for($i=0;;$i++){
    $a = new A();
    if($i%1000 == 0){
        echo memory_get_usage()."\n";
    }

}

}

上面就构造了一个会产生环状引用的例子;每次创建一个A对象的实例a,a就创建一个B对象的实例b,同时让b引用a ;这样,每个A对象永远被一个B引用,而每个B对象同时被一个对象A引用;引用环就这样产生了。

在php5.2的环境下执行这段代码,会发现内存使用在单调上涨,也没有A和B的析构函数被执行后输出的“A/B desctruct”信息;直到内存耗尽,输出“PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 40 bytes)”。

在php5.3的环境下执行这段代码,则发现内存使用在上跳下窜,但是永远没有超过一个限额;程序也会输出大量的“A/B desctruct”,这说明析构函数被调用了。

我的同事的程序中,就存在这种引用的环路,而他的脚本,实在php5.2.3下执行的。simple_html_dom工具中,有两个类,分别是simple_html_dom和simple_html_dom_node,前者中有一个数组成员变量nodes,数组中每个元素都是一个simple_html_dom_node对象;而每个simple_html_dom_node对象都有一个成员变量dom,该dom的值就是前面的simple_html_dom对象——这样就形成了一个漂亮的引用环,导致了内存泄露。解决的办法也很简单,就是simple_html_dom对象在使用完毕时,主动调用其clear函数,清空其成员变量nodes,环就被打破了,内存泄露也就不会发生了。

3.其他:

1)垃圾回收的时机

Php中,引用计数为0,则内存立刻释放;也就是说,不存在环状引用的变量,离开变量的作用域,内存被立刻释放。

环状引用检测则是在满足一定条件下触发,所以在上面的例子中,会看到使用的内存有大幅度的波动;也可以通过 gc_collect_cycles 函数来主动进行环状引用检测。

2) &符号的影响

显式引用一个变量,会增加该内存的引用计数:

$a = “something”;

$b = &$a;

此时unset($a), 但是仍有$b指向该内存区域的引用,内存不会释放。

3)unset函数的影响

unset只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数-1;在上面的例子中,循环体内部,$a=new A(); unset($a);并不会将$a的引用计数减到零;

4)= null 操作的影响;

$a = null 是直接将$a 指向的数据结构置空,同时将其引用计数归0。

5)脚本执行结束的影响

脚本执行结束,该脚本中使用的所有内存都会被释放,不论是否有引用环。

抱歉!评论已关闭.