垃圾回收是Java的一大特性,主要发生在堆中,在垃圾回收的过程中需要完成三件事:
1)哪些内存需要回收?
2)什么时候回收?
3)如何回收?
1、哪些内存需要回收
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是需要确定这些对象中哪些还存活,哪些已经死去。
如何判断对象已死(不可能再被任何途径使用的对象)?
1)引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时候,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。该方法原理简单,判断效率也高。但是在以下情况(循环引用)下该方法失效:
A对象引用了B对象,A对象引用计数为1,B对象也引用了A对象,B对象的引用计数也为1,但是没有其他变量再引用A或B对象,所以虽然这两个对象都不会再被引用,但是不能被当做垃圾进行回收,会造成内存上的泄露。早先的Python垃圾回收采用的是这种,Java中采用的是可达性分析算法。
2)可达性分析法:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在Java体系中,固定可作为GC Roots的对象包括:
1)在虚拟机栈中引用的对象;
2)在方法区中类静态属性引用的对象;
3)在方法区中常量引用的对象;
4)在本地方法栈中本地方法中引用的对象
5)Java虚拟机内部的引用
6)所有被同步锁持有的对象
引用细分
传统定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。
后来将引用分为:强引用、软引用、弱引用和虚引用。
强引用:一般new对象都是强引用,只有所有GC Roots对象都不通过强引用引用该对象时候,该对象才能被垃圾回收
软引用:描述一些还有用、但是非必须的对象。仅有软引用引用该对象的时候,在垃圾回收后,内存仍然不足时会再次发生垃圾回收,回收软引用对象
弱引用:用来描述那些非必须对象,但是它的强度比软引用还要更弱一些。仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
虚引用:最弱的一种引用关系,为一个对象设置虚引用关系的唯一目的就是为了能在这个对象被回收的时候收到一个系统通知。
2、垃圾收集算法
标记-清除算法
定义:算法分为“标记”和“清除”两个阶段,首先标记处所有需要回收的对象,在标记完成之后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有没有被标记的对象。
优点:简单
缺点:第一是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这个时候必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随着对象数量增长而降低;第二是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象的时候无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。
标记-复制算法
简称为复制算法,将内存容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性的清理掉。
优点:实现简单,运行高效,没有空间碎片
缺点:可用内存缩小为原来的一半,空间浪费未免太多了一点
标记-整理算法
该算法的标记过程与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
标记-清除算法与标记-整理算法的本质差异是在于前者是一种非移动式的回收算法,而后者是移动式的,是否移动回收后的存活对象是一项优缺点并存的额风险决策:
1)如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方金辉是一种极为负重的操作,而且这种对象移动操作必须全称暂停用户应用程序才能进行。
2)如果和标记-清除一样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题只能依赖更为复杂的内存分配和内存访问其来解决。
3、垃圾收集器
堆内存逻辑分区:
以上对内存分区不适用不分代垃圾回收器。
1)Serial收集器:单线程工作的收集器,其单线程意义并不仅仅是说明它只会使用一个处理器或者一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集的时候,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”带来恶劣体验。
优点:简单高效,没有线程交互的开销,专心做垃圾收集
缺点:“Stop The World”的恶劣体验。
2)ParNew收集器:实质上是Serial收集器的多线程并行版本,只能与CMS收集器配合工作。
3)Parallel Scavenge收集器:一款新生代收集器,同样是基于标记-赋值算法实现的收集器,也是能够并行收集的多线程收集器。但是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器中消耗时间的比值,吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)。该收集器也称为“吞吐量优先收集器”
4)CMS收集器:是一种以获取最短回收停顿时间为目标的收集器,CMS收集器是基于标记-清除算法实现的,整个过程分为四个步骤,包括:
初始标记、并发标记、重新标记、并发清除。其过程如下:
初始标记:会发生“Stop The World”,初始标记仅仅只是标记GC Roots能直接关联到的对象,速度很快;
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程消耗时间较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
重新标记:会发生“Stop The World”,该阶段就是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但是也远比并发标记阶段的时间短;
并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
CMS收集器优点:并发收集,低停顿
CMS收集器缺点:对处理器资源非常敏感;无法处理“浮动垃圾”,有可能出现“Concurrent Mode Failure”失败而导致另一次完全“Stop The World”的Full GC产生;基于标记-清除会导致大量空间碎片产生
5)Garbage First收集器:也称为G1收集器,并非纯粹追求低延迟,其目标是在延迟可控的情况下获得尽可能高的吞吐量。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域,每个区域都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,有限回收垃圾最多的区域。该过程分为四个步骤:
初始标记、并发标记、最终标记、筛选回收