什么是GC垃圾回收呢。日常生活中我们去餐厅吃饭吃完饭,吃完饭走了餐具不用管,服务员在把餐具拿走,这是一种方式,服务员怎么知道他要来把餐具拿走呢,因为你走了,这个位置空了。服务员什么时候拿走餐具很重要,第一你没吃完,不会回收吧,第二很多人吃饭,你在里面,他也不一定回收吧,他会找一个合适的时机,一次性回收很多吧。
第二种方法:自己吃完饭把餐具带到回收的地方吧。
哪一种方式更好呢?
第一种方式自己也就是使用者不用管,服务员找一个合适的时机把垃圾回收了。
第二种自己去吧垃圾倒了。
在java中GC垃圾回收用的是第一种方式,系统找一个合适的时机把垃圾回收掉,我们不用手动去干预。这也是C#使用的垃圾回收机制。开发员人员只关心内存申请,不关心内存释放的问题
下面我们看一看垃圾回收GC回收算法。
1.1. 引用计数法
引用计数法是一种古老的垃圾收集方法,引用计数器实现很简单,对于一个对象A,有任何一个对象引用了A,那么A的计数器+1,引用失效时,A的计数器-1。当A的引用计数器是0的时候。那么A对象就不能被使用了。
引用计数法实现很简单就是额外的为每一个对象搞一个计数器,但是也有缺点:
1.引入计数器是解决一个问题又附加的引入一个问题。因为每次加减操作,可能影响系统性能。
2.无法处理循环引用问题,因此java的垃圾回收器中,没有使用这种算法。
下面举一个简单的例子:A对象和B对象,对象A中有B的引用,对象B中有A的引用。此时A和B两个对象的计数器都不是0,但在使用中没有其他对象引用A和B,说白了,A和B应该是被回收的对象,但是他们之间相互引用。计数器不为0,系统没有这种探针检测到循环引用,从而无法垃圾回收,引发内存泄露。
解释一个可达对象,不可达对象。
可达对象:通过根节点进行引用搜索,最终可以到达的对象。
不可达对象:通过根节点进行引用搜索,最终没有被引用的对象。
不可达的对象出现循环引用,它的引用计数器都不是0如下图所示:
1.2. 标记清除法
标记清除算法将垃圾的回收两阶段进行:标记阶段和清除阶段。
在标记阶段,顾名思义,就是从根节点开始标记可到达的对象。没有被标记的对象也就是没有被引用的对象那个就是垃圾对象。在第二个阶段也就是清除阶段,清除所有没有被标记的对象。标记清除算法。可能会产生空间的碎片,这也是这个算法的最大的问题。
如下图所示,使用标记清除算法对一块连续的内存空间进行回收,从根节点开始,所有的引用关系对象被标记为存活对象,
不可到达的对象被标记为垃圾对象,在清除阶段回收所有不可到达的对象,也就是没有被引用的对象。
如上图所示,回收后的空间是不连续的。在对象的堆空间分配中,大对象的内存分配,不连续的内存空间的效率低于连续的空间。这也是该算法的最大缺点。
标记阶段通过根节点查找所有可达对象,清除节点清除所有的不可达对象。完成系统的垃圾回收。
1.3. 复制算法
复制算法的原理:将分配的内存空间分为2块,每次只是用1块,在垃圾回收呢时,讲正在使用的内存中的存活对象复制到没有被使用的内存的一块,完成之后,清除正在使用的内存块中的所有对象,然后两个内存块交换,以此完成垃圾回收。
如果系统中的垃圾对象有很多,存活对象比较少,复制算法的优势就体现出来了。由于对象是在垃圾回收过程中统一被复制到新的内存空间,所以复制算法可以确保新的内存空间是没有碎片的。但是缺点就是,把内存分为了2块,不能充分的利用内存。
如下图所示:A、B两块相同的内存,A在进行垃圾回收的时候,讲存货的对象复制到B的空间,B的空间在复制后是连续的。复制完成之后,清除A。把B设置为当前使用的空间。
在java的新生代串行垃圾回收中,使用了复制算法,新生代之前的章节也讲过分为eden区域,from、to三个部分,之前也说过from、to是相等的可以替换的两个空间、地位相等、角色相等。from、to空间也被称之为survivor空间,英文解释为幸存者,用来存放没有被回收的对象,如下图所示。
新生代:堆空间中的年轻人,对象刚刚创建或者经历的垃圾回收次数不多的对象。
老年代:堆空间中的老年人,老年对象指经历了很多次垃圾回收亦然存活的对象,(看来大部分老年人存贮内存挺高的对象)。
在垃圾回收的过程中,eden空间存活的对象被复制到survivor空间,假如我们正在使用from空间,因此den空间存活的对象被复制到to空间。如果to空间内存满了,则对象直接进入老年代,可参考虚拟机参数设置的章节参考实例。此时eden空间和from空间剩余的对象就是垃圾对象,直接清空,to空间存放的就是存活的对象,这种改进之后的算法,保证了空间的连续性,这点很重要,又避免了大量的空间的浪费,因为这些空间是可以手动分配的。参考之前的章节。上图显示了复制算法的时机回收过程。
复制算法比较适合新生代,因为新生代垃圾对象一般比存活对象要多,这块区域,朝生夕死足矣。如果这个地方的对象存活就可能是常住内存的对象了。
1.4. 标记压缩法
上面也说了 压缩算法的使用前提是存活对象少、垃圾对象多的情况下使用。这种情况经常发生在新生代,在老年到就不合适了,因为老年代大部分都是存活对象,如果要使用复制算法,复制的成本很高,所以老年代使用的是其他的算法,老年人比较特殊嘛。
标记压缩算法是在标记清除算法基础之上优化了,标记压缩算法也是从根节点开始,对对象的可达性做一次扫描标记,之后不是直接清除未标记的对象,而是将所有的存活对象压缩到内存的一端,之后,清除边界外的所有空间,这样做的好处是避免了内存的不连续性,不需要内存设置两块相同的内存进行复制交换。所以压缩标记算法的性价比好啊。
标记压缩算法如下图所示完成垃圾回收。
标记压缩算法其实就是在标记清除算法完成之后,进行一次随便整理。所以称之为标记压缩算法。
1.5. 分区算法
上面说的都是出来堆空间的算法,对于栈是如何处理的呢。分区算法是将整个堆空间分成连续的不同的小区间,如下图所示。每一个小区间都是独立使用,独立回收,这样设计可以控制一次回收多少个区间。不回去全扫描。
一般来说在相同的条件下,堆空间越大,一次垃圾GC回收所需要的时间就越长,那么引起的副作用就是停顿时间越长,就是假死需要等待的时间就越长,使用分区算法的好处就是,合理的回收区间,不是扫描整个堆空间,从而减少一次GC产生的停顿。跟餐厅收餐具一样一样,分房间去收餐具,不是每次都按照顺序收餐具。