学说:为什么在通过关联访问实体时我不能释放内存?

时间:2015-08-12 19:17:08

标签: symfony doctrine-orm

我的ApplicationApplicationFile

有关系
/**
 * @ORM\OneToMany(
 *   targetEntity="AppBundle\Entity\ApplicationFile",
 *   mappedBy="application",
 *   cascade={"remove"},
 *   orphanRemoval=true
 * )
 */
private $files;

文件实体有一个存储二进制数据的字段,最大可达2MB。迭代大量应用程序及其文件时,PHP内存使用量会增加。我想保持低调。

我试过这个:

$applications = $this->em->getRepository('AppBundle:Application')->findAll();
foreach ($applications as $app) {
  ...
  foreach ($app->getFiles() as $file) {
    ...
    $this->em->detach($file);
  }
  $this->em->detach($app);
}

分离对象应该告诉实体管理器停止关心这个对象并取消引用它,但它令人惊讶地对内存使用量没有影响 - 它会不断增加。

相反,我必须手动加载应用程序文件(而不是通过关联方法检索它们),并且内存使用量不会增加。这有效:

$applications = $this->em->getRepository('AppBundle:Application')->findAll();
foreach ($applications as $app) {
  ...

  $appFiles = $this
      ->em
      ->getRepository('AppBundle:ApplicationFile')
      ->findBy(array('application' => $application));

  foreach ($appFiles as $file) {
    ...
    $this->em->detach($file);
  }
  $this->em->detach($app);
}

我使用xdebug_debug_zval来跟踪对$file对象的引用。在第一个例子中,有一个额外的引用,这解释了为什么内存膨胀 - PHP无法垃圾收集它!

有谁知道这是为什么?这个额外的参考在哪里,我该如何删除它?

编辑:在其循环结束时显式调用unset($file)无效。此时仍然有两个对象的引用(用xdebug_debug_zval证明)。一个包含在$file中(我可以解开),但是另一个我无法解开的其他地方。在主循环结束时调用$this->em->clear()也没有效果。

编辑2:解决方案:@origaminal的回答让我找到了解决方案,所以我接受了他的答案,而不是提供自己的答案。

在我通过$application上的关联访问文件的第一种方法中,这会产生一种副作用,即在$files对象I&#39上初始化以前未初始化的$application集合; m在外循环中迭代。

调用$em->detach($application)$em->detach($file)只会告诉Doctrine的UOW停止跟踪对象,但它不会影响$applications I' m的阵列迭代,现在已经填充了$files的集合,这会占用内存。

我完成后,我必须取消设置每个$application对象,以删除对已加载的$files的所有引用。为此,我修改了循环:

    $applications = $em->getRepository('AppBundle:Application')->findAll();
    $count = count($applications);
    for ($i = 0; $i < $count; $i++) {
        foreach ($applications[$i]->getFiles() as $file) {
            $file->getData();
            $em->detach($file);
            unset($file);
        }
        $em->detach($applications[$i]);
        unset($applications[$i]);

        // Don't NEED to force GC, but doing so helps for testing.
        gc_collect_cycles();
    }

4 个答案:

答案 0 :(得分:4)

级联

EntityManager::detach确实应该删除Doctrine对enities的所有引用。但对关联实体自动执行相同操作

您需要通过添加detach关联的cascade选项来级联此操作:

/**
 * @ORM\OneToMany(
 *   targetEntity="AppBundle\Entity\ApplicationFile",
 *   mappedBy="application",
 *   cascade={"remove", "detach"},
 *   orphanRemoval=true
 * )
 */
private $files;

现在$em->detach($app)应足以删除对Application实体及其关联的ApplicationFile实体的引用。

查找与收藏

我非常怀疑通过关联加载ApplicationFile实体,而不是将存储库用于findBy(),这是您的问题的根源。

确保通过关联加载时,Collection将引用这些子实体。但是当取消引用父实体时,整个树将被垃圾收集,除非有其他对这些子实体的引用。

我怀疑你展示的代码是伪/示例代码,而不是生产中的实际代码。请仔细检查该代码以找到其他参考文献。

清除

有时值得清除整个EntityManager并重新合并一些实体。您可以尝试$em->clear()$em->clear('AppBundle\Entity\ApplicationFile')

清除无效

您说清除EntityManager无效。这意味着您在EntityManager(UnitOfWork)中搜索的引用,因为您刚刚清除了它。

学说,但不是学说

您是否正在使用任何事件监听器或-subscribers?任何过滤器?任何自定义映射类型?多个实体管理员?还有什么东西可以融入Doctrine或它的生命周期,但不一定是Doctrine本身的一部分吗?

特别是在搜索问题根源时,事件监听器/订阅者经常被忽视。所以我建议你开始看那里。

答案 1 :(得分:1)

诀窍在PHP的垃圾收集器中,有点奇怪。首先关闭所有脚本需要内存时它会从RAM分配内存,即使你使用unset()$object = null或其他技巧来释放内存,分配的内存也不会返回给操作系统直到脚本没有完成并且与之相关的进程被杀死。

如何修复

  1. 通常在Linux系统上完成 创建使用limitoffset参数运行所需脚本的命令,并以小批量重新运行所需的脚本。通过这种方式,脚本将使用更少的内存,并且每次在脚本完成时释放内存。
  2. 摆脱Doctrine自己气球的记忆,PDO更快,成本更低。

答案 2 :(得分:1)

对于内存中的对象可能导致泄漏的那种任务,您应该使用Doctrine2 iterators

$appFiles = $this
      ->em
      ->getRepository('AppBundle:ApplicationFile')
      ->findBy(array('application' => $application));
应该重构

以返回query对象而不是ArrayCollection,然后从该查询对象中,您可以轻松调用iterate()方法并在每次对象检查后清理内存

修改

您有“隐藏引用”,因为detach操作不会删除内存中的对象,它只会告诉EntityManager不再处理它。这就是你应该使用我的解决方案或使用php函数unset()对象的原因。

答案 3 :(得分:1)

如果我们谈论您的第一个实现,您在PersistentCollection::coll属性的Application::files中有额外的链接指向该对象 - 该对象由Doctrine在Application实例化创建。

使用分离,您只需删除对象的UoW链接。

有许多方法可以解决这个问题,但应该应用很多黑客攻击。最好的方法可能是分离Application对象并取消设置。

但是,batch processing使用更高级的方法仍然更可取:有些是在另一个答案中列出的。当前的方式迫使doctrine使用代理并向DB抛出额外的查询以获取当前对象的文件。

修改

第一种和第二种实现之间的区别在于第二种情况下没有循环引用:Application::files保留未初始化PersistenceCollectioncoll中没有元素)。

要检查这一点 - 您可以尝试显式删除文件关联吗?