深入浅出Java四种引用
深入浅出Java四种引用(未完)
Java的四种引用已是老生常谈了,一般由内存泄露的解决办法引出。然而真正理解其背后的设计原理,并灵活应用它们不是一件容易的事。以下是四种引用的基本概念,但如果你以为这是一篇 “概念解释+使用场景+示例代码”的三段式技术帖,花2分钟浏览和1分钟复制粘贴修改就可以get到新技能,那么你可能要失望了。因为我希望用人话将Java的四种引用阐述清楚,使人一旦理解,就不会忘记。
- 强引用 最常见的引用,比如A a=new A(); 强引用的对象,JVM宁愿抛出OutofMemory异常也不会回收。
- 软引用 软引用的对象,当内存不足时,才会被JVM回收。
- 弱引用 弱引用的对象,当JVM执行GC时,就会被回收。
- 虚引用 不持有任何引用,在对应对象被回收后,其引用会被放到ReferenceQuene中。
强引用
为什么我说抽象的定义说的都不是人话?为了尽量精确、概括、简练、完整、无二义性,定义一般都必须是抽象的。这有利于统一人们的认知,即每次大家都使用这同一描述,指代非常明确。但是它却不容易理解、传授。正所谓学问之美,在于使人一头雾水。什么叫“JVM宁愿抛出OutofMemory异常也不会回收”?它至少包含以下几层含义:
JVM
即Java虚拟机,在Android中就是Dalvik虚拟机。关于Java虚拟机和Dalvik虚拟机的区别,你随便一搜可以找到许多教科书式的回答总结。这不是本文讨论的重点,此处我们把Dalvik虚拟机和Java虚拟机同等看待。很多Android程序员天天把Java虚拟机挂在嘴边,但是并不理解:一个App就是一个虚拟机!没错,Android手机上有许许多多的虚拟机,它们彼此独立,互不影响。一个App崩溃不会传染给另一个与它关的App,更不会导致整个Android系统崩溃。
OutOfMemory异常
你写代码new对象向系统(这里指虚拟机)申请内存,一般情况都会成功,但也有极端情况,就是内存不够了用完了,虚拟机就会抛出 OOM异常。
回收
Java当初火起来,除了跨平台的特性,最显著的卖点就是垃圾回收机制。它不必像C/C++一样用free/delete去清除使用完的内存。Java内存分配和回收的相关知识请移步 [Java 内存从分配到泄露]
这里只需记住一点,垃圾回收器是一条优先级较低的线程,它像一个不知疲倦的保洁阿姨,在你的办公室来回巡逻,一旦发现有垃圾,就会清扫出去。那么问题来了,什么是垃圾?换句话说,什么样的物品可以随时被清扫出去?
答案是,强引用指向的对象不是垃圾!即a所引用的new A()的那部分内存不会被回收。如果一个虚拟机内充斥着大量这样强引用的对象,并且存在时间非常长,比如静态的,static B b=new B() 生命周期贯穿整个程序,持续申请内存却不释放,那么虚拟机很快就会抛出OOM异常。然而,即使虚拟机知道了马上要OOM了,它也不敢去回收强引用所引用的对象。因为它是重要的,有用的,不是垃圾!
注意,以上用的是“如果”和“并且”,真实的情况是,虚拟机中确实充斥着大量强引用的对象。但这些强引用的对象的大部分都是非静态的(回想一下你敲代码的习惯,会不会每new一个对象都在前面加static?),只会存在一段时间。你确实可以手动将一个对象赋空,即a=null,那么new A()这个对象的内存肯定会被回收。但貌似你并没有这么做啊,如果这么做,那和C/C++不就一样了吗?那么问题又来了,一个强引用没有被赋空但最终它又准确被回收,没有引发程序逻辑问题,这是如何达成的呢?
答案是,引用这些强引用对象的那个对象被赋空了。这有点绕,举个栗子,你最初学习Android时开发了一个简单的App,所有对象都定义在Activity中。你并不需要在onDestroy回调函数中将每个强引用赋空(如果需要这样做,那onDestroy和C++的析构函数无异),只要Activity退出前台页面一段时间后被销毁,Activity中的所有对象都会被销毁。如下图所示,
act对ActivityA对象是强引用,ActivityA对A1,A2,A3对象都是强引用;但是一旦act被赋空,右边的这些强引用都樯橹灰飞烟灭。因此“强引用的对象都不会被回收”这句话是错的。其实这里牵扯到一个“JVM如何确定一个对象可以被回收”的问题,参见 [Java 内存从分配到泄露] 的 ” 可达性分析算法”章节
软引用
如果Java只有强引用一种引用,那么程序员往往不敢轻易使用static关键字。原因在于,全局静态的强引用会一直占用内存而不会被回收直到程序结束退出。而如果该全局静态的强引用对象又引用了别的对象时,问题会更加严重。想想单例就很好理解了。在Android中,单例的生命周期贯穿App始终。如果单例强引用了某个复杂页面的Activity,该Activity包含大量的强引用对象,某些强引用对象又强引用着其他大对象,那么这个时候内存占用是非常大的。即使该页面已退出到后台(页面跳转到其他Activity去了),它也不会被销毁。因为它被单例对象所强引用着,被认为是有用的,不是垃圾,不能回收其内存。实际上静态的强引用对象是典型的一种GC Root,为了不过多的引入新概念,关于GC Root和“GC Root强可达”还是请参见 [Java 内存从分配到泄露] 的 ” 可达性分析算法”章节。
每当问到如何解决单例引起的内存泄露,被面试者往往背诵网上的标准答案说尽量使用ApplicationContext来代替Activity的Context。难道Android中的单例就只会出现强引用Activity的情况,不可能引用别的大对象吗?比如数据类、业务类的对象。况且,某些情况ApplicationContext并不能完全代替Activity的Context,比如需要弹对话框的时候,比如要访问Activity中某些属性或方法的时候。针对这种情况,聪明的被面试者会继续回答说用软引用或弱引用代替单例中的强引用。那么为什么这样做可以有效解决单例的内存泄露问题呢?
回到文中最开始的软引用的定义:软引用的对象,当内存不足时,才会被JVM回收。也就是说当内存充足时,它和强引用一样死乞白赖占着内存;但是一旦内存不足,启动GC时,GC会毫不留情地把它回收掉。好比是你告诉保洁阿姨没什么事我工位上不要清扫,但是一旦地上堆满了东西让人根本无法下脚的时候,阿姨会不经过你同意就直接清扫出一片空地来。被扫掉的东西无法恢复,后果由你自己负责,谁叫你使用的规则是“软引用”,而不是强引用呢?
使用软引用确实能有效缓解内存泄露的情况,至少不会因为泄露过多而OOM。因为在内存不足接近OOM的时候,GC一定会回收掉软引用所指向的对象的内存。犹如咬文嚼字,只有在接近OOM时回收软引用对象,意味着程序的占用往往会维持一个比较高的水平,只是不会导致OOM而已。
对象缓存
软引用由内存泄露的解决办法引出,但绝不是只有解决内存泄露一种用途。最典型的用法是设计对象缓存。这里说的不是磁盘缓存,而是内存缓存。比如App中下载的网络图片,为了快速读取和显示保存在内存中;但是内存容量是有限的,当内存不足时,虚拟机会自动清除掉它们;当再次访问,所得为null,那么再从网络上下载或者从磁盘读取,如果做了磁盘缓存的话。如果我们不使用流行的开源图片加载框架诸如Glide、ImageLoader,那么我们可以使用软引用来实现自己的缓存框架。不必手动去删除,而让虚拟机自动为你完成,充分利用垃圾回收机制。