JVM系列(三)-- 垃圾收集基础(GC)

概述

起源

误解: 垃圾收集(Garbage Collection,简称GC)是Java语言的伴生产物。
事实上,垃圾收集的历史远远比Java久远,在1960年诞生于麻省理工学院的Lisp是第一门开始使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期时,其作者John McCarthy就思考过垃圾收集需要完成的三件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

Why need it?

经过半个世纪的发展,今天的内存动态分配与内存回收技术已经相当成熟,一切看起来都进入了“自动化”时代,那为什么我们还要去了解垃圾收集和内存分配?

答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。

对象

我们在系列2中学习了Java内存运行各个区域,分别为程序计数器、虚拟机栈、本地方法栈、Java堆、方法区。(如果忘记了,快回头复习)

其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。

Java堆和方法区有很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这个不分内存的分配和回收是动态的。这也就是GC所关注的对象!
这里我们就回答了之前提出的第一个问题 – 哪些内存需要回收。

判断对象生死

在垃圾收集器对内存进行回收时,第一步就是要判断对象的状态。

引用计数算法

引用计数算法:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
当引用失效时,计数器值就减一;
任何时刻计算器为零的对象就是不可能再被使用的。

这种方法是我编程中最常用的了,这里被狠狠地打脸了。
这个算法面临的问题:无法解决对象之间相互循环引用的问题。

objA.instance = objB; objB.instance = objA;objA 和 objB 都不会再被访问后,它们仍然相互引用着对方,所以它们的引用计数器不为 0,将永远不能被判为不可用。
(可以好好思考一下)

可达性分析算法

基本思路: 当前对象到根对象(GC Roots)是否是可达的 (图论知识)

  • 从 “GC Roots” 对象作为起点开始向下搜索,走过的路径称为引用链(Reference Chain);
  • 从 “GC Roots” 开始,不可达的对象被判为不可用。

如下图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。

tl4Qv4.png

Java中可作为GC Roots的对象

  1. 栈中(本地变量表中的Reference)
    • 虚拟机栈中,栈帧中的本地变量表所引用的对象;
    • 本地方法栈中,JNI引用的对象(native方法)
  2. 方法区中
    • 类的静态属性引用的对象;
    • 常量引用的对象;

      这上面的对象没搞懂

即便如此,一个对象也不是一旦被判为不可达,就立即死去的,宣告一个的死亡需要经过两次标记过程。

引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。

在JDK 1.2版以前,Java里面的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。–我自己的以往理解也是这样的

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(SoftReference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

4种引用

  1. 强引用:Object obj = new Object()这种,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  2. 软引用: 用来描述还有用但非必须的对象。对于软引用对象,在OOM(OutOfMemory)前,虚拟机会把这些对象列入回收范围中进行第二次回收,如果这次回收后,内存还是不够用,就OOM。实现类:SoftReference
  3. 弱引用: 跟软引用类似,比它更弱一点。被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。实现类:WeakReference
  4. 虚引用: 也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。唯一作用就是为了能在这个对象被收集器回收时收到一个系统通知。实现类:PhantomReference

To be or not to be?

要真正宣告一个对象死亡,至少要经历两次标记过程:

  1. 第一次标记: 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
  2. 第二次标记: 筛选此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

finalize方法

如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。

这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。
这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。

存活方式:
如果对象要在finalize()中成功拯救自己–只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;

finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时,所以笔者建议大家完全可以忘掉Java语言里面的这个方法。

回收方法区

方法区垃圾收集的“性价比”通常也是比较低的:在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。

回收的内容:废弃的常量和不再使用的类型

  • 废弃常量: 例如一个字符串”abc”,当没有任何引用指向”abc”时,它就是废弃常量了。
  • 无用的类: 同时满足一下3个条件的类。
    1. 该类的所有实例已被回收,Java堆中不存在该类的任何实例;
    2. 加载该类的Classloader已被回收;
    3. 该类的Class对象没有被任何地方引用,即无法在任何地方通过反射访问该类的方法。

Free Talk

今天的垃圾收集基础部分算是写完,本来打算把算法部分也讲了,但是发现算法部分另起一篇的效果更好一点,同时打算补一篇Java基础,发现自己上学期学的Java遗忘的比较厉害了,而且上学期学的时候也没有学全,再拿来复习学习一下比较好。然后我发现自己对内存管理里面的数据区域了解的太少了,遇到了总是弄不清楚。

热爱未知,比如宇宙和清晨
image

参考资料:
Github
《深入理解java虚拟机》