美文网首页程序员
小心点,别被当成垃圾回收了。

小心点,别被当成垃圾回收了。

作者: 灬佐手边 | 来源:发表于2020-06-17 09:34 被阅读0次
我们说的不同的引用类型其实都是逻辑上的,而对于虚拟机来说,主要体现的是对象的不同的可达性(reachable) 状态和对垃圾收集(garbage collector)的影响。 image.png

初识引用

对于刚接触 Java 的 C++ 程序员而言,理解栈和堆的关系可能很不习惯。在 C++ 中,可以使用 new 操作符在堆上创建对象,或者使用自动分配在栈上创建对象。下面的 C++ 语句是合法的,但是 Java 编译器却拒绝这么写代码,会出现 syntax error 编译错误。 image.png
Java 和 C 不一样,Java 中会把对象都放在堆上,需要 new 操作符来创建对象。本地变量存储在栈中,它们持有一个指向堆中对象的引用(指针)。下面是一个 Java 方法,该方法具有一个 Integer 变量,该变量从 String 解析值 image.png
这段代码我们使用堆栈分配图可以看一下它们的关系 image.png
首先先来看一下 foo() 方法,这一行代码分配了一个新的 Integer 对象,JVM 尝试在堆空间中开辟一块内存空间。如果允许分配的话,就会调用 Integer 的构造方法把 String 字符串转换为 Integer 对象。JVM 将指向该对象的指针存储在变量 baz 中。

上面这种情况是我们乐意看到的情况,毕竟我们不想在编写代码的时候遇到阻碍,但是这种情况是不可能出现的,当堆空间无法为 bar 和 baz 开辟内存空间时,就会出现 OutOfMemoryError,然后就会调用垃圾收集器(garbage collector) 来尝试腾出内存空间。这中间涉及到一个问题,垃圾收集器会回收哪些对象?

垃圾收集器

Java 给你提供了一个 new 操作符来为堆中的对象开辟内存空间,但它没有提供 delete 操作符来释放对象空间。当 foo() 方法返回时,如果变量 baz 超过最大内存,但它所指向的对象仍然还在堆中。如果没有垃圾回收器的话,那么程序就会抛出 OutOfMemoryError 错误。然而 Java 不会,它会提供垃圾收集器来释放不再引用的对象。
当程序尝试创建新对象并且堆中没有足够的空间时,垃圾收集器就开始工作。当收集器访问堆时,请求线程被挂起,试图查找程序不再主动使用的对象,并回收它们的空间。如果垃圾收集器无法释放足够的内存空间,并且JVM 无法扩展堆,则会出现 OutOfMemoryError,你的应用程序通常在这之后崩溃。还有一种情况是 StackOverflowError ,它出现的原因是因为线程请求的栈深度要大于虚拟机所允许的深度时出现的错误。
标记 - 清除算法

Java 能永久不衰的一个原因就是因为垃圾收集器。许多人认为 JVM 会为每个对象保留一个引用计数,当每次引用对象的时候,引用计数器的值就 + 1,当引用失效的时候,引用计数器的值就 - 1。而垃圾收集器只会回收引用计数器的值为 0 的情况。这其实是 引用计数法(Reference Counting) 的收集方式。但是这种方式无法解决对象之间相互引用的问题,如下 image.png

然而实际上,JVM 使用一种叫做 标记-清除(Mark-Sweep)的算法,标记清除垃圾回收背后的想法很简单:程序无法到达的每个对象都是垃圾,可以进行回收。
标记-清除收集具有如下几个阶段
阶段一:标记

垃圾收集器会从 根(root) 引用开始,标记它到达的所有对象。如果用老师给学生判断卷子来比喻,这就相当于是给试卷上的全部答案判断正确还是错误的过程。 image.png
阶段二:清理
在第一阶段中所有可回收的的内容都能够被垃圾收集器进行回收。如果一个对象被判定为是可以回收的对象,那么这个对象就被放在一个 finalization queue(回收队列)中,并在稍后会由一个虚拟机自动建立的、低优先级的 finalizer 线程去执行它。 image.png

阶段三:整理(可选)

一些收集器有第三个步骤,整理。在这个步骤中,GC 将对象移动到垃圾收集器回收完对象后所留下的自由空间中。这么做可以防止堆碎片化,防止大对象在堆中由于堆空间的不连续性而无法分配的情况。 image.png

所以上面的过程中就涉及到一个根节点(GC Roots) 来判断是否存在需要回收的对象。这个算法的基本思想就是通过一系列的 GC Roots 作为起始点,从这些节点向下搜索,搜索所走过的路径称为 引用链(Reference Chain),当一个对象到 GC Roots 之间没有任何引用链相连的话,则证明此对象不可用。引用链上的任何一个能够被访问的对象都是强引用 对象,垃圾收集器不会回收强引用对象。
因此,返回到 foo() 方法中,仅在执行方法时,参数 bar 和局部变量 baz 才是强引用。一旦方法执行完成,它们都超过了作用域的时候,它们引用的对象都会进行垃圾回收。

下面来考虑一个例子 image.png 变量 foo 是一个强引用,它指向一个 LinkedList 对象。LinkedList(JDK.18) 是一个链表的数据结构,每一个元素都会指向前驱元素,每个元素都有其后继元素。 image.png
当我们调用add() 方法时,我们会增加一个新的链表元素,并且该链表元素指向值为 111 的 Integer 实例。这是一连串的强引用,也就是说,这个 Integer 的实例不符合垃圾收集条件。一旦 foo 对象超出了程序运行的作用域,LinkedList 和其中的引用内容都可以进行收集,收集的前提是没有强引用关系。

Finalizers

C++ 允许对象定义析构函数方法:当对象超出作用范围或被明确删除时,会调用析构函数来清理使用的资源。对于大多数对象来说,析构函数能够释放使用 new 或者 malloc 函数分配的内存。在Java中,垃圾收集器会为你自动清除对象,分配内存,因此不需要显式析构函数即可执行此操作。这也是 Java 和 C++ 的一大区别。
然而,内存并不是唯一需要被释放的资源。考虑 FileOutputStream:当你创建此对象的实例时,它从操作系统分配文件句柄。如果你让流的引用在关闭前超过了其作用范围,该文件句柄会怎么样?实际上,每个流都会有一个 finalizer 方法,这个方法是垃圾回收器在回收之前由 JVM 调用的方法。对于 FileOutputStream 来说,finalizer 方法会关闭流,释放文件句柄给操作系统,然后清除缓冲区,确保数据能够写入磁盘。
任何对象都具有 finalizer 方法,你要做的就是声明 finalize() 方法。如下


image.png

虽然 finalizers 的 finalize() 方法是一种好的清除方式,但是这种方法产生的负面影响非常大,你不应该依靠这个方法来做任何垃圾回收工作。因为 finalize 方法的运行开销比较大,不确定性强,无法保证各个对象的调用顺序。finalize 能做的任何事情,可以使用 try-finally 或者其他方式来做,甚至做的更好。

对象的生命周期

综上所述,可以通过下面的流程来对对象的生命周期做一个总结 image.png

对象被创建并初始化,对象在运行时被使用,然后离开对象的作用域,对象会变成不可达并会被垃圾收集器回收。图中用红色标明的区域表示对象处于强可达阶段。

JDK1.2 介绍了 java.lang.ref 包,对象的生命周期有四个阶段:􏲧强可达􏰛(Strongly Reachable􏰜)、软可达(Soft Reachable􏰜)、弱可达(Weak Reachable􏰜)、 幻象可达(Phantom Reachable􏰜)。 image.png
如果只讨论符合垃圾回收条件的对象,那么只有三种:软可达、弱可达和幻象可达。
软可达:软可达就是􏱬我们只能通过软引用􏳂才能访问的状态,软可达的对象是由 SoftReference 引用的对象,并且没有强引用的对象。软引用是用来描述一些还有用但是非必须的对象。垃圾收集器会尽可能长时间的保留软引用的对象,但是会在发生 OutOfMemoryError 之前,回收软引用的对象。如果回收完软引用的对象,内存还是不够分配的话,就会直接抛出 OutOfMemoryError。

弱可达:弱可达的对象是 WeakReference 引用的对象。垃圾收集器可以随时收集弱引用的对象,不会尝试保留软引用的对象。
幻象可达:幻象可达是由 PhantomReference 引用的对象,幻象可达就是没有强、软、弱引用进行关联,并且已经被 finalize 过了,只有幻象引用指向这个对象的时候。
除此之外,还有强可达和不可达的两种可达性判断条件
强可达:就是一个对象刚被创建、初始化、使用中的对象都是处于强可达的状态
不可达(unreachable):处于不可达的对象就意味着对象可以被清除了。

下面是一个不同可达性状态的转换图 image.png
判断可达性条件,也是 JVM 垃圾收集器决定如何处理对象的一部分考虑因素。
所有的对象可达性引用都是 java.lang.ref.Reference 的子类,它里面有一个get() 方法,返回引用对象。如果已通过程序或垃圾收集器清除了此引用对象,则此方法返回 null 。也就是说,除了幻象引用外,软引用和弱引用都是可以得到对象的。而且这些对象可以人为拯救,变为强引用,例如把 this 关键字赋值给对象,只要重新和引用链上的任意一个对象建立关联即可。

ReferenceQueue

引用队列又称为 ReferenceQueue,它位于 java.lang.ref 包下。我们在􏰓建各种引用(软引用,弱引用,幻象引用)并关联到响应对象􏰐时,可以选择是否需要关联引用队列。JVM 会在特定的时机将引用入队到队列中,程序可以通过判断引用队列中是否已经加入引用,来了解被引用的对象是否被GC回收。

Reference

java.lang.ref.Reference 为软(soft)引用、弱(weak)引用、虚(phantom)引用的父类。因为 Reference 对象和垃圾回收密切配合实现,该类可能不能被直接子类化。

相关文章

  • 小心点,别被当成垃圾回收了。

    我们说的不同的引用类型其实都是逻辑上的,而对于虚拟机来说,主要体现的是对象的不同的可达性(reachable) 状...

  • 2019-04-28

    信息爆炸的时代更要擦亮眼睛,别被垃圾淹没浑然不知。

  • 捡垃圾瓶。

    今天妈妈说,看姐。收垃圾的这么辛苦,我们也收垃圾了。我们收了很多纸夹子。然后又收了很多垃圾瓶。总共卖了十块钱。其实...

  • 别被人当成猴耍

    魏则西事件还在持续发酵中,李彦宏被国家网信办约谈,部队医院也已清查,一切都往好的方向发展,可这发展是要付出惨痛的代...

  • 兼职?小心别被骗!

    文/人生如梦sky 经济基础决定上层建筑。每个人都想通过努力,能拥有自己的小金库,能独立、能自主,能买想买的东西,...

  • 小心,别被“圈子”泡了!

    去年,我参加某时间管理主题活动时候,向一位伙伴请教学习心得。他回复我说:时间管理就是个圈,你泡久了,方法就掌握了。...

  • 小心,别被淘汰了

    忽然想起很多人,但是联系不上了,于是想上一下QQ找找看,却发现手机上面没有安装这款软件。阿偶,这事儿办的,曾经我们...

  • 小心别被馅饼砸

    2019.1.22 晴 晚上查看了朋友圈,发现几个姐妹都转发了关于吴秀波事件的文章,她们统一观点:渣男可耻,小三咎...

  • 小心,别被焦虑拖垮

    这个春节,估计很多人都是这么过的:起床后第一件事就是拿起手机,看看病情最新进展。一天在微信、微博上,看到无数痛苦、...

  • 『 特殊需求孩子的正面管教 』之 自然后果

    小美女倒垃圾的时候不小心把部分垃圾漏到了地上。先让她把垃圾盒里的倒到指定的垃圾桶,再让她拿扫把撮箕过来一点点扫干净...

网友评论

    本文标题:小心点,别被当成垃圾回收了。

    本文链接:https://www.haomeiwen.com/subject/dssjxktx.html