JVM中垃圾回收概念和算法

      说道垃圾回收,首先需要理解的是,什么才算做为垃圾,我们所知的垃圾回收指存在于内存中的、不会再次被使用的对象,而回收则是相当于将原来对象所占用的内存进行清除,这样的话就能够有足够的空间进行运行,所以如果我们不对产生的对象进行清除的话那么就会出现内存全部占用,造成内存溢出的情况,所以,垃圾回收也算一种常用的内存清理,在早期的C++中,其实是通过new和delete控制对象的创建和销毁,而在java中仅有new标识符,没有delete,其实在java中对于不适用的对象java有自己的内存回收机制,这个机制允许程序开发者不必显式的去调用delete类似i的操作进行对象的销毁,所以,我们需要了解一下,在内存中,或者说是在堆中是如何进行垃圾回收的。

      其实这种垃圾回收机制并不是在现在才有,早在20世纪60年代垃圾回收机制就已经被lisp语言所使用,现在除了java,C#和Python等都有类似的垃圾回收机制,可以说类似于这种的垃圾回收机制是现代开发语言一个需要的标准吧。

 1.引用计数法

引用计数法,是一种较为古老的垃圾回收算法,这个在微软的COM组件技术等可以找到这种计数的缩影,具体的原理较为简单,其过程如下:   
       .首先找到一个对象,这个对象如果被引用了,那么这个这个对象中的引用计数器就加一,假如说这个对象的引用失效了,那么就减去1,从算法的过程来看,实现并不是很困难,只是核心的想法就是在对象中添加一个计数器,这个计数器记录对象被引用的次数,不过这个回收算法也有很大的缺点:
1.我发处理循环引用的情况,因此java的垃圾回收器中并没有应用这个算法,其实现以下也明白,循环中每次作用的每个对象都依赖另一个对象,组成了一个换种的依赖引用关系,但是,每次被依赖就不确定到底哪个是开始,那个是依赖的结束,所以,一旦碰到这种情况,算法失效。
2.当我们在进行处理加减计数器的时候,都会伴随一个加减法的操作,所以对整个系统的性能都会有一定的影响。加入还是不明白循环引用,那看下面的例子:

对应不可达对象的情况就属于循环引用的情况,因为他们的计数器的值都不为0 ,假如出现这种情况,内存就无法快速的被释放回收。可以看到其不仅仅有性能上的问题,而且还用一定的技术缺陷,没有对不可达对象进行合理的处理,所以java中没有采用这个算法。

2.标记清除法

标记清除法是现代来及回收算法的思想基础,为什么这么说呢,其实看一下下面其工作的过程就会体会到了,其包含了标记阶段和清除阶段:

标记阶段:

一种可行的办法是通过根节点标记所有的从根节点出发的开始的可达的可达对象,也就是说不可达的对象应该不会被标记,也就是说我们认为的垃圾对象,不再使用的对象不进行标记。

清除阶段:

清除阶段清除所有的未被引用或者是使用的对象,将没有被标记的对象进行释放,释放空间供下次使用。

结合上面的图可以发现使用此算法可以对一块连续的内存空间进行回收,从根节点开始标记了存活的对象,清除阶段将不可达对象(垃圾对象)进行了清除,但是可以发现,清楚后的空间并不是连续的,在对象的堆空间分配过程中尤其是大对象的内存分配,不连续的内存的工作效率要低于连续的空间,因此这个是该算法的最大的缺陷。

3.复制算法

复制算法的核心是:将原有的内存空间分为两个部分,每次只使用其中的一个部分,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存中,然后清除正在使用的内存中的所有的对象,交换两个内存的角色,完成垃圾回收的过程。
但是如果系统中有很多的对象被确定为垃圾需要回收的时候,可以将其中的存活的较少的对象复制到其他的内存中,将这个内存中所有的对象进行清理,这个可以保证清理的内存具有很好的连续性,可以保证没有过多的碎片产生,但是复制算法的代价是系统中系统内存将折半,因此单纯的复制算法也很难让我们接受,结合上面标记清除算法的示意图,其标记部分的工作大致相同不同的是,清除部分,复制算法就需要两个空间,并将存活的对象进行了复制:


其实,在Java中的新生代串行垃圾回收器中,就使用了这种复制算法的思想,新生代分为eden空间,from空间和to空间三个部分,其中from和to空间可以视为用于复制的两个大小相同地位相等,且可以进行角色互换的两个空间块。from和to空间也被称为survivor空间,即幸存空间,用于存放没有被回收的对象:

        在垃圾回收时,假设eden空间中存活的对象被复制到未使用的survivor空间的to空间,正在使用的为from空间,那么from空间中的年轻对象也会被复制到to空间,如果遇到大对象或者是老年对象则直接进入老年代,如果to空间已经满了,那么对象也会进入老年代,此时的eden空间和from空间中的剩余的对象都是垃圾对象,可以直接清空,to空间中则存放了此次回收后存活的对象,这中改进的算法既保证了空间的连续性,同时也保证了空间的连续性,同时也避免了大量的内存空间浪费。对象复制到对应位置后,eden和survivor空间的from空间中的对象都会被清理掉。

4.标记压缩法

     复制算法的高效是建立在存货对象少,来及对象多的情况下,这种情况经常的在新生代发生,但是,老年代存留的大多数对象都是存活对象,如果依然使用复制算法,成活对象偏多,复制的成本也很高,因此老年代需要使用其他的算法进行回收。
     标记压缩算法是一种老年代的回收算法,它在标记清除算法的上做了一些优化,和标记算法一样,也需要从根节点开始,对所有的可达性对象做一次标记,但是之后它并不是简单的清理对象,而是将所有的存活对象压缩在内存的一端,之后需清理边界外的所有空间,这种方法既避免了碎片空间的产生,同时又不需要两块相同的内存空间,因此性价比较高。在内存中其会将标记后的对象移动到一端,并保持引用关系,之后清理边界外的工作就可以完成。
 

5. 分代算法

     上述的算法都有自己的优点,其中并没有一种算法可以完全的替代其他算法,都具有自己的优势和特点,所以根据垃圾回收的特性选择性的选择垃圾回收算法才是较好的选择,分代算法就是基于这种理念,根据内存间对象的特点分成几块,根据每块内存的特点使用不同的垃圾回收算法,提高牢记回收的效率。
     一般来说,Java虚拟机会将所有的新建对象都放置在新生代内存区域,新生代的特点是对象朝生夕灭,大约90%的新建立的对象都会被回收,因此,新生代比较适合使用复制算法,对象经过几次新生代的回首之后仍然存活,那么这个对象就会被放入老年代内存区域,在老年代中几乎所有的对象都是经过几次垃圾回收后存活的对象,因此这些对象在短时间内不会被回收,可以认为这些对象在一段时期内,甚至是整个应用程序的整个生命周期中,将是长期存在的。
因此,我们可以简单的认为,新生代适用复制算法,老年代则使用标记压缩方法或者是标记清除法,提高垃圾的回收效率。
       通常说来,新生代的回收频率很高,但是每次都会消耗较短的时间,而老年代的回收频率较低,会消耗跟更多的时间,为了支持高频率的新生代回收,虚拟机使用一种叫做CardTable的数据结构,卡表为一个比特位的集合,每一个比特位可以用来表示老年代某一区域中所有对象是否持有新生代对象的引用,这样新生代GC时,可以不需要花费大量的时间扫描所有的老年代,来确定每一个对象的引用关系,而可以先扫描卡表,只有当卡表的标记位为1的时候才需要扫描给定区域的老年代对象,而卡表位为0的所在的区域的老年代对象一定不含有新生代对象的引用。

6分区算法:

      分代算法是将算法按照对象的生命周期的长短进行了一次划分,而分区算法则是将整个堆空间划分成连续的不同的小区域,每一个小的区域都会独立使用,独立回收,这种算法的好处是可以一次控制回收多少个小的区域。一般的,相同条件下堆空间越大一次GC的时间就越长,从而产生的停顿时间也较长,有为了更好的控制GC产生的停顿时间,讲一个大的内存区域分割成多个小块,根据目标的停顿时间每次合理的回收若干个小区间而不是整个的堆区间,从而减少一次GC的停顿。

猜你喜欢

转载自blog.csdn.net/qq_18870127/article/details/79894680