GC 一篇文章彻底了解Java垃圾收集机制

垃圾收集(Garbage Collection ,GC),是一个长久以来就被思考的问题,当考虑GC的时候,我们必须思考3件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?
那么在JAVA中,我们要怎么来考虑GC呢?首先回想以下内存区域的划分,其中程序计数器、本地方法栈、虚拟机栈三个区域随线程而生,随线程释放,栈中的栈帧随着方法的进入和退出执行着出栈和入栈的操作,每一个栈帧分配多少内存基本是在类结构确定时就已经固定的(可能会进行一些优化,但是大体上已知),因此这几个区域就不需要考虑回收的问题,因为方法结束或者线程结束时,内存自然都被回收 。不需要额外的GC算法等 。
然而Java堆和方法区则不一样,一个接口所对应的多个实现类所需要的内存可能不一样,一个方法中的多个分支所需要的内存也可能不一样,我们只有在程序处于运行期间才能知道程序需要创建那些对象,这部分的内存的分配和回收是动态的,因此,垃圾收集器关注的是这方面的内存 。
一. 如何确定对象可以回收1.引用计数算法
最容易想到与理解的算法,即对于每一个对象,每当该对象被引用时,计数器值就+1,引用失效时,计数器就-1 。因此,当对象的引用计数为0时,即为不可再被使用的 。该算法也在一些领域被使用来进行内存管理,但是JAVA虚拟机中并没有选用该算法 。主要是因为不能很好的解决循环引用的问题 。
举个简单的例子来说明循环引用:
class Container{ public Object obj ;
}public class ReferTest { public static void main(String[] args){
Container c1 =new Container();
Container c2 =new Container();
c1.obj = c2 ;
c2.obj = c1 ;
c1 = null ;
c2 = null ; //此时c1 c1会被判定为死亡对象么? }
}
事实上会被判定为死亡对象,因为JAVA虚拟机不是采用引用计数来进行判断的,因此如果发生垃圾回收,c1,c2 都会被回收内存 。
2.可达性分析
Java、C#的主流实现都是采用该种方式,来判断对象是否存活 。
这个算法的基本思路就是一系列“GC Roots”作为起始点,从这些节点向下搜索,搜索到的所有引用链中的对象都是可达的,其余的对象都是不可达的,如上例,即使c1,c2互相引用,但是c1,c2都不属于GC Roots对象,因此都不可达 。
Java中,以下几种对象可以作为GC Roots:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象 。
  • 本地方法栈JNI方法引用的对象 。
  • 方法区类的静态属性引用的对象 。
  • 方法区常量引用的对象 。
3.引用的分类
了解了GC Roots之后,我们可能会希望存在这么一种对象,内存够的时候不进行回收,当需要内存时再将其回收 。JDK 1.2 中对引用进行了扩充 。将引用分为了4种,从强到弱依次为;
强引用(Strong Reference)
我们一般情况下使用的都是强引用,如Object o = new Object(),之类的代码 。只要强引用还在,垃圾收集器就永远不会回收被引用的对象 。
软引用(Soft Reference)
SoftReference类来实现,用来描述一些还有用但是不必须的对象,在系统如果不回收就会发生OOM时才会对软引用进行内存回收 。
弱引用(Weak Reference)
WeakReference类来实现,描述非必需的对象,强度弱,只能活到下一次发生垃圾回收前,无论那时内存是否短缺,都会对软引用对象进行内存回收
虚引用(Phantom Reference)
PhantomReference类实现,不会对生存时间发生任何影响,唯一目的时能在这个对象被收集器回收时得到一个通知 。
4.其他
及其不建议使用finalize()方法,虽然可以在回收时被调用,但是finalize()方法的执行代价高昂,不确定性大,无法保证各个对象的调用顺序 。使用finalize()能做的工作,使用try()finally()或其他方式可以执行的更好 。大家可以忘记JAVA中有这个方法的存在 。本身就是在JAVA刚诞生时向C/C++程序员做的妥协,但是未得到优化 。
方法区(永久代)进行GC的效率极低,花费较大,但是在大量使用反射、动态代理等场景都需要虚拟机具备类卸载的功能,以保证永生代的空间 。
二.垃圾收集算法1.标记清除算法(Mark-Sweep)
【GC 一篇文章彻底了解Java垃圾收集机制】算法分为两个阶段,标记与清除 。
标记阶段:标记出所有需要回收的对象 。回收阶段:将所有标记区域回收 。由于该算法不对空间进行整理,因此会产生大量的内存碎片,内存空间碎片过多会导致在分配较大的对象时,因为没有连续的内存而不得不提前触发一个GC 。另外,标记与清除的过程效率都不高 。这也是最基础的GC算法 。


推荐阅读