天天看点

垃圾收集器概述判断对象已死再谈引用生存还是死亡方法区的回收引用总结结束语

垃圾收集器(Garbage Collection, GC)的历史可以追溯到1960年MIT的第一门真正使用内存动态分配和垃圾收集技术的语言——Lisp。人们当时就在思考GC需要完成的3件事:

哪些内存需要回收?

什么时候回收?

如何回收?

经过半个世纪的发展,目前内存的动态分配与内存回收技术已经相当成熟。在Java这门语言中,

程序计数器、虚拟机栈、本地方法栈三个区域的内存随线程的创建而创建,随线程的销毁而销毁;

堆和方法区中,内存的分配和回收都是动态的。

栈中的栈帧随着方法的进入和退出而有条不紊的执行着出栈和入栈操作,每一个栈帧中分配多少内存基本上是在类结构确定下来的时候就已知的(尽管在运行时期会由JIT编译器进行一些优化,但基本还是可以认为编译期可知)。因此这几个区域的内存分配和回收都具有确定性,因此不需要过多的考虑内存回收问题。

在堆和方法区中,因为接口中多个实现类需要的内存可能不一样,而一个方法中的多个分支需要的内存也可能不一样,只有当程序处于运行期的时候,才能确定需要分配多大的内存。因此垃圾收集器所关注的便是这部分内存。

判断对象已死

引用计数法

在传统方法中获取对象引用次数的方法为

引用计数法

(Reference Count),其基本的算法如下:

给对象添加一个引用计数器,每当有一个对象引用它时,计数器值加1;当引用实效时,计数器值减1;

任何时刻计数器值为0的对象便是不可能被用的对象,此时可以判断对象“已死”。

但是在主流的Java虚拟机中都没有引用

引用计数法

来管理内存,主要原因是该算法很难解决对象之间相互引用导致的相互循环引用问题。比如对象objA和objB都有字段instance,那么令:

objA.instance = objB
objB.instance = objA
           

很显然,两个对象相互引用,导致引用计数不能为0,于是引用计数器无法通知GC收集器回收它们。

可达性分析算法

Java, C#等主流程序语言中,均使用

可达性分析

(Reachability Analysis)算法来判定对象是否存活的。该算法的基本思想如下:

将”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路称为

引用链

(Reference Chain)`;

当一个对象到GC Roots没有任何引用的链相连时(图论中称为对象不可达),则证明此对象“已死”。

在Java语言中,可作为GC Roots的对象包括下述几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性和常量引用的对象;
  • 本地方法栈中JNI(Native Method)引用的方法;

再谈引用

无论是通过

引用计数法

判断对象的引用数量,还是通过

可达性分析

算法判断对象的引用链是否可达,判定对象是否“存活”都与引用相关。

JDK1.2之前,一个对象的只存在两种状态,即引用和被引用。

JDK1.2之后对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

强引用

强引用就是代码中普遍存在的,类似于

Object object =  new Object()
           

这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用

软引用是用来描述一些还有用,但并非必需的对象。

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用可用来实现内存敏感的高速缓存。

Java1.2之后,提供了

SoftReference

类来实现软引用。

弱引用

弱引用也是用来描述非必须对象的,但是它的强度比软引用更弱一些。

被软引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

Java1.2之后,提供了

WeakReference

类来实现弱引用。

虚引用

虚引用也称为幽灵引用或幻影引用,是最弱的一种引用关系。

虚引用并不会决定对象的生命周期,当然也无法通过一个虚引用来取得一个对象实例。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾收集器回收。为一个对象设置虚引用的唯一目的就是在这个对象被收集器回收时可以得到一个系统通知。

Java1.2之后,提供了

PhantomRefernce

类来实现虚引用。

生存还是死亡

在Java垃圾收集器中,对象并不会直接被标记为“死亡”,宣告一个对象真正死亡需要经历两次标记过程:

可达性分析之后发现并没有与GC Roots相连接的引用链,那么它将被第一次标记;

对F-Queue中的对象第二次分析后,仍然没有发现与GC Roots相连接的引用链,则标记对象“死亡”;

其完整过程如下图所示:

垃圾收集器概述判断对象已死再谈引用生存还是死亡方法区的回收引用总结结束语

注意:

finalize()方法是在Java刚诞生的时候,为了方便C/C++而作出的一个妥协,其运行代价高昂,不确定性大,无法保证各个对象的调用顺序。因此不建议在Java中使用finalize()这个方法

方法区的回收

方法区(即HotSpot中的永久代)中垃圾收集器是可有可无的。方法区中的垃圾回收主要回收两部分内容:废弃常量和无用的类。

判定废弃常量可被回收

如果系统中某常量在其他地方未被引用,则该常量被系统清理出常量池;否则不清理。

判定类可被回收

  • 该类所有的实例都已被回收,即Java堆中不存在任何类的引用;
  • 加载该类的

    ClassLoader

    已经被回收;
  • 该类对应的

    java.lang.Class

    对象没有在任何地方被引用,无法在任何地方通过反射机制访问该类的方法;

虚拟机可以对满足上述三个条件的类进行回收,但并不一定会回收。

而且虚拟机也针对这些无用类提供了部分参数来进行控制,比如

查看类加载和卸载信息:

-verbose:class

-XX:+TraceClasLoading

-XX:+TraceClassUnLoading

其中

-verbose:class

-XX:+TraceClasLoading

可以在Product版的虚拟机中使用,

-XX:+TraceClassUnLoading

则需要FastDebug版的虚拟机支持。

引用总结

引用类型 垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 Unknown Unknown Unknown

结束语

到这里已经尝试着回答了开始的时候提到的三个问题中的两个:

  • 哪些内存需要回收?

    堆和方法区

  • 如何回收?

    可达性分析+引用类型

对于最后一个问题

什么时候回收?

在新生代的Eden区中没有足够空间进行分配对象时,虚拟机将会发起一次Minor GC;

如果从新生代晋升到老年代的对象所需空间大于老年代的剩余空间,虚拟机会发起一次Full GC;

这里先说个概要,至于具体细节会在内存分配策略和回收策略中说明。