弱参考 - 它们有多大用处?

问题描述 投票:33回答:4

所以我最近一直在考虑一些自动内存管理的想法 - 具体来说,我一直在寻找基于引用计数实现内存管理器。当然,每个人都知道循环引用会杀死天真的引用计数。解决方案:弱引用。就个人而言,我讨厌以这种方式使用弱引用(还有其他更直观的方法来解决这个问题,通过循环检测),但它让我思考:弱引用还有什么用处?

我认为它们必然存在某种原因,特别是在具有跟踪垃圾收集的语言中,它们不会受到循环参考陷阱的影响(C#和Java是我熟悉的,Java甚至有三种弱引用!)。但是,当我试图为他们找到一些可靠的用例时,我几乎只有像“使用它们来实现缓存”这样的想法(我已经在SO上看过几次)。我也不喜欢它,因为它们依赖于跟踪GC在不再强烈引用之后可能不会立即收集对象的事实,除非在低内存情况下。这些类型的情况对于引用计数GC是完全无效的,因为对象在不再被引用之后立即被销毁(除了可能在循环的情况下)。

但这真让我感到疑惑:弱引用怎么可能有用呢?如果你不能指望它引用一个对象,并且它不需要像打破周期那样,那么为什么要使用它?

weak-references
4个回答
38
投票

事件处理程序是弱引用的一个很好的用例。触发事件的对象需要引用对象以调用事件处理程序,但是您通常不希望事件生成器的引用保留以阻止事件使用者进行GC。相反,您希望事件生成器具有弱引用,然后它将负责检查引用的对象是否仍然存在。


12
投票

但这真让我感到疑惑:弱引用怎么可能有用呢?如果你不能指望它引用一个对象,并且它不需要像打破周期那样,那么为什么要使用它?

我有一种公认的教条主义观点,即弱引用实际上应该是持久存储对具有强引用的对象的引用的默认方式,需要更明确的语法,如下所示:

class Foo
{
    ...
    // Stores a weak reference to bar. 'Foo' does not
    // own bar.
    private Bar bar;

    // Stores a strong reference to 'Baz'. 'Foo' does
    // own Baz.
    private strong Baz baz;
}

...与函数/方法内的本地人相反:

void some_function()
{
    // Stores a strong reference to 'Bar'. It will
    // not be destroyed until it goes out of scope.
    Bar bar = ...;

    // Stores a weak reference to 'Baz'. It can be
    // destroyed before the weak reference goes out 
    // of scope.
    weak Baz baz_weak = ...;

    ...

    // Acquire a strong reference to 'Baz'.
    Baz baz = baz_weak;
    if (baz)
    {
        // If 'baz' has not been destroyed,
        // do something with it.
        baz.do_something();
    }       
}

恐怖故事

为了理解为什么我有这种坚定的意见以及为什么弱引用是有用的,我将根据我在一家全面接受GC的公司的经验分享个人故事。

这是一个3D产品,它处理网格和纹理等重要的东西,其中一些可以单独跨越内存中的千兆字节。该软件围绕场景图和插件架构展开,其中任何插件都可以访问场景图和内部元素,如纹理或网格或灯光或相机。

现在发生的事情是团队和我们的第三方开发人员对弱引用并不那么熟悉,因此我们让人们将对象引用存储在场景图中的左右两侧。相机插件将存储要从相机视图中排除的强对象引用列表。渲染器将​​存储用于渲染的对象列表,如光引用列表。灯光的作用类似于相机并具有排除/包含列表。着色器插件将存储对其使用的纹理的引用。这个清单一直在继续。

实际上,在我们发现了这么多漏洞后,我们的团队必须在开发一年后对我们团队的弱引用的重要性进行演示,尽管我不是那个推动使用GC的设计决策的人(我实际上是反对这个)。我还必须在演示之后实现对我们的专有垃圾收集器的弱引用的支持,因为我们的垃圾收集器(由其他人编写)甚至不支持最初的弱引用。

逻辑泄漏

当然,我们最终得到了一个软件,当用户想要从场景中删除对象时,如网格或纹理,而不是释放内存,应用程序只是继续使用内存,因为某些东西,在整个大的某个地方codebase仍然保持对这些场景对象的引用,并且在用户明确请求时不让它们运行。即使在清除场景后,软件也可能占用3 GB的内存,使用时间越长,内存就越多。这都是因为代码库(包括第三方开发人员)在适当的时候未能使用弱引用。

因此,当用户请求从场景中删除网格时,可能存储了对给定网格的引用的9/10个位置将正确地释放引用,将其设置为空引用或从列表中删除引用以允许垃圾收集器来收集它。然而,通常会有第十个地方忘记处理这样的事件,将网格保留在内存中,直到该事物本身也从场景中移除(有时这些事物生活在场景之外并存储在应用程序根目录中) 。而且有时级联到软件会消耗越来越多内存的时间越长,处理插件(即使在清除场景后仍然存在)也会延长整个场景本身的生命周期。 DI的场景根注入引用,此时即使清除整个场景也不会释放内存,要求用户每隔一两个小时定期重启软件,以使其恢复到合理的内存使用量。

这些都不容易发现。我们只能看到应用程序运行的时间越长,内存越来越多。这不是我们可以在短期单元或集成测试中轻松再现的东西。有时经过数小时的详尽调查后,我们发现甚至不是我们自己的代码导致这些内存泄漏。它位于第三方插件中,用户经常使用这个插件,最终存储了对网格或纹理的引用,而这些引用并未响应场景移除事件而释放。

在用垃圾收集语言编写的软件中,存在泄漏越来越多内存的倾向,程序员在适当时不小心使用弱引用。理想情况下,在对象不拥有另一个对象的所有情况下,应使用弱引用。应该有更多的情况,这比有力的参考更有意义。对于引用所有内容的每个对象来说,分享所有内容的所有权是没有意义的。对于大多数软件而言,最明智的设计是系统中有一件事要拥有另一件事,比如“场景图拥有场景对象”,而不是“摄像机也拥有网格,因为它们在摄像机排除列表中引用它们”。

害怕!

现在GC在一个大型,性能关键的软件中非常可怕,这种逻辑泄漏会导致应用程序在很长一段时间内占用数百GB的内存,而在运行它的时间越长就越慢。 ,快速启动,然后慢慢变慢,直到重新启动它。

当你试图调查所有这些泄漏的来源时,你可能会看到2000万行代码,包括更多的控件,由插件开发人员编写,并且这些行中的任何一行都可以默默地延长其生命周期。对象远远超过适当的时间,仅仅存储一个对象引用,并且无法释放它以响应适当的事件。更糟糕的是,所有这些都在质量保证和自动化测试的雷达下飞行。

在这样的环境中,这是一个噩梦般的场景,我认为避免这种情况的唯一合理方式是,如果您使用GC或者首先避免使用GC,那么编码标准很大程度上依赖于弱引用。

GC泄漏

现在我从来没有对垃圾收集最积极的看法,这是因为至少在我的领域,没有必要让一个逻辑资源泄漏在测试的雷达之下,例如悬挂指针崩溃可以很容易地检测和复制,并且很可能在开发人员提交他的代码之前由开发人员纠正,如果有合理的测试和CI程序的话。

在我的特殊情况下,如果我们在邪恶中选择最容易被发现和重现的错误,那么GC类型的资源泄漏并不容易被发现,并且在任何意义上都不容易重现,这有助于你发现泄漏的来源。

但是,我对GC的看法在团队和代码库之间变得更加有利,这些团队和代码库大量使用弱引用,并且只使用强引用,从高级设计的角度来看它实际上是有意义的,以延长对象的生命周期。

GC不是防止内存泄漏的实际防范,恰恰相反。如果是这样,世界上漏洞最少的应用程序将用支持GC的语言编写,如Flash,Java,JavaScript,C#,并且可以想象的最漏洞的软件将用C语言等手动内存管理的语言编写,此时Linux内核应该是一个漏洞操作系统的地狱,需要每隔一两个小时重新启动以减少内存使用。但事实并非如此。它通常与针对GC编写的泄漏应用程序完全相反,这是因为GC实际上倾向于使得更难以避免逻辑泄漏。它有帮助的地方是避免物理泄漏(但物理泄漏很容易在一开始就检测和避免,无论你使用什么语言),它确实有助于防止在任务关键型软件中悬空指针崩溃,这是更理想的泄漏内存而非崩溃,因为一个人的生命受到威胁,或者因为崩溃可能导致服务器无法连续数小时无法使用。我不在任务关键领域工作;我在性能和内存关键的工作,史诗数据集正在处理每个渲染的帧。

毕竟,我们要做的就是用GC创建逻辑泄漏:

class Foo
{
     // This makes 'Foo' instances cause 'bar' to leak, preventing
     // it from being destroyed until the 'Foo' instances are also
     // destroyed unless the 'Foo' instances set this to a null 
     // reference at the right time (ex: when the user requests 
     // to remove whatever Bar is from the software).
     private Bar bar;
}

......但弱引用不会冒这个问题。当你一方面看上面的数百万个LOC和另一方面的史诗内存泄漏时,你必须调查哪个类比的Foo未能将类比的Bar设置为适当的空引用,这是一个噩梦般的场景。时间,因为这是一个非常可怕的部分:只要您忽略千兆字节的内存泄漏,代码就可以正常工作。什么都没有触发任何类型的错误/异常,断言失败等。没有什么事情崩溃。所有单位和整合都没有投诉。这一切都正常工作,只是它正在泄漏千兆字节的内存,导致用户投诉左右,而整个团队都在摸索代码库的哪些部分泄漏,而QA试图通过实际建议用户进行损害控制时没有做什么保存他们的工作并每半小时重新启动一次软件,好像这应该是某种解决方案。

弱参考帮助很多

因此,请在适当的时候使用弱引用,并且适当地,我的意思是当一个对象在另一个对象中共享所有权没有意义时。

它们非常有用,因为您仍然可以检测对象何时被销毁而不延长其生命周期。当您真正需要延长对象的生命周期时,强引用很有用,例如在短期线程内部,以便在线程完成处理之前不销毁对象,或者在真正有意义的对象内部另一个。

使用我的场景图示例,相机排除列表不需要拥有场景图已经拥有的场景对象。从逻辑上讲,这是没有意义的。如果我们在绘图板上,没有人会想,“是的,除了场景图形本身之外,相机还应该拥有场景对象。”

它只需要那些引用就能够轻松地返回那些元素。当它这样做时,它可以从处理它们之前存储的弱引用中获取对它们的强引用,并且还检查它们是否在这样做之前被用户删除,而不是将它们的生命周期可能无限延长到这一点。内存泄漏的地方,直到相机本身也被移除。

如果相机想要使用一种方便的懒惰实现,而不必费心去除场景移除事件,那么弱引用至少允许它在不泄漏整个地方的大量内存的情况下执行此操作。弱事引用仍然允许它事后发现何时从场景中移除了对象,并且可能从列表中删除被破坏的弱引用,而不必烦扰场景移除事件。对我来说理想的解决方案是使用弱引用并处理场景删除事件,但至少相机排除列表应该使用弱引用,而不是强引用。

团队环境中弱引用的有用性

这触及了对我的弱引用的有用性的核心。如果团队中的每个开发人员在适当的时间彻底删除/清空对象引用以响应适当的事件,则绝不需要它们。但至少在大型团队中,工程标准未能完全阻止的可能发生的错误通常会最终发生,有时甚至会以惊人的速度发生。而且弱引用是一种极好的防御措施,可以防止应用程序围绕GC运行时出现逻辑泄漏的趋势。在我看来,它们是一种防御机制,可以帮助将以难以检测的内存泄漏形式出现的错误转换为对已经破坏的对象的无效引用的易于检测的使用。

安全

从组装程序员可能没有太多用于类型安全的相同意义上看,它们似乎不太有用。毕竟,他只需要原始的位和字节以及适当的汇编指令即可完成所需的一切。但是,类型安全性有助于通过使人类开发人员更明确地表达他们想要做的事情并限制他们允许对特定类型执行的操作来更轻松地检测人为错误。我看到类似意义上的弱引用。它们有助于检测人为错误,否则如果不使用弱引用则会导致资源泄漏。它故意对自己施加限制,例如,“好吧,这是对象的弱引用,因此它不可能延长其寿命并导致逻辑泄漏”,这是不方便的,但是对于汇编程序员来说类型安全性也是如此。它仍然可以帮助防止一些非常讨厌的错误。

它们是一种语言安全功能,如果你问我并喜欢任何安全功能,它不是绝对必需的,你通常不会欣赏它,直到你遇到一个团队一遍又一遍地绊倒相同的东西,因为这样的安全功能缺乏或没有充分利用。对于独立开发人员来说,安全通常是最容易忽视的事情之一,因为如果你有能力和谨慎,你真的可能不会亲自需要它。但是,整个团队的混合技能会增加错误的风险,安全功能可以成为你迫切需要的东西,而人们开始类似地在湿地板上左右滑动,你每天都小心避免,导致尸体堆积在你身边。我找到了大型团队,如果你没有一个易于遵循的编码标准,而是用铁拳放下安全的工程实践,在一个月之内,你就可以积累超过十万行极端有错误,难以检测的错误的错误代码,如上面提到的GC逻辑泄漏。在没有标准的情况下可以在短短一个月内累积的破坏代码量就可以防止常见错误。

无论如何,我对这个问题有点教条,但意见形成于一大堆史诗般的内存泄漏,为此,我看到的唯一答案只是对开发人员说:“小心点!你们这样的内存就像泄露一样疯!”是为了让他们更频繁地使用弱引用,此时任何粗心都不会转化为大量的内存泄露。它实际上已经到了我们在后见之明发现了如此多泄漏的地方,在测试的雷达下飞行,我故意在我们的SDK中破坏了源兼容性(虽然不是二进制兼容性)。我们曾经有这样的约定:

typedef Strong<Mesh> MeshRef;
typedef Weak<Mesh> MeshWeakRef;

...这是在C ++中实现的专有GC,在单独的线程中运行。我改成了这个:

typedef Weak<Mesh> MeshRef;
typedef Strong<Mesh> MeshStrongRef;

...而语法和命名约定的简单改变极大地帮助防止了更多的泄漏,除了我们做了几年太晚了,使得损害控制比其他任何事情都要多。


5
投票

WeakReference引用的对象可以在gc进程之前访问。

因此,如果我们想要拥有对象的信息,只要它存在,我们就可以使用WeakReference。例如,调试器和优化器通常需要具有对象的信息,但它们不希望影响GC过程。

顺便说一下,SoftReference与WeakReference不同,因为只有在内存不足时才会收集相关对象。因此,SoftReference通常用于构建全局缓存。


0
投票

我经常将WeakReferenceThreadLocalInheritableThreadLocal结合使用。如果我们希望一个值在有意义的情况下可被多个线程访问,但后来从这些线程中删除了值,我们实际上无法自己释放内存,因为没有办法篡改一个ThreadLocal值。除当前线程之外的线程。但是你可以做的是将值放在WeakReference中的那些其他线程中(当创建值时 - 这假设同一个实例在多个线程中共享;请注意,这仅在只有一部分线程应该有权访问时才有意义这个值或你只是使用静态)并在另一个ThreadLocal中存储一个硬引用,用于某个将要删除该值的工作线程。然后,当值不再有意义时,您可以要求工作线程删除硬引用,这会导致所有其他线程中的值立即排队以进行垃圾回收(尽管它们可能不会立即被垃圾收集,因此值得有一些其他方法来防止访问该值)。

© www.soinside.com 2019 - 2024. All rights reserved.