说到垃圾回收(Garbage Collection,GC),很多人会自然而然地把它和Java联系起来。在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。顾名思义,垃圾回收是释放垃圾占用的空间,那么在Java中,什么样的对象会被认定为“垃圾”?那么当一些对象被确定为垃圾之后,采用什么样的策略来进行回收(释放空间)?在目前的商业虚拟机中,有哪些典型的垃圾收集器?下面我们来逐一探讨这些问题。以下是本文的目录大纲:
  一.如何确定某个对象是“垃圾”?
  二.典型的垃圾收集算法
  三.典型的垃圾收集器

  如果有不正之处,希望谅解和批评指正,不胜感激。
  一.如何确定某个对象是“垃圾”?
  在这一小节我们先了解一个基本的问题:如果确定某个对象是“垃圾”?既然垃圾收集器的任务是回收垃圾对象所占的空间供新的对象使用,那么垃圾收集器如何确定某个对象是“垃圾”?—即通过什么方法判断一个对象可以被回收了。
  在java中是通过引用来和对象进行关联的,也是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象成为可被回收的对象了。这种方式成为引用计数法。
  这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。看下面这段代码:
  public class Main {
  public static void main(String[] args) {
  MyObject object1 = new MyObject();
  MyObject object2 = new MyObject();
  object1.object = object2;
  object2.object = object1;
  object1 = null;
  object2 = null;
  }
  }
  class MyObject{
  public Object object = null;
  }
  后面两句将object1和object2赋值为null,也是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器永远不会回收它们。
  为了解决这个问题,在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上真的成为可回收对象了。
  至于可达性分析法具体是如何操作的我暂时也没有看得很明白,如果有哪位朋友比较清楚的话请不吝指教。
  下面来看个例子:
  Object aobj = new Object ( ) ;
  Object bobj = new Object ( ) ;
  Object cobj = new Object ( ) ;
  aobj = bobj;
  aobj = cobj;
  cobj = null;
  aobj = null;
  第几行有可能会使得某个对象成为可回收对象?第7行的代码会导致有对象会成为可回收对象。至于为什么留给读者自己思考。
  再看一个例子:
  String str = new String("hello");
  SoftReference<String> sr = new SoftReference<String>(new String("java"));
  WeakReference<String> wr = new WeakReference<String>(new String("world"));
  这三句哪句会使得String对象成为可回收对象?第2句和第3句,第2句在内存不足的情况下会将String对象判定为可回收对象,第3句无论什么情况下String对象都会被判定为可回收对象。
  后总结一下平常遇到的比较常见的将对象判定为可回收对象的情况:
  1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象,比如下面的代码:
  Object obj = new Object();
  obj = null;
  Object obj1 = new Object();
  Object obj2 = new Object();
  obj1 = obj2;
  2)局部引用所指向的对象,比如下面这段代码:
  void fun() {
  .....
  for(int i=0;i<10;i++) {
  Object obj = new Object();
  System.out.println(obj.getClass());
  }
  }
  循环每执行完一次,生成的Object对象都会成为可回收的对象。
  3)只有弱引用与其关联的对象,比如:
  WeakReference<String> wr = new WeakReference<String>(new String("world"));
  二.典型的垃圾收集算法
  在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。
  1.Mark-Sweep(标记-清除)算法
  这是基础的垃圾回收算法,之所以说它是基础的是因为它容易实现,思想也是简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段是回收被标记的对象所占用的空间。具体过程如下图所示:

  从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
  2.Copying(复制)算法
  为了解决Mark-Sweep算法的缺陷,Copying算法被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来不容易出现内存碎片的问题。具体过程如下图所示:

  这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
  很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。