在Java中,垃圾回收(GC)机制可谓是老生常谈了,大部分人都会把这项技术当作Java语言的伴生产物。谈及GC垃圾回收机制,必然就要讲讲对象的“生”和“死”,那他们是在哪里“生”和“死”的呢,它们的生死就是GC回收机制的具体体现。

  这里插一个题外话,因为垃圾回收机制是在Java虚拟机的内存中进行的,这个内存又被称为Java虚拟机运行时数据区,根据Java虚拟机规范Java SE7版的规定,主要划分以下几个数据区域:

其他的数据区域暂不做过的阐述,这里只针对Java堆做一个简单的介绍,Java堆也是虚拟机所管理的内存中最大的一块,主要是存储对象实例,几乎所有(不是绝对的)对象实s例都会在这里被分配内存。Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆,垃圾回收机制主要就在这个区域进行。

一、检测对象的生和亡

  引用《道德经》第四十二章的一句话:“道生一,一生二,二生三,三生万物。这句话亦可作为一个对象的缘起,即有“生”,必有“亡”,GC收集器回收垃圾便是这个一个具体的体现,回收前的一个步骤必定是检测对象是“存活”的和“死去”,这样才能有目的性的清除无用(无效)对象。

1、引用计数器算法

  或许你会接触过引用计数器算法,这一种检测堆中对象生死的算法,实现原理是:给对象添加一个引用计数器,当此对象被一个地方引用时,计数器值+1,当其引用失效时计数器值-1,任何对象引用计数器值为0时,表对象的引用时不可用的,最终会被GC回收。这种算法的实现简单且效率高,但这也面临另外一个问题,当存在两个对象,对象中都存在字段get:

      public  class PersonProperty{
      
          private PersonProperty get=null;
          
          public static void main(String[] args){
              PersonProperty p1=new PersonProperty();
              PersonProperty p2=new PersonProperty();
              p1.get=p2;
              p2.get=p1;
              p1=null;
              p2=null;
           
          }
     }

这个案例里面,两个对象相互引用,造成死锁,以此循环往复,在引用计数器中永远不可能为0,也就无法通知GC收集器回收它们,所以,Java中并未使用它作为JVM判断对象的生死的算法。

2、可达性分析算法

  前面拿引用计数器算法做了就简单分析,在Java商业JVM中,并未使用此算法作为判断堆中对象生死的实现方案,在这里引入了另外一种算法:可达性分析算法。其实现原理是:以GC Roots为根节点,向下搜索对象,如果有对象直接或间接通过引用链(搜索时所走过的路径)与GC Roots相连,则表明GC Roots到这些对象是可达的,也就是证明此对象是可用的,反之,则表明GC Roots到这些对象是不可达的,这些对象是不可用的,最终会通知GC收集器回收这些对象。下面我们给出一个简单的案例:

       public class  ObjectRefrence{

             private String objectName;

             private ObjectRefrence;

             public ObjectRefrenc(String objectName){
                        this.objectName=objectName;
             }
             public Object(String objectNmae,ObjectRefrence refernce){
                        this.objectName=objectName;  
                        this.refrence=refrence;     
             }

            public static void main(String[] args){
                  
                         ObjectRefrence A=  new ObjectRefrence("A");
 
                         ObjectRefrence B=  new ObjectRefrence("B");
  
                         ObjectRefrence C=  new ObjectRefrence("C");

                         A.refrence=B;

                         B.refrence=C;

                         new ObjectRefrence("D",new ObjectRefrence("E"))
            } 
        }

  在上述案例中,假设A是GC Roots的话,B和C与A都存在直接或间接的引用,它们之间存在直接或间接联系的引用链,所以对象A是B和C的的可达对象;对于D和E,虽然彼此之间存在对象可达性,但对作为GC Roots的A并没有直接或间接的引用,也就是A并不是D和E的的可达对象,最终D和E会被判定为可回收对象,它们的关系如下图所示:

  讲到这里我想大家都不知道什么是GC Roots吧?GC Roots其实就是对象,垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程的,即触发STW。然后再从GC Roots这些根节点向下搜寻,可达的对象就保留,不可达的对象就回收。那么哪些对象会成为GC Roots呢?作为GC Roots对象的可分成两大类:

下面就理解一下,这几类对象可以被称为GC Roots。

  • 虚拟机栈中的本地变量表引用:我们平时讲的“堆栈”中的“栈”,严格意义上来说是虚拟机栈中的局部变量表引用,里面存储了基本数据类型、对象引用指针、引用地址等。线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象可作为GC Roots。
  • 本地方法栈中JNI(Native方法)引用的对象:是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。与上面的原理基本一致。
  • 被同步锁持有的对象:被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁,GC如果回收了对象,锁就失效了。
  • 方法区静态属性对象引用:全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。
  • 方法区常量池引用对象: 属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。

二、垃圾收集算法

  由于垃圾收集算法的实现涉及到大量的程序细节,且各个平台的虚拟机操作内存的方式各不相同,这里面就探究以下几种垃圾收集算法的思想。

1、标记-清除算法

  这个算法的功能如其名,先是标记出所有需要回收的对象,在标记完成后,然后再回收被标记的对象。标记-清除算法的优缺点都很明显,优点:实现简单。缺点:效率低,因为要对所有的标记然后再清理,另外还会产生内存碎片。具体思路实现如图所示:

2、复制算法

  复制算法是在标记-清除算法的基础上改良而来,它的原理是将一块内存容量对半分,执行的时候先用其中的一半,另一半空着,当使用着的这块内存被占满时,就将存活的对象复制到另一半内存中,然后回收掉无用对象,值此,这部分的内存也空着了,另一半的内存就重复着这样的工作,一直循环的利用被划分的这两块内存。该算法的优点:高效,实现简单,能有效回收时清除产生的碎片。缺点:与其他算法相比较,需要两倍的内存空间。具体思路实现如图所示:

3、标记-清理算法

  此算法结合了标记-清除算法复制算法某些痛点而优化出的结果,它的原理是标记回收对象与标记-清除算法一致,但不是立马对可回收对象进行清理,而是先让存活的对象向一段移动,然后清理掉端边界意外的内存。其优点是:同要的结果,这种算法需要的内存比复制算法要小50%(无需划分内存),此外也不会产生内存碎片。缺点:实现的效率非常慢,毕竟要标记又要移动,最后清理这些都需要时间的。具体思路实现如图所示:

4、分代收集算法

  前面三种算法是分代收集算法的细化体现,因为当前商业虚拟机是采用了这种算法去实现的,它的思想是根据对象的存活周期划分几个内存模块,一般吧Java堆分为:年轻代和老年代。结构如下图所示:

  在年轻代中,又划分了Eden、From Survivor(S0)和To Survivor(S1),IBM有专门部门研究过,新生代中的对象98%是“朝生夕死”,其中,Eden区域是对象出生的地方,所以这个区域比其他两个区域的容量划分会大一些,在Eden中出生的对象通过先是可达性算法判断其对象与GC Roots是否可达,如果可达则这些对象就会被送入S0区域,同理的在S0区域也是先使用可达性算法判断对象是否存活,如果存活就通过复制算法 将存活对象复制到S1区域中,然后将S0的内存清除,这时S0区域的内存就空出来了,这时S0和S1进行同样的角色转换,处理的方式也一样,循环往复。那这些熬到最后的对象是怎么扔进老年区中的呢?在年轻代中,存活下来的对象每经过一次GC且在这次GC存活下来的对象就增加一岁,默认阈值是15,超过15次后,这些最终存活下来的对象就会被扔到老年区里面,存货对象晋升到老年区的年龄阈值,可通过参数-XX:MaxTenuringThreashold设置。