JVM内存垃圾回收:复制、标记回收和引用计数法

      还记得以前自己写C程序时,需要手动为指针变量申请空间,最后回收不用的指针,像这样:

Typedef struct LinkList *List;

List *p;

p = (List)malloc(sizeof(struct List));

// ……

free(p);

      有时会忘记free释放掉不用的指针,有时是不确定何时需要释放空间,对于我自己来说虽然都是学习过程中写的一些简单算法,即使忘记释放空间,也不至于造成内存泄漏,但是对于大型程序,内存的申请和释放操作频繁,如果不时常对程序中的“垃圾”空间进行清理和回收复用,那么这些无用的对象变量就会一直保留,无法被其他对象使用,直到程序运行结束,如果可分配空间不足,程序就可能出现内存溢出。JVM提供的自动内存管理,GC(garbage collection),大家应该都熟悉,其用来回收内存中不再被使用的对象,通过遍历Java堆上的所有对象,查看他们是否有被引用,如果没有任何引用,则可以对其进行回收。

 

复制算法

      复制算法进行垃圾回收,复制的是什么?把它复制到哪里?首先将内存空间分成两部分,假设A和B,A部分分配给需要的对象,另一部分B孔金留着垃圾回收时使用。在进行垃圾回收时,遍历所有的对象,将有引用的活的对象从A空间复制到B空间中,那些没有引用了,不再被使用的对象,则当作垃圾回收,它们不需要复制,就这样,将所有活的对象复制到新空间后,清除旧空间里的所有垃圾对象,完成一次GC。

JVM内存垃圾回收:复制、标记回收和引用计数法

      像上图这样,垃圾回收过程将A空间里的可回收存活对象复制到B空间里,并且让它们呈连续排放,最后清空原来的A空间,并把B空间设置为当前使用的空间。复制算法的优点在于,即使当前使用空间里需要回收的垃圾对象很多,但只需将那少量的活的对象复制到新的空间即可,其他的直接清除,回收过程复制对象到新的空间时也会将它们连续排放,效率还是比较高的。缺点也很明显,就是需要将系统的内存空间平分成两份。

JVM中的复制回收

      在JVM的垃圾回收器中,复制算法将新生代(也就是存放年轻对象的堆空间,指那些刚创建的对象和尽力垃圾回收次数不多的对象)堆空间分成三个区,eden、from和to区,from区和to区是两块大小相同的空间。在垃圾回收时,eden区的活的对象和from区中年轻对象被复制到to区,如果复制时to区满了,那么对象会变成老年对象(经历了很多次垃圾回收但仍然存活没有被回收的对象),放到老年代堆空间中,复制完成后,eden区和from区中的需回收垃圾对象就可以全部直接清除。

 

标记回收法

      标记回收法需要遍历一遍所有的对象,类似于图的遍历,从根对象开始作为起点,标记每一个可以到达的对象,对象可到达表示该对象还存在引用,而那些不可到达的对象则表示在程序中已经不存在引用了,是垃圾对象,扫描完所有对象后,将未被标记的对象清除即可。

JVM内存垃圾回收:复制、标记回收和引用计数法

      可以看到,标记回收法对一整块内存A空间进行扫描,标记出每一个可以到达的对象,在所有对象标记完毕后,剩下的垃圾对象直接回收掉,得到B空间。和复制算法对比可以看出,复制算法进行GC在复制过程可以把对象连续排列,得到新的连续的空间,而标记回收方式GC后得到的内存空间是不连续的,会产生内存碎片。总的来说,标记回收方式分为两步骤,第一步先遍历所有对象,标记出可到达对象,为活的对象,不可达的标记为垃圾对象,第二步直接清除所有的不可达对象,完成GC。

引用计数法

      引用计数法和标记回收法类似,都会对对象进行一些标记,根据标记的状态来决定哪些对象是活的,可以保留,哪些是垃圾对象,需要回收。具体的做法是在每一个对象创建时为其开辟一个空间定义一个引用计数器,当有一个对象引用了这个对象后,引用计数器就会+1,当某一个引用无效后,引用计数器就-1,当对象中的计数器值为0后,表明该对象已经不再被使用,可以进行回收。该方法在对象每次产生引用和失去引用时都需要做加法和减法操作,对性能有一点小小的影响,除此之外有一个更重要的问题是,如果两个或以上对象出现互相循环引用的情况,那么引用计数法无法辨别出来。

循环引用问题

      对象之间互相循环引用的情况,举个例子,有两个对象S1和S2,S1存在对S2的引用,S2也存在对S1的引用,所以两个对象中的引用计数器值都等于1,除此之外,程序中没有任何的对象对它们两个进行引用,正常情况这一对对象S1和S2是需要被回收的,但是由于它们的引用计数器值都不为0,所以GC无法对其进行回收。

JVM内存垃圾回收:复制、标记回收和引用计数法

      上图可见,从根对象开始扫描所有可到达的对象,存在循环引用的S1和S2对象已经不可达,但由于计数器的值显示它们都存在引用,所以GC不会对它们进行回收,由此产生的内存垃圾如果越堆积越多,最后也会产生内存泄漏。