【问题标题】:Get the reference count of an object in PHP?获取PHP中对象的引用计数?
【发布时间】:2011-04-15 10:45:06
【问题描述】:

我意识到对这个问题的下意识回答是“你不知道”,但请听我说完。

基本上,我在 SQL 上的活动记录系统上运行,为了防止同一数据库行的重复对象,我在工厂中为每个当前加载的对象保留一个“数组”(使用自动增量 'id'作为关键)。

问题是,当我尝试通过这个系统处理 90,000+ 行时,PHP 会遇到内存问题。这很容易通过每隔几百行运行一次垃圾收集来解决,但不幸的是,由于工厂存储了每个对象的副本 - PHP 的垃圾收集不会释放这些节点中的任何一个。

我能想到的唯一解决方案是检查存储在工厂中的对象的引用计数是否等于 1(即没有引用该类),如果是,则释放它们。这将解决我的问题,但是 PHP 没有引用计数方法? (除了debug_zval_dump,但那几乎没用)。

【问题讨论】:

  • 你只是将ID存储在数组中还是整个对象中?
  • ID 是键,整个对象的“引用”是值: static $cache = array(1122=>new Client(1222), 1864=>new Client(1864), ...)

标签: php


【解决方案1】:

PHP 7.4 现在有 WeakReference

要知道$obj 是否被其他东西引用,您可以使用:

// 1: create a weak reference to the object
$wr = WeakReference::create($obj);

// 2: unset our reference
unset($obj);

// 3: test if the weak reference is still valid
$res = $wr->get();
if (!is_null($res)) {
    // a handle to the object is still held somewhere else in addition to $obj
    $obj = $res;
    unset($res);
}

【讨论】:

    【解决方案2】:

    我知道这是一个非常古老的问题,但它仍然是搜索结果中的热门结果,所以我想我会给你一个“正确”的答案来解决你的问题。

    不幸的是,你发现引用计数是一个雷区,但实际上你不需要它来解决可能需要它的 99% 的问题。

    你真正想要使用的是WeakRef 类,很简单,它持有一个对象的弱引用,如果没有其他对该对象的引用,它将过期,允许垃圾收集器清理它.它需要通过 PECL 安装,但它确实是您在每个 PHP 安装中都需要的东西。

    你会这样使用它(请原谅任何错别字):

    class Cache {
        private $max_size;
        private $cache = [];
        private $expired = 0;
    
        public function __construct(int $max_size = 1024) { $this->max_size = $max_size; }
    
        public function add(int $id, object $value) {
            unset($this->cache[$id]);
            $this->cache[$id] = new WeakRef($value);
    
            if ($this->max_size > 0) && ((count($this->cache) > $this->max_size)) {
                $this->prune();
                if (count($this->cache) > $this->max_size) {
                    array_shift($this->cache);
                }
            }
        }
    
        public function get(int $id) { // ?object
            if (isset($this->cache[$id])) {
                $result = $this->cache[$id]->get();
                if ($result === null) {
                    // Prune if the cache gets too empty
                    if (++$this->expired > count($this->cache) / 4) {
                        $this->prune();
                    }
                } else {
                    // Move to the end so it is culled last if non-empty
                    unset($this->cache[$id]);
                    $this->cache[$id] = $result;
                }
                return $result;
            }
            return null;
        }
    
        protected function prune() {
            $this->cache = array_filter($this->cache, function($value) {
                return $value->valid();
            });
        }
    }
    

    这是使用弱引用和最大大小的过度杀伤版本(将其设置为 -1 以禁用它)。基本上,如果它变得太满或太多结果过期,那么它会修剪任何空引用的缓存以腾出空间,并且只有在必要时才删除非空引用。

    【讨论】:

    • 或者只使用来自同一个 PECL 扩展的WeakMap!它可以像数组一样直接使用,无需将每个元素包装在 WeakRef 中。
    【解决方案3】:

    似乎最好的答案仍然是获取引用计数,尽管 debug_zval_dump 和 ob_start 太丑陋了,无法包含在我的应用程序中。

    相反,我编写了一个带有 refcount() 函数的简单 PHP 模块,可在:http://github.com/qix/php_refcount

    【讨论】:

    • 请注意,这将始终为引用的变量返回 1 的引用计数(由于写时复制)。
    • @ircmaxell,“引用变量” 是什么意思? php.net/manual/en/function.debug-zval-dump.php#example-5193 似乎说 refcount 为 2 是很有可能的。
    • @Pacerier:有可能。对于具有引用的变量。链接代码(扩展)接受一个变量值。这意味着如果原始变量是一个引用(is_ref 设置为1),该变量将在扩展的函数被调用之前被分叉。因此,如果一个变量在进入该函数之前是一个引用,则返回的引用计数将始终1,这要归功于写时复制...
    • 不错。我看到您采用“如果您不喜欢引擎的工作方式,请更改它”的方法:-)
    • 我认为任何记录在案的解决方法都比仅仅为了获取引用计数而必须安装和维护扩展要好得多。
    【解决方案4】:

    Sean 的 debug_zval_dump 函数看起来可以告诉您引用计数,但实际上,从长远来看,引用计数对您没有帮助。

    您应该考虑使用有界数组作为缓存;像这样:

    <?php
    class object_cache {
       var $objs = array();
       var $max_objs = 1024; // adjust to fit your use case
    
       function add($obj) {
          $key = $obj->getKey();
          // remove it from its old position
          unset($this->objs[$key]);
          // If the cache is full, retire the eldest from the front
          if (count($this->objs) > $this->max_objs) {
             $dead = array_shift($this->objs);
             // commit any pending changes to db/disk
             $dead->flushToStorage();
          }
          // (re-)add this item to the end
          $this->objs[$key] = $obj;
       }
    
       function get($key) {
          if (isset($this->objs[$key])) {
              $obj = $this->objs[$key];
              // promote to most-recently-used
              unset($this->objs[$key]);
              $this->objs[$key] = $obj;
              return $obj;
          }
          // Not cached; go and get it
          $obj = $this->loadFromStorage($key);
          if ($obj) {
              $this->objs[$key] = $obj;
          }
          return $obj;
       }
    }
    

    这里,getKey() 为您要存储的对象返回一些唯一的 id。 这依赖于 PHP 记住插入其哈希表的顺序这一事实;每次添加新元素时,它都会在逻辑上附加到数组中。

    get() 函数确保您访问的对象保留在数组的末尾,因此数组的前面将是最近最少使用的元素,这是我们要处理的元素当我们确定空间较小时; array_shift() 为我们做这件事。

    这种方法也称为最近使用或 MRU 缓存,因为它会缓存最近使用的项目。这个想法是您更有可能访问您最近访问过的项目,因此您将它们保留在身边。

    您在这里获得的是控制您保留的最大对象数量的能力,并且您不必在故意难以访问的 php 实现细节上四处寻找。

    【讨论】:

    • 这是个好主意,虽然它并没有解决核心问题。如果缓存中的对象在其他地方被引用,则调用它 $o1;然后使用 MRU 从缓存中删除对象;然后再次加载为 $o2。现在 $o1 != $o2,这很不方便——但更重要的是,如果我在 $o1 上设置一个属性,它不会传递给 $o2; $o1->信用+= 50; $o1->保存(); $o2->信用+= 10; $o2->保存(); -- 如果是同一个对象 ->credit 会增加 60,但是会被覆盖。
    • 是的,这将是一个问题,但内存中有 90k 个对象的问题是您无法将它们放入内存中。
    • 呃,回车提交评论,太糟糕了。无论如何,MRU 缓存的目的是您始终通过缓存请求您的对象,并让它处理删除您不需要的项目。如果您确实需要一次在内存中拥有 90k 个对象,那么 MRU 和引用计数技巧都无法帮助您——您需要购买更多内存。
    【解决方案5】:

    是的,您绝对可以从 PHP 中获取引用计数。不幸的是,引用计数并不容易获得,因为它没有内置于 PHP 中的访问器。没关系,因为我们有 PREG!

    <?php
    function refcount($var)
    {
        ob_start();
        debug_zval_dump($var);
        $dump = ob_get_clean();
    
        $matches = array();
        preg_match('/refcount\(([0-9]+)/', $dump, $matches);
    
        $count = $matches[1];
    
        //3 references are added, including when calling debug_zval_dump()
        return $count - 3;
    }
    ?>
    

    来源:PHP.net

    【讨论】:

    • 这不太可能,主要是因为可能不会直接要求对象提供值 - 我可能会引用它之前返回的内容......无论如何,PHP 为我保留了一个引用计数- 我不应该只能使用它吗?
    • 是的,你是对的。我想避免 PREG,因为您正在处理 90K 条目。我把执行时间放在易于编码之前。我更新了我的答案以反映获取引用计数的方式。
    • 我知道使用 debug_zval_dump 的能力,尽管它有点疯狂。该函数在我的类上输出数千行数据(他们的所有孩子和各自的数据)。当我开始使用它时,我遇到了执行时错误——比脚本退出时更糟糕的是,它有时会将一些 debug_zval 数据转储给用户,因为它正在被写入输出缓冲区。
    • 是的,它想让你变得亲密,让你了解每个指针的生活故事。除此之外,我没有弹药了。也许制作一个数组,其键对每个对象都是可识别的。如果键存在,则忽略该对象。
    • @Sean,您的函数不适用于数组和对象以及循环引用......
    猜你喜欢
    • 2011-02-25
    • 1970-01-01
    • 1970-01-01
    • 2010-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多