垃圾收集算法

/ Jvm / 没有评论 / 316浏览

垃圾收集器是Java虚拟机中自带的功能,它的目的是帮助我们管理内存,正是因为有它的存在所以,我们在开发时,基本不用考虑内存溢出等问题。基本不用考虑不代表,一定不会遇到内存溢出等问题,在上一篇中我们用简单的方法模拟了一些内存溢出的问题,在这一篇我们将重点分享一下,Java中垃圾收集器的一些实现算法知识,了解这方面的知识有助于,在内存溢出时,快速的解决问题。垃圾收集器的主要功能有3个。

实现上述的3个功能,有很多种算法,下面我们具体看一下每一种算法的利与弊

给对象添加一个引用计数器,每当有对象引用时,计数器的值就加1。当引用失效时,计数器的值就减1。这样当对象的计数器为0时,该对象就是没有被引用的对象,也就是可以被回收的对象了。但这种算法有一个弊端,就是如果对象循环引用时,这两个对象的引用计数器的值都不为0,这样,垃圾收集器就无法准时的回收它们了。正是因为有这样的弊端,所以在Java虚拟机中的垃圾收集器并不是采用这种算法实现的。

通过称为“GC Roots”的对象作为起始点,从这些节点开始搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,就说明当前对象是不可用的。 也就是可以被垃圾收集器回收的对象。

QQ截图20170410165404.jpg

在Java中,可以做为GC Roots的对象主要包括下面几种:

上面我们提到了引用,下面我们看一下引用的介绍。引用:如果某类型中的数据存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在Java中引用主要包括4种它们分别是:

在可达性分析算法中当真真正正确认一个对象是否可以回收,通常要经历两次标记过程。当对象进行分析后,发现没有与GC Roots相连接的引用链,那么它还会对已经标记的对象,进行最后一次筛选,目的是判断对象是否可以执行finalize()方法。 当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机将回收此对象。如果对象覆盖了finalize()方法,并且在finalize()方法中重新创建了新引用,那么虚拟机将在第二次标记时,将此对象移除出“即将回收”的集合。如果对象在虚拟机执行finalize()方法时还没有任何引用链,那此对象将基本被回收。finalize()方法是对象防止被垃圾收集器回收的最后机会。

算法主要分为标记和清除两个阶段。首先要标记出所有需要被回收的对象,然后在所有对象标记完成后,在统一回收已标记的对象。这种算法有两个弊端:一个是效率问题,标记和回收时所消耗的时间比较长。别一个弊端就是在回收时因为对象有可能不是连续的,所以导致回收后的内存也是不连续的,也就是会造成很多内存碎片,内存碎片大多有一个问题,就是如果虚拟机要分配大的对象时,由于无法找到满足条件的足够的连续内存,所以不得不在一次触发一次垃圾收集,所以性能较慢。

在标记-清除算法中因为有效率等问题,所以,复制算法出现了。它基本逻辑是将内存分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完时,就将还存活着的对象(也是采用标记的方式)复制到另外一边,然后再把这一块的内存回收。 这样使回收后的内存不会在有内存碎片了。 但这种方式也有弊端就是它将内存划分了两半,每次使用时只能用一半内存来存储数据。还有一个弊端就是,因为要复制已经存活的对象,如果存活的对象比较多时,复制时所消耗的时间比较大,效率比较低。

标记-整理算法也是先采用标记方式,找出所有存活的对象,然后不在是复制方式,而是将所有存活的对象都向一端移动,然后在回收边界以外的内存。

QQ截图20170411114934.jpg

分代收集算法是根据对象的存活周期的不同将内存划分为不同的几块。 一般是把堆分为新生代和老年代。 在新生代中因为大部分对象都不是存活的,所以只要进行少量的复制即可,所以采用复制算法。 而老年代中因为大部分对象都是存活的,如果拷贝的话,效率会有影响,于是就就采用“标记—清理”或者“标记—整理”算法来进行回收。