1. 哪些对象可以回收?
1.1 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是可被回收的对象。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其主要的原因是它很难解决对象之间相互循环引用的问题。所谓对象之间的相互引用问题。例如:除了对象objA和objB相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC回收器回收它们。
//循环引用案例
public class Test {
Object instance = null;
public static void main(String[] args) {
Test objA = new Test();
Test objB = new Test();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
1.2 根可达性算法
将GC Roots对象作为起点,从这些节点开始向下搜索引用对象,找到的对象都标记为非垃圾对象,其余未标志的对象都是垃圾对象,GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等。
2. 垃圾回收算法
2.1 Mark-Sweep(标记清除)
2.2 Copying(复制算法)
2.3 Mark-Compact(标记整理)
以上3种算法的区别:
- Mark-Sweep(标记清除):位置不连续,产生碎片。
如果下次有比较大的对象实例需要在堆上分配比较的的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。
- Copying(复制算法):没有碎片,浪费空间。
此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。Java堆中的新生代就使用了GC复制算法。
- Mark-Compact(标记整理):没有碎片,效率偏低
对于新生代,大部分对象都不会存活,所以在新生代中使用复制算法较为高效,而对于老年代来讲,大部分对象可能会继续存活下去,如果此时还是利用复制算法,效率则会降低。标记-压缩算法首先还是“标记”,标记过后,将不用回收的内存对象压缩到内存一端,此时即可直接清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。老年代的垃圾回收称为“Major GC”。
3. 内存模型和垃圾收集
堆内存的分代模型和分区模型:
在jdk8及之前都是分代模型,8之后都是分区模型。
内存模型:
堆内存:
YOUNG GC过程:
- 刚生成的对象放到年轻代的Eden区。
- Eden区中的对象快满的时候,触发GC,由执行引擎开启GC垃圾收集线程来对Eden去区中的可回收对象进行回收。
- Eden区中的存活对象利用复制算法,把对象转移到S0区,此时对象年龄加1
- 当Eden区中的对象再一次快满的时候,再次触发GC,这时,垃圾收集线程对Eden区和S0区中的可回收对象进行收集,存活下来的对象被转移到S1区中(包含Eden区和S0区中的存活对象)
- 就这样,对象在S0和S1区中来回转移。每发生一次垃圾回收仍然存活下来的对象转移后,对象的年龄就加1
- 当对象年龄达到15的时候,就被转移到老年代中。
以上发生在年轻代的GC 称之为YOUNG GC,无论是YOUNG GC还是FULL GC,都会发生STW(停止用户线程),但是YOUNG GC的时间特别短,可以忽略不计。
FULL GC过程:
当老年代中的对象快满的时候,就会触发FULL GC,垃圾收集线程会对年轻代和老年代中的所有可回收对象进行收集,这个时候的STW时间是较长的。老年代的垃圾回收是用的标记整理算法。
4. 垃圾收集器
串行收集器:
jdk8之前用的是Serial和Serial Old 串行收集器分别用在年轻代和老年代中。
年轻代的Serial和老年代的Serial Old垃圾收集器都是串行的,只会开启一个垃圾收集线程,耗时比较长。
并行收集器:
jdk8中默认的垃圾收集器就是PS和PO
Parallel Scavenge和Parallel Old ,简称PS 和 PO
CMS(Concurrent Mark Sweep)垃圾收集器:(注意他是用的是标记清除不是并发标记整理)
年轻代中使用ParNew(类似PS收集器,是从PS改良过来的),老年代中使用CMS
- CMS(Concurrent Mark Sweep)收集器,以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。
- CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤:
- 初始标记(CMS initial mark)。
- 并发标记(CMS concurrent mark)。
- 重新标记(CMS remark)。
- 并发清除(CMS concurrent sweep)。
- 其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
- 初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
- 并发标记阶段【也就说明不会阻碍业务线程继续执行,因为它所以还会有下面要说的“重新标记”阶段了】就是进行GC Roots Tracing【啥意思?其实就是从GC Roots开始找到它能引用的所有其它对象】的过程。
- 重新标记阶段则是为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- CMS收集器,在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:
并发收集、低停顿【注意:这里的停顿指的是停止用户线程】,Oracle公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause Collector)。
缺点:
- CMS收集器对CPU资源非常敏感。
- CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。如果在应用中老年代增长不是太快,可能适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要时,虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
Floating Garbage,就是指在之前判断该对象不是垃圾,由于用户线程同时也是在运行过程中的,所以会导致判断不准确的, 可能在判断完成之后在清除之前这个对像已经变成了垃圾对象,所以有可能本该此垃圾被回收但是没有被回收,只能等待下一次GC再将该对象回收,所以这种对像就是浮动垃圾
- 收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前进行一次Full GC。CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。