带着问题阅读

  • Java的内存区域中,有哪些区域是垃圾收集器所关注的?
  • 怎么判断一个对象是不是需要回收?

导语

前面两讲,给大家讲解了Java的内存区域和常见的内存溢出异常,从这一讲开始,我们来学习Java如何进行垃圾回收。首先,让我们来看看虚拟机是如何判断一个对象是否需要回收的。

本文是Effective Java专栏Java虚拟机专题的第四讲,如果你觉得看完之后对你有所帮助,欢迎订阅本专栏,也欢迎您将本专栏分享给你身边的工程师同学。

在学习本节课程之前,建议您了解一下以下知识点:

  • Java虚拟机的都有哪几块内存区域?每个区域存储的都是什么数据?

当我们在讨论内存分配回收时  我们在讨论什么

之前我们了解了Java虚拟机的内存区域,对于程序计数器、虚拟机栈、本地方法栈这三个区域的数据,随线程生而生,随线程灭而灭,每个栈帧(什么是栈帧?)分配多少内存,也基本是在类结构确定(编译期)的时候就已知了,因此这几个区域的内存分配和回收,都具有确定性,也就不需要考虑太多的回收问题。

我们常说的垃圾回收,主要指的是Java堆方法区的垃圾回收。一个接口的多个实现类需要的内存可能不一样,而编译期只知道对象的静态类型;一个方法中需要创建多少对象,也只有在运行期才知道,因此,这些部分的内存分配和回收都是动态的,垃圾收集器关注的是这部分的内存。

故而课程里讨论的内存分配和回收,也仅是针对Java堆和方法区的内存。

对象生死的判断策略

垃圾收集器在对堆进行回收之前,第一件事就是要确定哪些对象已经“死去”,需要回收。判断对象生死的算法,主要有以下两种。

引用计数算法

这种算法,给每个对象设置一个引用计数器,每当有一个地方引用它时,计数器加1;引用失效时,计数器减1;计数器为0,意味着对象独自漂泊在堆中,没人认识它,不可能再被使用,这时就是一个“废柴”,可以回收了。

这种算法,实现简单,判定效率也高,但是有一个致命的缺陷——很难解决对象之间相互引用的问题。

什么是对象相互引用,看下面这个例子:

/*** testGC()方法执行后,objA和objB会不会被GC呢? */
public class ReferenceCountingGC {public Object instance = null;private static final int _1MB = 1024 * 1024;/*** 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过*/private byte[] bigSize = new byte[2 * _1MB];public static void testGC() {ReferenceCountingGC objA = new ReferenceCountingGC();ReferenceCountingGC objB = new ReferenceCountingGC();objA.instance = objB;objB.instance = objA;objA = null;objB = null;// 假设在这行发生GC,objA和objB是否能被回收?System.gc();}
}

testGC()方法的前四行执行之后,objA对象被objA和objB.instance引用着,objB也类似;执行objA=null和objB=null之后,objA对象的objA引用失效,但是objB.instance引用仍然存在,因此如果采用单纯的引用计数法,objA并不会被回收,除非在执行objB=null时,遍历objB对象的属性,将里面的引用全部置为无效。

可达性分析算法

在主流的商业程序语言(Java、C#),都是通过可达性分析来判断对象是否存活的。这个算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,当GC Roots到一个对象不可达时,则证明这个对象是不可用的,可以将其回收。

这个算法很好的解决了引用计数法在处理相互引用时遇到的难题,如下图,object5和object6虽然相互引用,但是由于他们到GC Roots都不可达,因此会被判定为可回收的对象。

在Java中,可作为GC Roots的对象主要有两种:

  • 全局性的对象,如常量或者类的静态属性,如果一个对象被全局对象所引用,那就不能被回收;
  • 执行上下文,如栈帧中的局部变量,如果方法上下文中有局部变量引用了这个对象,那就不能被回收;

可达不一定就安全了

默认情况下,到GC Roots可达的对象都不会被回收,这种对象,我们成为“强引用”。

然而,实际开发中,并不是所有强引用的对象,我们都认为是不能回收的,比如一个从缓存获取的很占用内存的对象,我希望他可以在下一次垃圾收集时被回收,如果下一次需要使用,再从缓存重新获取。

针对这种“食之无味,弃之可惜”的对象,从JDK 1.2开始,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)4种:

  • 强引用:也就是默认的引用,只要到GC Roots可达,就不会被回收;
  • 软引用:对象在将要发生内存溢出之前,会被回收;
  • 弱引用:对象在下一次GC时被回收;
  • 虚引用:形同虚设,虚引用的对象,可以视为GC Roots不可达的对象;

这里以弱引用为例,演示一下如何使用引用类型:

public class WeakReferenceTest {public static void main(String[] args) throws InterruptedException {WeakReference<WeakReferenceTest> weakReference = new WeakReference<WeakReferenceTest>(new WeakReferenceTest());// 第一次打印弱引用所引用的对象 System.out.println(weakReference.get());// 进行一次GCSystem.gc();// 由于GC进行需要时间,这里等一秒钟Thread.sleep(1000);// 再次打印弱引用所引用的对象 System.out.println(weakReference.get());}
}

运行结果:

com.hzy.jvm.chp03.WeakReferenceTest@6f92c766
null

起死回生

即使在可达性分析算法中不可达的对象,也不是“非死不可”的。

对象在被标记为不可达之后,如果对象覆盖了finalize()方法并且该对象还没有调用过finalize(),那么这个对象会被放入F-Queue队列中,并在稍后一个由虚拟机建立的、低优先级Finalize线程中去执行对象的finalize()方法。稍后GC会对F-Queue的对象进行再一次的标记,如果对象的finalize方法中,将对象重新和GC Roots建立了关联,那么在第二次标记中就会被移除出“即将回收”的集合。

但是,finalize线程的优先级很低,GC并不保证会等待对象执行完finalize方法之后再去回收,因而想通过finalize方法区拯救对象的做法,并不靠谱。鉴于finalize()方法这种执行的不确定性,大家其实可以忘记finalize方法在Java中的存在了,无论什么时候,都不要使用finalize方法。

总结

这一讲讲解了Java虚拟机如何判断对象是否需要回收,重点介绍了可达性分析算法。下一讲,让我们来了解一下HotSpot是如何实现可达性分析算法的。

课后思考

GC开始后,是否需要暂停除GC线程以外的其他线程?为什么?欢迎在评论区写下您的答案,O(∩_∩)O谢谢。

上一节课后思考题的答案

上一讲的问题是——“除了本文所讲的异常,你还见过什么其他的内存溢出异常”

就我个人而言,遇到过一个比较少见的异常:

java.lang.OutOfMemoryError: GC overhead limit exceeded

查询了一下Oracle上的文献:

The parallel collector will throw an OutOfMemoryError if too much time is being spent in garbage collection: if more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, an OutOfMemoryError will be thrown. This feature is designed to prevent applications from running for an extended period of time while making little or no progress because the heap is too small. If necessary, this feature can be disabled by adding the option-XX:-UseGCOverheadLimit to the command line.

大致意思就是:

并发垃圾收集器如果花费太多的时间在垃圾收集上就会抛出此异常。具体标准是:虚拟机将98%以上的时间花在了回收少于2%的垃圾上,就会抛出这个异常。虚拟机的这个特性可以防止应用程序由于堆设置太小的缘故而花费太多时间在垃圾收集上做无用功。如果想放弃这个特性,可以加入这个参数-XX:-UseGCOverheadLimit.

StackOverFlow上也有如下的描述:

This message means that for some reason the garbage collector is taking an excessive amount of time (by default 98% of all CPU time of the process) and recovers very little memory in each run (by default 2% of the heap).
This effectively means that your program stops doing any progress and is busy running only the garbage collection at all time.
To prevent your application from soaking up CPU time without getting anything done, the JVM throws this Error so that you have a chance of diagnosing the problem.
The rare cases where I've seen this happen is where some code was creating tons of temporary objects and tons of weakly-referenced objects in an already very memory-constrained environment.

我将在讲完垃圾收集器之后,尝试给大家重现这个异常。

参考资料

  • 《深入理解Java虚拟机》 周志明

生存还是死亡 —— Java虚拟机如何判断对象是否需要回收相关推荐

  1. java判断对象已经被回收_Java中JVM判断对象已死的基本算法分析

    原标题:Java中JVM判断对象已死的基本算法分析 jvm中 有各种的垃圾收集器,每个收集器都有各自的算法. 但是一切的根本都需要找到找到应该被消除的对象,理解如何找到死亡对象才是理解垃圾收集器的基础 ...

  2. Java的if判断对象为null时,null放在比较运算符的左边还是右边较好?

    如java中:if(name == null)和if(null == name)有什么讲究吗? 答:在java里面,它们是一样的.但是通常写为null == name.这其实是在C语言里面引申出来的. ...

  3. 学习笔记:Java虚拟机——JVM内存结构、垃圾回收、类加载与字节码技术

    学习视频来源:https://www.bilibili.com/video/BV1yE411Z7AP Java类加载机制与ClassLoader详解推荐文章:https://yichun.blog.c ...

  4. java虚拟机如何判断两个类相同_你有没有想过: Java 虚拟机是如何判断两个对象是否相同的?判断的流程是什么?...

    在Java程序运行时,会产生那么多的对象,那 Java 虚拟机是如何判断两个对象是否相同的呢?判断的流程是什么? 参考解答: Java 虚拟机会先判断两个对象的hashCode是否相同,如果hashC ...

  5. 【JAVA】如何判断对象已经死亡?

    如何判断对象已经死亡? JVM在进行垃圾回收时,要做的第一件事情,就是去寻找那些已经没有任何变量引用的对象,从而对此类对象进行回收.那么,JVM是如何判断对象已经死亡了呢? 一.引用计数法 程序给对象 ...

  6. Java虚拟机(四)—— Java虚拟机中的对象

    1. Java 对象在虚拟机中的生命周期 在 Java 对象被类加载器加载到虚拟机中后,Java 对象在 Java 虚拟机中有 7 个阶段. 1.1 创建阶段(Created) 创建阶段的具体步骤为: ...

  7. 【深入Java虚拟机JVM 09】JVM垃圾回收finalize方法--对象最有一次自我拯救

    说明:文章所有内容均摘自<深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)> 即使在可达性分析算法中不可达的对象,也并非是"非死不可"的,这时候它们暂时处于& ...

  8. 《深入理解Java虚拟机》读书笔记3--垃圾回收算法

    转载:http://blog.csdn.net/tjiyu/article/details/53983064 下面先来了解Java虚拟机垃圾回收的几种常见算法:标记-清除算法.复制算法.标记-整理算法 ...

  9. 判断对象是否可回收、垃圾回收算法

    本节将会介绍下判断对象是否都能回收的两种方式:引用计数法.可达性分析,另外会介绍一下常用的垃圾回收算法:标记清除算法,复制算法,标记整理算法,分代回收算法. 目录 对象是否可回收 引用计数算法 可达性 ...

最新文章

  1. boost::phoenix::lambda相关的测试程序
  2. Ubuntu16.04安装jdk8
  3. 文献记录(part41)--Residual multi-task learning for facial landmark localization and expression ...
  4. linux启动mysql1820_linux 系统下MySQL5.7重置root密码(完整版,含ERROR 1820 (HY000)解决方案)...
  5. 【图说word】 宏
  6. 18C 也不能避免 SQL 解析的 Bug
  7. Facebook 的大牛组长什么样?
  8. agx 安装ros opencv_(五)树莓派3开发环境搭建——5.Android手机端与robot端ROS网络通信...
  9. python处理excel表格-如何用python处理excel表格
  10. python使用numpy的np.mod函数计算numpy数组除以某一特定数值剩余的余数(remainder)、np.mod函数和np.fmod函数对负值的处理方式有差异
  11. 长隆大马戏机器人_长隆娱乐登陆
  12. OpenCV cvBoundingRect应用举例
  13. Deep Learning for Massive MIMO CSI Feedback-学习笔记
  14. ubuntu只读文件系统
  15. Zynq-Linux移植学习笔记之24-VPVN温度监测
  16. 基于Inception v2实现判别mnist手写数据集
  17. jq及html通过url下载文件
  18. 科研实习 | 新加坡国立大学尤洋老师课题组招收Data-centric AI科研实习生
  19. 基于java的在线学生管理系统【原创】
  20. webm视频怎么转换成mp4格式?

热门文章

  1. qt-everywhere 交叉编译安装
  2. 小小程序员买个手环都要搞事???
  3. 出货量突破1亿台!全球智能穿戴技术品牌Amazfit发布全新品牌标志
  4. Android实现对Dialog的截图并保存在本地
  5. mdp框架_MDP实现细节(一)-- 贝尔曼方程
  6. 手机发烫是为何—— App 电量测试定位方法
  7. Farce Photo产品免责声明、安装许可使用协议
  8. 比尔·盖茨:2019 年这 10 大技术必成潮流!
  9. 铂金05:致胜良器-无处不在的“阻塞队列”究竟是何面目
  10. Oracle 建表语句的关键字详解