3

Java弱引用(WeakReferences)

 3 years ago
source link: https://zxs.io/article/1442
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

  前一段时间当我面试有些来应聘高级java开发工程师岗位的候选人时,在我问的众多问题中,有个问题是“你能告诉我弱引用是啥吗”,我不期望得到像论文中的细节一样的答案。我很可能从有个20多年的老工程师口中得到“嗯……是不是和gc有关”这样的答案,所有哪些至少有5年以上经验的工程师只有两个人知道弱引用的存在,只有其中一个知道引用的相关知识。我甚至尝试给他们解释下看是否有人会有“哦,原来是这样”的反应,然而并没有。我不确定为啥这个知识点鲜为人知,但自Java1.2之后发布的弱引用确实是有个非常有用的功能。

虽然作为一个java工程师我不建议你成为弱引用的专家,但我认为你至少应该知道他们是啥。换句话说你应该知道如何用他们。一直以来弱引用貌似是一个鲜为人知的功能,这里简单介绍下弱引用,以及如何使用和何时使用他们。

强引用(Strong references)

  首先我们需要先来复习下强引用,强引用就是你每天在java中用到的最常见的引用,例如:

   StringBuffer buffer = new StringBuffer();

  上面一行代码创建了一个StringBuffer对象,并且用一个变量buffer存储了它的强引用。是的,就是这么简单,但请耐心听我说完。强引用最重要的部分,它强在哪里?是如何和gc交互的? 明确的说,如果一个对象通过强引用链可达,它就不会被gc掉。因为谁也不希望垃圾收集器毁掉我们正在用的对象。

强应用太强?  

  应用程序使用不能合理的继承的类的情况并不少见,这些类可能被简单标记为final,或者更复杂一些,比如由工厂方法返回的接口,该方法由数量未知(甚至不可知)的具体实现支持。假设你必须使用Widget类,但因为某些原因,不可能添加新功能。  
  如果你想持续追踪这个对象的额外信息会发生什么? 这种情况下,假设我们需要跟踪每个Widget的序列号,但是Widget类实际上没有序列号属性,而且因为Widget不能继承,我们也加不了。没关系,我们可以用hashmap。

serialNumberMap.put(widget, widgetSerialNumber);

  表面上看起来可以了,但widget的强引用肯定会导致问题。我们必须百分百确定何时Widget的序列号没有在被用了,然后我们可以从map中移除这个实体。否则就会发生内存泄露(如果未移除不用的widget)或者莫名其妙的丢失序列号(如果移除还在用的widget)。这些问题听起来很熟悉吧,这是那些没有gc的语言在尝试管理内存时遇到的问题,在java这样的现代语言中,我们不用担心这个问题。
  另一个常见的强引用问题就是缓存中,尤其是缓存像图片那样非常大的数据时。假设你一个给用户提供图片的应用,就像网页设计应用工具。你很自然的想到去缓存那些图片,因为从硬盘加载成本太高了,并且你也希望避免在内存中存在两份图片副本的可能性。
  因为图片缓存应该可以避免我们每次都重新加载图片,但你会很快意识到cache任何时候都会包含已经加载到内存中图片的引用。但是,对于普通的强引用,该引用本身将强制图片保留在内存中,这就要求你(如上所述)以某种方式确定何时不再需要该图片,并将其从缓存中删除,这样它就有能被gc掉了。你又被迫重复实现了垃圾收集器的功能。

弱引用(Weak references)

  弱引用,简单说就是不是那么能够强到让对象保持在内存中的应用。 弱引用能让你拥有GC的能力,让你能确定对象的可达性。你不用自己做,你只需要像下面一样创建一个弱引用就行了。

    WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);

  在代码的其他地方你就可以用weakWidget.get() 真正的Widget对象了。弱应用没有强大到能阻挡GC,所以你会发现当没有强引用指向widget时,weakWidget.get()会返回null。
  为了解决上文提到的widget序列号的问题,最简单的方式用就是用WeakHashMap,WeakHashMap和HashMap的工作方式很像,除了WeakHashMap把key替换为弱引用(不是Value),如果WeakHashMap的key变成了垃圾对象,整个entry会被自动清除。这种方式避免了我提到的陷阱,而且也只是需要把HashMap替换为WeakHashMap就足够了。如果你代码遵循Map的接口标准,甚至都不需要改其他代码。

引用队列(Reference queues)

  一旦弱引用开始返回null,它指向的对象肯定已经被gc掉了,弱引用对象也没啥用了。通常这意味着可以做一些清理工作了。对于WeakHashMap而言,它会清理到没用的entry,从而避免存着越来越多的死弱引用。
  引用队列让跟踪死引用变得容易。如果你给WeakReference传一个ReferenceQuene的构造参数,当弱引用所指向的对象变成垃圾对象后,引用对象会被自动插入到引用队列中。然后你就可以通过引用队列里的对象来做一些必要的清理工作了。

各种不同强度的应用 Different degrees of weakness

  除了上面我提到的弱引用外,其实java总共有4中不同的引用,其引用强度从强到弱分别是强应用、软引用、弱引用、虚引用。我们上文已经讨论过强应用和弱引用,接下来我们看下软引用和虚引用。

软引用(Soft references)

  软引用和弱引用很想,除了它并没有弱引用那么急着想扔掉它引用的对象。一个只被弱引用引用的对象会在下次gc的时候被处理掉,但被软引用引用的对象会存在一段时间。
  软引用和弱引用行为没啥不同,但在实际过程中,只要内存足够,软引用引用的对象会一直被保留。这是作为缓存很好的一个基础,比如上面提到的图片缓存问题,然后你就可以让gc去考虑哪些对象可达和这些对象消耗了多少内存。

虚引用(Phantom references)

  虚引用和软引用、弱引用都不同。他对对象的应用非常弱,弱到你都不能通过get方法获取的对象(get始终返回null)。他只能用来跟踪某个对象何时进入引用队列,只要它进队列了,就说明对象已死,但这和弱引用有什么区别?
  区别就是入队的发生发生时间不一样。弱引用只要对象变成弱可达就入队列,是在finalization和GC之前,理论上,对象可以被某些非正规的finalize复活,但指向其的弱引用则不会。虚引用只会在对象从内存中移除时入队,get()始终返回null是为了防止你复活将死的对象。
  那虚引用有什么好的地方?我只列举两点。首先,它可以让你判断是否一个对象已经被从内存中删除,事实上只有这一种方法判断,大部分情况下这个没啥用,但在某些非常特殊的情况下,比如操作大型图像时,它可能会派上用场:如果您确定某个映像应该被gc掉,那么你可以等到它确实被gc之后再尝试加载下一个图片,从而低OutOfMemoryError发生的可能性。
  其次,虚引用避免了finalize()通过创建强应用复活一个对象的问题。你说啥?问题是如果一个对象重载了finalize()方法,通过两次gc周期它才能被回收。第一次是确定它是否是垃圾对象,然后它就变成finalization。因为有可能它在finalization过程中会被复活,gc收集器必须重新gc来确保对象被真正去除掉。并且由于finalization可能没有及时发生,因此在对象再被gc掉前可以经历了非常多次的gc周期。 这可能意味着实际清理垃圾对象的严重延迟,这就是为什么即使堆里大多数对象都是垃圾也会导致OutOfMemoryErrors。
  用虚引用,这种情况是不可能出现的,绝对没有方法获取到一个指向已死对象的指针(因为已经不在内存里了)。因为虚引用不能用来复活一个对象,这个对象可以在gc的第一阶段发现只有虚引用引用的时候被清理掉。然后你可以在方便的时候处理你需要的任何资源。
  可以说,finalize()最开始就不应当被提供。虚引用比finalize()更加高效和安全,放弃finalize()也可以让VM更简单。还有很长的路要走,我承认我大多数时候仍然用finalize(),但好消息是你至少有个选择。

  看到这你肯定已经在发恼骚了,因为我正在给你们讲已经有近10年历史的api,而且也没讲新内容。 但这确实是事实,好多java程序猿真的不了解弱引用,而且也需要学习下。我希望你能从这篇文章学到一些东西。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK