垃圾回收(Garbage Collection, GC)诞生于1960年的Lisp语言。在Java虚拟机运行时数据区中,虚拟机栈、操作数栈和程序计数器都是随线程而生,随线程而亡。当方法或者线程结束时,这部分的内存就自然回收。而堆和方法区这部分内存的分配是动态的,因此需要在程序运行期间进行动态回收。
对于垃圾回收,主要关注两点。第一,如何确定一个对象是否存活。第二,如何回收。
对于如何确定对象是否存活,目前有两种方法:
1.程序计数器
给对象添加一个引用计数器,每当有引用指向这个对象,计数器就加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。虽然这种方法简单高效,但是有一个致命的弱点,它很难解决对象之间互相循环引用的问题。
例子:
2.可达性分析
通过一系列称为"GC Root"的对象作为起点,向下检索,检索的路径称为引用链。当一个对象的GC Root对象没有任何引用链时,则这个对象是不可用的。
GC Root可以是以下几种。
1、虚拟机栈(栈帧中的局部变量表)中引用的对象。
2、方法区中静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中JNI(常说的Native方法)引用的对象。
可以想象一下,当我们一直往房间里堆东西,很快的房间就放不下其他东西了,所有我们会经常对房间进行清理以保证有活动的空间。对于堆来说也是如此,需要定期的清理不需要的对象保证有足够的空间可以存放新生成的对象。对此JVM提供了4种垃圾回收算法。
1.标记清除
将可回收的对象做上标记,最后统一回收被标记的对象。这是最简单的垃圾回收算法,有2点不足的地方。 一是效率问题,标记和清除两个过程效率都比较低。二是空间利用率的问题,单单标记和清除容易产生大量不连续的内存碎片,这将会导致内存无法分配一整块大的内存而进行一次垃圾回收。
2.复制算法
将可用内存分为两块,每次只使用其中的一块,当这一块内存用完时,将存活的对象复制到另一块去,以此类推。这种回收算法简单高效,但是空间利用率很低,每次只能使用一半的内存,确实是有点浪费。经过IBM专门研究表明,新生代中98%的对象都是"朝生夕死"的,所以可以将新生代切割成一块较大的Eden区和两块较小的Survivor区。每次使用一块Eden区和一块Survivor区,每次回收将生存的对象放到另一块Survivor区,这样每次就只浪费10%的内存空间。但生存下来的对象不可能每次都小于这10%的内存,这时候就要求助于老年代来存放这部分对象。
3.标记整理
这个算法相对于标记清除算法稍微高级一点。它比标记清除算法多了一步让存活的对象向一边移动的过程,最后只清理边界以外被标记的对象。
4.分代收集
这个算法就相当于以上几个方法在不同堆区域中的综合适用。因为新生代每次回收只会存活少量对象所以适用复制算法,而老年代中的对象通常生存周期长而且对象占用的空间也比较大,没有多余的空间对这些对象进行空间担保,因此多使用标记算法。
垃圾回收发生在堆中,而堆内存一般分为新生代,老年代和永久带。再细致一点,新生代可以分为一个Eden区,From Survivor区和To Survivor区。被创建的对象优先在Eden区分配,当Eden区内存不足时,触发一次Minor GC。因为Java对象大多数生命周期较短,所以Eden区都使用复制算法来回收对象。当需要大量的连续内存来存放Java大对象时(长字符串或数组),直接进入老年代。虚拟机提供-XX:PretenureSizeThreshold参数设置,如果对象的空间需求大于该参数则直接进入老年代。想起有部分生命力顽强的对象一直在Survivor区来来去去,经历了这么多次打拼,这些对象也应该告老还乡了。虚拟机为每一个对象定义了一个年龄计数器,对象每经过一次Minor GC,年龄计数器+1。当年龄达到指定值时(默认是15,可以通过-XX:MaxTenuringThreshold设置),对象就晋升到老年代。
在新生代对象晋升之前,虚拟机会先去检查老年代最大可用的连续内存是否大于晋升对象所需要的内存空间大小,如果成立则这次的Minor GC是安全的。反之虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于这次晋升对象的平均大小,如果大于则进行一次Minor GC,如果小于或者HandlePromotionFailure设置不允许冒险,这时就要进行一个Full GC。
上面讲了很多关于回收的算法,它只是垃圾回收的方法论。接下来讲述垃圾回收关注的第二点,如何回收。垃圾回收其实主要是通过各种收集器来进行的。
Serial收集器
串行收集器是最古老,最稳定以及效率高的收集器。它是一个单线程收集器,单线程的意思是它工作的时候只能它一个线程在工作。好比他吃饭的时候别人必须只能看着他吃。新生代采用复制算法,老年代采用标记整理算法,工作时暂停其他所有线程。
-XX:+UseSerialGC 串行收集器。
ParNew
它是Serial收集器的多线程版。新生代并行执行,老年代并发执行。
并行:多条垃圾收集线程并行工作,但用户线程仍处于等待状态。
并发:用户线程和垃圾回收线程同时工作。
-XX:+UseParNewGC ParNew收集器。
-XX:ParallelGCThreads 限制线程数量。
Parallel Scavenge
Parallel Scavenge 与ParNew收集器类似,它更关注系统的吞吐量。它可以通过调节执行GC自 适应调节策略来达到最大吞吐量的目的。
-XX:+UseParallelGC 使用Parallel收集器+老年代串行。
CMS(Concurrent Mark Sweep)
它是一种以获取最短回收停顿为目标的收集器。常用于互联网或B/S此类重视响应速度的服务端。
CMS的清除过程相对于前面几种更为复杂。
1.初始标记 stop-the-world 仅标记GC Roots能直接关联到的对象。
2.并发标记 stop-the-world 进行GC Roots Tracing的过程 停顿时间长。
3.重新标记 修正并发标记期因用户程序继续运行导致标记变动的部分。
4.并发清除 与工作线程一起工作。
-XX:+UseConcMarkSweepGC 使用CMS收集器。
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长。
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理。
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)。
优点:并发收集,停顿时间短。
缺点:使用标记清除算法产生大量不连续空间碎片,并发阶段降低吞吐量。
G1
它是当前垃圾收集器最前沿的成果之一,目标是在未来能替换掉CMS收集器。
优势:
1.并发与并行 缩短stop-the-world停顿时间。
2.分代收集 独立管理整个GC堆。
3.空间整理 采用标记整理算法不会产生不连续空间碎片。
4.可预测停顿 在追求低停顿的同时还能建立可预测的停顿时间模型,使消耗在垃圾收集的时间上不超过N毫秒。
清除过程:
1.初始标记 stop-the-world 触发Minor GC。
2.根区域扫描 回收survivor区域,这过程必须在minor GC之前完成。
3.并发标记 在整个堆中并发标记,并计算每个区域中存活对象的比例。
4.再标记 用于收集并发标记阶段产生的新垃圾。
5.复制清除 多线程清除对象,stop-the-world G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区并把它返回到空闲区域链表中。