当分配到内存中的对象再也不会被访问的时候,垃圾收集器(Garbage Collection,GC)就会自动回收这些对象占用的内存空间,把腾出来的空间留给新的对象。栈中变量的存储空间随着栈帧的入栈和出栈自动分配和回收,所以垃圾收集器主要作用的区域有两个:堆和方法区。

方法区垃圾回收

类的常量池类型信息都存储在方法区里,这两部分内容也是垃圾回收的重点目标。

判断常量是否可回收

假如常量池中有个字面量 “NAME” ,但是当前系统中没有任何字符串的值是“NAME”,而且它也没有在别的地方被访问到。这个时候如果垃圾收集器执行垃圾回收,这个常量将会被虚拟机“清除”出去,将它占用的存储空间释放出来。

判断类型信息是否可回收

一个类型如果同时满足下列三个条件,那么它就允许被回收。

  1. 该类以及他的所有子类的所有实例都已经被回收了。
  2. 加载该类的类加载器已经被回收了。
  3. 该类对应的 java.lang.Class 没有在任何地方被引用,(这点是防止通过反射的方式访问该类)。

满足上三个条件表示类型“可被回收”,回收是否执行还需要配置外部参数。而且方法区的垃圾回收成果往往很低,所以该区不是垃圾回收的重点区域。

堆垃圾回收

如何判断对象可回收

堆中存放着几乎所有的对象,这些对象的引用放在栈中,而且会出现多个引用指向同一个对象的。如果一个对象没有引用指向它,则这个对象可回收。基于此,有两种判断对象是否可回收的算法。

引用计数算法

它的原理很简单:在对象中添加个引用计数器,每当有个地方引用它时,计数器就加一,当引用失效时,计数器就减一,当对象的引用计数器的值为零时,表示这个对象不可能再被使用了。这种算法优点是简单,缺点是无法解决两个对象循环引用的问题,也因为此,主流的java虚拟机都没有使用这种算法来管理内存

可达性分析算法

基本思想:通过一系列被称为“GC Root ”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程走过的路径称为“引用链”,如果一个对象到“GC Root”之间没有任何引用链相连,则这个对象不可被访问。

固定可作为GC Root的对象包括以下几种:

  • 栈中(其实是栈栈帧中)的引用的对象。
  • 本都方法栈中引用的对象。
  • 方法区中类静态成员变量引用的对象。
  • 方法区域中常量引用的对象。
  • 所有被同步锁(synchronized 关键字)持有的对象。

判断对象是否可回收都和引用有关,HotSpot虚拟机采用直接引用法(引用中存储的值是对象的起始地址)。在JDK1.2后,java对引用的概念进行了扩充,将引用分为强引用软引用弱引用虚引用,四种引用强度一次减弱。强引用指向的对象在任何时候都不会被收集,即使内存溢出也不会被收集。其他三个引用指向的对象在一定程度下都是可以被回收的。

分代收集理论

可达性分析判断完对象是否可回收后,接下来就应该确定对象应该怎么回收。HotSpot中的垃圾收集算法属于追踪式垃圾收集的范畴。目前大多数垃圾收集器都遵循“分代收集”的理论,该理论建立在两个分代假说之上:

  1. 弱分代假说 :绝大多数对象都是朝生夕灭的。
  2. 强分代假说: 熬过越多次垃圾收集过程的对象就越难消亡。

这两个假说奠定了垃圾收集器的设计原则:将内存对象划分出不同的区域,然后将对象依据其年龄(分代年龄信息放在对象的头中)分配到堆的不同区域进行存储,针对不用的区域使用不同的垃圾收集算法。

一般会把java堆分成 新生代(Young Generation)老年代(Old Generation) 两个区域,新生代中每次垃圾回收时都会有大批的对象死去,而每次回收后存活的对象会逐步的晋升到老年代中存放(很显然,刚new出来的对象肯定是分配到新生代中的)。垃圾回收器根据区域中对象的消亡情况安排相应的收集算法,从而就有了:标记-清除、标记-复制、标记整理算法。如下图所示:

标记-清除算法(Mark-Sweep)

该算法分为”标记“和”清除“两个阶段:首先通过根节点标记所有从根节点开始可达的对象,因此,未被标记的对象就是不会再被访问的对象,然后清除所有未被标记的对象。该算法是最基础的垃圾收集算法,后续的垃圾收集算法都是在其基础上改进的。

算法缺点: 垃圾收集后会产生大量不连续的空间碎片,如果后面要给大对象分配内存,可能还要再执行一次垃圾收集操作。如果当前区域有大量的待收集对象,而且个头还不小,该算法的执行效率会大大降低(所以该算法不适合用在新生代区域中)。

标记-复制算法(Mark-copying)

该算法主要解决的是清楚大量可回收对象是效率低的问题。它将内存按照容量划分成大小相等的两块,分别叫做A1,A2,每次只使用其中一块(A1),当执行垃圾回收时,将内存A1中存活的对象复制到内存A2中去,再把内存A1中的所有对象全部清除。

优点:如果当前区域的可回收对象很多时,那需要复制的对象就很少了,可以大大提高垃圾收集效率(所以适合应用在新生代)。当向A2复制对象时,因为A2不存在空间碎片的问题,所以直接移动堆指针,按照顺序分配存储空间即可。

缺点:可用的内存空间减少一半,还是很浪费的。

主流的java虚拟机都优先采用这种收集算法回收新生代。并且根据新生代区域中的对象“朝生夕灭”的特点,IBM公司提出了“Appel式回收”,它将新生代划分成更细的区域 :把新生代划分为一块较大的Eden区域和两块较小的Survivor区域,这两块Survivor区域大小一样。每次分配内存只使用Eden和其中一个Survivor区域,发生垃圾收集时,将Eden和Survivor区域中存活的对象复制到另一个Survivor中,之后将Eden和已用过的那块Survivor区域中的对象直接清空。HotSpot虚拟机默认的Eden和Survivor区域的大小比例是8:1,这样仅仅只浪费了10%的内存空间(Appel式回收解决了标记-复制算法内存利用率不高的问题)。加入另一块Survivor上没有足够的空间存放上一次新生代收集存活下来的对象,那这些对象直接通过分配担保机制直接进入老年代。

标记-整理算法(Mark-Compact)

该算法是针对老年代对象消亡特征提出的。该算法依旧是先标记,不过后续操作不是直接收集可回收对象,而是将存活多想都向内存的另一端移动,然后直接清除掉边界以外的内存。老年代中有大量的存活对象,移动这些对象是一种极为负重的操作,而且还需要停暂停用户的应用程序才能进行,这种“暂停”被虚拟机的最初设计者形象的描述为“Stop The world”。“暂停”的目的是终止所有应用线程的执行,只有这样系统中才不会有新的垃圾产生,同时停顿保证了系统在某个瞬间的一致性,也有利于垃圾收集器更好地标记垃圾对象。

总结

垃圾回收在方法区和对两个区域发生,目的是腾出内存空间放置内存溢出,方法区的垃圾回收效果不明显,所以垃圾回收主战场是堆。

根据可达性分析算法,如果堆中的对象到GC Root之间没有任何引用链相连,那么这个对象是可被回收的。

根据分代收集理论将堆内存分为新生代和老年代两个区域,新创建的对象都分配在新生代中,多次回收操作后仍能存活的对象将晋升到老年代。

对于堆中不同的区域,采用不同的垃圾回收算法,新生代使用“标记-复制”算法,老年代使用“标记-整理”和“标记-清除”算法。