弱引用指针有什么用处?

15
我一直在研究垃圾回收,寻找适合用于我的编程语言的特性,然后我发现了"弱指针"。从这里得知:

弱指针类似于指针,但是由于来自弱指针的引用不会阻止垃圾回收,因此在使用弱指针之前必须检查其有效性。

弱指针与垃圾回收器交互,因为它们所指向的内存可能仍然有效,但是包含的内容可能与创建弱指针时不同。因此,每当垃圾回收器回收内存时,它必须检查是否有任何弱指针指向它,并将它们标记为无效(这不一定要以如此幼稚的方式实现)。

我以前从未听说过弱指针。虽然我想支持我的语言中的多种功能,但在这种情况下,我实在想不出有用的场景。那么,人们将如何使用弱指针呢?

我不知道...也许如果你只需要临时访问数据,垃圾回收器会比普通指针更快地清理它? - mpen
9个回答

9
一个非常重要的概念是缓存。让我们思考一下缓存的工作原理:
缓存的思想是将对象存储在内存中,直到内存压力变得如此之大,以至于一些对象需要被推出(或者当然可以被显式地使其失效)。所以你的缓存仓库对象必须以某种方式持有这些对象。通过弱引用持有它们,当垃圾收集器因为内存不足而寻找要回收的东西时,仅由弱引用引用的物品将出现作为垃圾回收的候选项。目前正在被其他代码使用的缓存中的项目将仍然具有活动的硬引用,因此这些项目将受到垃圾回收的保护。
在大多数情况下,您不需要自己编写缓存机制,但使用缓存是很常见的。假设您希望拥有一个属性,该属性引用缓存中的一个对象,并且该属性在长时间内保持有效。您希望首选从缓存中获取对象,但如果无法使用缓存,则可以从持久存储中获取。您还不希望迫使特定对象在内存中保留太久,因此您可以使用对该对象的弱引用,这将允许您在对象可用时获取它,但也允许其离开缓存。

我倾向于支持Asaph的观点;我以前实现过缓存,似乎有更好的方法来解决这个问题。 - Imagist
3
在Java中,缓存应该使用软引用(SoftReferences),而不是弱引用(WeakReferences)(弱引用的清除比软引用更积极)。根据被接受的答案,弱引用应该用于诸如附加对象属性之类的东西。 - Keith Randall
1
我同意Keith的观点。一个完美的垃圾回收器应该能够立即(且不会有额外开销)释放任何既没有强引用又没有被弱引用的对象。实现者们会非常努力地为您提供这样的垃圾回收器。但是,这种理想的垃圾回收器会使您的“缓存”几乎无用,其中的条目会像筛子中存储的一样掉落。垃圾回收器并不知道程序的速度/内存消耗权衡。程序员知道(或应该知道)。因此,程序员应该决定为缓存使用多少内存。 - Pascal Cuoq
@Pascal:确实,我在 OCaml 中尝试过这种缓存方法,但在理解弱引用的真正作用之前,垃圾回收器几乎立即收集了所有内容。对于缓存来说完全无用,因此,在我看来是错误的答案。 - J D
为什么大家都对这个答案进行攻击?如果你认为它是错误的,请说明为什么,而不仅仅是说它行不通。我之所以来到这里,是因为我不太理解弱指针,而这个回答的发布者似乎已经尽力提供了一个合理的用法。如果你不同意,请告诉我们原因;我相信还有其他人想知道。 - quant
显示剩余2条评论

8

一个典型的用例是存储额外的对象属性。假设您有一个具有固定成员集的类,而且您想要从外部添加更多成员。因此,您创建了一个字典对象 -> 属性,其中键是弱引用。然后,字典不会阻止键被垃圾回收;对象的删除也应该触发WeakKeyDictionary中值的删除(例如通过回调函数)。


6
扩展方法与 GC 和弱引用完全没有任何关系 - lubos hasko
@lubos 我猜测一旦扩展方法被加载到内存中,它就会一直存在,但是可以将其作为弱引用加载以减少对内存使用的影响。如果您将方法视为特定类型的对象属性,则这正是Martin所描述的用例。 - Imagist
@lubos:就实现而言,它们彼此之间不必做任何事情。但是,扩展方法允许使用我描述的方法从外部扩展类的方法,就像该方法允许使用其他属性扩展类一样。 - Martin v. Löwis
扩展方法只是静态方法的语法糖。它们与弱引用无关。 - BlueRaja - Danny Pflughoeft

5
如果你的语言的垃圾回收器无法收集循环数据结构,那么你可以使用弱引用来使其能够进行收集。通常情况下,如果你有两个对象彼此引用,但没有其他外部对象引用这两个对象中的任何一个,它们将成为垃圾回收的候选对象。但是,一个天真的垃圾回收器不会回收它们,因为它们彼此包含引用。
为了解决这个问题,你需要让一个对象对第二个对象有强引用,但第二个对象对第一个对象只有弱引用。然后,当第一个对象的最后一个外部引用消失时,第一个对象就成为垃圾回收的候选对象,紧随其后的是第二个对象,因为现在它唯一的引用是弱引用。

2
这对我来说似乎是一种糟糕的语言设计选择。在我的书中,需要程序员管理自己的内存的垃圾回收并不值得多少。 - Imagist

3

另一个例子......不完全是缓存,但类似:假设一个I/O库提供了一个对象,它包装了一个文件描述符并允许访问该文件。当对象被收集时,文件描述符会被关闭。希望能够列出所有当前打开的文件。如果您对此列表使用强指针,则文件永远不会关闭。


1

当您希望保留对象的缓存列表但不阻止这些对象被垃圾回收时,请使用它们,如果对象的“真实”所有者完成,则可以进行垃圾回收。

Web浏览器可能会具有历史记录对象,该对象保留对浏览器在其他地方加载并保存在历史记录/磁盘缓存中的映像对象的引用。 Web浏览器可能会过期其中一个图像(用户清除了缓存,缓存超时已过等),但页面仍将具有引用/指针。 如果页面使用弱引用/指针,则对象将按预期消失,并且内存将被垃圾收集。


弱指针的有效用法(我制定了以下准则以确定有效用法:“当设计明确区分了存在强指针和不存在强指针两种情况时,则是有效用法”)。 - Pascal Cuoq

0

弱指针可以防止它们所指向的对象成为“生命支持系统”的形式。

假设您有一个Viewport类、两个UI类和一堆Widget类。您希望您的UI控制其创建的小部件的生命周期,因此您的UI保留了对所有受其控制的小部件的SharedPtrs。只要您的UI对象存在,它引用的任何小部件都不会被垃圾回收(感谢SharedPtr)。

但是,Viewport是实际执行绘图的类,因此您的UI需要将指向小部件的指针传递给Viewport以便它可以绘制它们。出于某种原因,您想将活动UI类更改为另一个UI类。让我们考虑两种情况,一种是UI传递了Viewport WeakPtrs,另一种是传递了SharedPtrs(指向小部件)。

如果您将所有小部件作为WeakPointers传递给Viewport,那么一旦删除UI类,就不会再有SharedPtrs指向小部件,因此它们将被垃圾回收,Viewport对对象的引用也不会使它们保持在“生命支持系统”上,这正是您想要的,因为您甚至不再使用该UI,更不用说它创建的小部件了。

现在,假设您已经将一个SharedPointer传递给了Viewport,您删除了UI,但Widget并没有被垃圾回收!为什么?因为Viewport仍然存在一个数组(向量或列表等),其中包含指向Widget的SharedPtr。实际上,Viewport已成为它们的一种“生命支持”,即使您已经删除了控制另一个UI对象的Widget的UI。
通常,语言/系统/框架会自动垃圾回收任何东西,除非内存中某个地方有一个“强”引用指向它。想象一下,如果每个东西都有对每个东西的强引用,那么什么也不会被垃圾回收!有时您需要这种行为,有时则不需要。如果使用WeakPtr,并且没有Shared/StrongPtr指向该对象(只有WeakPtr),则尽管存在WeakPtr引用,对象也将被垃圾回收,并且WeakPtr应设置为NULL(或删除或其他操作)。
同样,当您使用WeakPtr时,基本上允许您所提供的对象能够访问数据,但WeakPtr不会像SharedPtr那样防止指向的对象被垃圾回收。当您考虑SharedPtr时,请想到“生命支持”,而WeakPtr则没有“生命支持”。垃圾回收(通常)直到对象没有生命支持才会发生。

0

拥有弱引用的一个重要原因是为了处理对象可能作为管道连接信息或事件源到一个或多个监听器的情况。如果没有任何监听器,就没有理由继续向管道发送信息。

例如,考虑一个可枚举集合,在枚举期间允许更新。该集合可能需要通知任何活动的枚举器它已经被更改,以便这些枚举器可以相应地调整自己。如果一些枚举器被其创建者抛弃,但集合对它们持有强引用,那么只要集合存在,这些枚举器将继续存在(并处理更新通知)。如果集合本身将在应用程序的生命周期内存在,那么这些枚举器将成为永久性的内存泄漏。

如果集合对枚举器持有弱引用,这个问题可以在很大程度上得到解决。如果枚举器被抛弃,即使集合仍然对它持有弱引用,它也将有资格进行垃圾回收。下次集合被更改时,它可以查看其弱引用列表,向仍然有效的枚举器发送更新,并从列表中删除无效的枚举器。

使用终结器和一些额外的对象可以实现许多弱引用的效果,而且可能使这样的实现比使用弱引用更高效,但是存在许多陷阱,很难避免错误。使用WeakReference可以更容易地实现正确的方法。该方法可能不是最优化的效率,但不会出现严重故障。


-1

弱引用可以在缓存场景中使用 - 通过弱引用可以访问数据,但如果长时间不访问数据或内存压力较大,则垃圾回收器可以释放它。


1
-1:再次强调,这个例子恰恰是你不应该使用弱指针的情况。 - J D
我不明白...为什么这个回答会被踩,而在它上面的完全相同的回答却得到了7个赞。 - Logicsaurus Rex

-1
垃圾回收的原因在于,在像C这样的语言中,内存管理完全由程序员显式控制,当对象所有权在线程之间或更难的是在共享内存的进程之间传递时,避免内存泄漏和悬空指针可能会变得非常困难。如果这还不够困难,您还必须处理需要访问比一次可以容纳的更多对象的情况 - 您需要有一种方法暂时释放一些对象,以便其他对象可以在内存中。

因此,一些语言(例如Perl、Lisp、Java)提供了一种机制,您可以停止“使用”对象,垃圾收集器最终会发现这一点,并释放用于对象的内存。它可以正确地执行此操作,而无需程序员担心他们可能犯错的所有方式(尽管程序员可能会出现很多错误)。

如果您将访问对象的次数概念性地乘以计算对象值所需的时间,可能再乘以没有准备好对象的成本或对象的大小,因为在内存中保留大型对象可能会防止保留几个较小的对象,那么您可以将对象分类为三类。

有些对象非常重要,您希望明确地管理它们的存在——它们不会由垃圾回收器管理,或者必须在显式释放之前永远不会被回收。有些对象计算成本低廉,体积小,访问频率不高,或具有类似的特征,使它们可以随时进行垃圾回收。

第三类是那些重新计算成本高昂但可以重新计算、访问频率较高(可能是短时间内的突发访问),大小较大等等的对象。您希望尽可能长时间地将它们保留在内存中,因为它们可能会被再次重用,但您不希望因为关键对象需要而耗尽内存。这些对象适合使用弱引用。

如果这些对象不与关键资源冲突,您希望尽可能长时间地保留它们,但如果需要内存来处理关键资源,则应该将它们删除,因为需要时可以重新计算。这就是弱指针的作用。

一个例子是图片。假设您有一个包含数千张图片的网页。您需要知道要布局多少张图片,也许您需要进行数据库查询以获取列表。保存几千个项目列表所需的

你一次只能在网页的一个窗格中展示几十张图片。你不需要获取用户看不到的图片的位数。当用户滚动页面时,你将收集可见图片的实际位数。这些图片可能需要很多兆字节才能显示它们。如果用户在几个滚动位置之间来回滚动,你希望不必反复重新获取那些兆字节。但你不能一直将所有图片保存在内存中。所以你使用弱指针。
如果用户一遍又一遍地只看几张图片,它们可能会留在缓存中,你就不必重新获取它们。但是如果他们滚动得足够多,你需要释放一些内存,以便可以获取可见的图片。使用弱引用,你在使用它之前检查引用。如果仍然有效,你就使用它。如果无效,你就进行昂贵的计算(获取)以获取它。

2
给你一个观察结果,以帮助你提供更好的回答:问题是“为什么弱指针有用?”,而不是“缓存是什么,弱指针如何有助于解决?”。尽量简洁明了地表达要点,并且如果问题需要详细回答,请利用格式标记来使你的回答更易读。 - Artelius
1
-1:这个例子恰恰是你不应该使用弱指针的情况。垃圾收集器只有在所有资源被消耗完之前才会泄漏。它们的收集非常积极。如果您使用GC来驱逐缓存行,那么它们很可能无法持续1毫秒,因为它们甚至无法在下一代0收集中幸存。 - J D

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接