谈到垃圾回收,大家都有一种很熟悉却又很迷茫的感觉。所谓垃圾回收,就是把那些用不到的数据从内存中清除掉,从而达到释放内存的作用。
#############################################################################################################
垃圾回收中的算法:
垃圾回收中的算法主要分为两部分,一个是如何判断对象已经死去,也就是如何判断内存中的“死数据”,另一个是如何清除数据,释放内存的算法。
先让我们来看下第一部分:除了JAVA,很多语言也实现了垃圾回收机制,如现在比较典型的python。python中判断对象是否存活的算法是引用计数法,即为每个对象维护一个引用计数器,当一个地方引用该对象时则计数器加1,当一个引用失效时,计数器减1。引用计数算法,实现简单,并且效率也很高,但是这类算法无法解决对象互相循环引用的问题:
ObjectA objA = new ObjectA(); ObjectB objB = new ObjectB(); objA.instance = objB; objB.instance = objA; objA = null; objB = null;l
JAVA中采用的算法:可达性分析算法,选取一系列叫做“GC ROOT”的对象作为起点(通常是常量,类静态属性,或者栈帧中的本地变量表),从这些节点向下搜索,形成引用链,与GC ROOT中不存在引用链的对象将会判定为可回收对象。事实上,java中对引用的分类并非只有引用有效,引用失效两种情况,根据不同情况下的回手策略,可以分为强引用、软引用、弱引用、虚引用等, 如软引用就是有用但是非必须的对象,只有在内存严重不足时,才会去考虑对这类对象进行回收。
了解完如何判定可回收对象后,接下来就是如何对这些对象进行回收:java采用分代进行回收的方式,即把内存分为新生代和老年代两个部分,新建的对象会放入新生代,经过了多轮次依然存留下来的对象会放入老年代。
一般JAVA虚拟机的新生代都会采取复制算法进行垃圾回收,即先把存活的对象复制到另外一个区域,再把当前区域一次清理掉,由于新生代中的对象大部分都是可以清理的,所以并不是简单的把新生代区域进行对半分,而是把它分成一个较大的Eden区和两个较小的survivor区,通常的比例为8:1,可以通过参数进行配置,至于为什么是两个suvivor,这是为了防止生成过多的内存碎片,通常新建的对象会放在Eden区域和其中一块survivor区上,进行垃圾回收时,把存活对象复制到另外一个survivor区上,当然,这么做会存在存活对象过多,survivor区存放不下的情况,这时候需要用到分配担保机制,把放不下的对象放到老年代去。
JAVA虚拟机的老年代采用的是标记-整理算法(Mark-Compact)算法,这在标记-清楚(Mark-sweep)算法上又加入了整理的一步,把所有的已用内存放到已一起,去除掉内存碎片。
几种常见的垃圾收集器:
新生代:
1、Serial :单线程垃圾回收器,在每次进行GC时都需要暂停所有的其他线程,并且只有一条GC线程
2、ParNew :Serial的多线程版本,在GC时也需要挂起其他线程,但是可以有多条GC线程
3、Parallel Scavenge:和ParNew很类似,但是关注点不一样,Parallel Scavenge关注的是CPU的吞吐量,即运行用户代码的时间/运行的总时间
老年代:
1、CMS:concurrent mark sweep,包括四个步骤,部分步骤可以与用户线程并发执行
2、Serial Old:Serial的老年代版本
3、Parallel Old:Parallel Scavenge老年代版本
最新的
G1:garbage first 这是当今收集器计数发展的最前沿成功之一,它的有点包括:并行与并发、分代收集、空间整合、可预测停顿。
G1在新生代和老年代的基础上又引入了Region的概念,新生代和老年代都是多个Region的集合,G1对每个Region都进行判断,每次只回收边际效益最高的Region,这种化整为零的思想大大提高了有限时间内的回收效率。接下来的GC又可以分为四个步骤:初始标记、并发标记、最终标记、筛选回收