GC
什么是GC
GC是java的内存回收机制。
为什么会有GC
在开发中控制对象的生命周期对于程序员来说是很复杂繁重的工作,不恰当的内存回收会导致程序的不稳定;
而java作为编程语言之初,把便捷的开发作为发展的方向,所以设计了GC帮我们完成了所有对象的清理工作。
GC相关知识
垃圾收集算法
标记-清除(Mark-Sweep):
标记和清除是两个阶段:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。
标记清除算法适合少量垃圾的回收中使用;
在垃圾较多的回收中效率很低;另外它会产生大量不连续的内存空间,这可能会导致在分配大对象的时候无法找到内存而触发GC。
复制算法(Copying):
复制算法会把可用内存分为A、B两部分,当A内存用完了,就把这一部分还存活的对象复制到B内存。
复制算法简单高效,适合大量垃圾的回收中使用;
缺陷是对内存的利用率不高,相当于只使用了一半的内存(在HotSpot中,是将内存分为一块较大的Eden空间和两块较小的Survivor空间,Eden与Survivor的比例是8:1,每次使用Eden和其中一块Survivor,回收时将Eden和Survivor中还存活着的对象一次性地复制到另外一块To Survivor空间上,再清理掉Eden和From Survivor空间);
另外在垃圾比较少、甚至没有垃圾的情况下,回收的性能很差:没有垃圾的极端情况下,复制算法不仅没有回收垃圾还要把A内存的所有对象(此时都是存活对象)复制到B内存。
标记-整理(Mark-Compact):
标记整理算法可以理解为标记-整理-清理的组合,它和标记清理算法一样,但后续步骤不是直接清理,而是多了整理的过程:让所有存活的对象都向一端移动,再清理另一端的回收对象。
标记整理算法和沿袭标记清理算法,所以也只适合回收对象较少的场景使用;
另外附加的整理过程让它解决了标记清理算法中碎片空间问题。
分代收集(Generational Collection):
分代收集算法被现在的虚拟机所采用,它把堆内存分为新生代、老年代两部分,然后根据新生代、老年代各自的特点使用合适的垃圾收集算法:
新生代的特点是存活率低,可回收的对象很多,所以当前虚拟机采用了复制算法;
老年代的特点是存活率高,可回收的对象很少,所以采用标记清理或标记整理算法。
垃圾收集器
垃圾收集器是垃圾收集算法的具体实现。
SerialNew(串行):
Serial是一个单线程的串行收集器,它是采用复制算法实现的新生代收集器;
进行垃圾收集的时候必须停止其他线程的工作;
不过Serial比起其他收集器的单线程更高效,适用于单核CPU且虚拟机内存较小的Clinet模式。
ParNew:
它是SerialNew收集器的多线程版本;
也是在收集的时候会停止其他线程的工作;
不过ParNew是唯一能与CMS收集器搭配;而且在多核CPU上性能优于Serial,适用于Server模式的新生代中。
Parallel Scavenge(并行清理):
Parallel Scavenge是一个多线程的并行收集器,它也是采用复制算法实现的的新生代收集器,
与parNew等收集器关注的减少用户线程的停顿时间不同,Parallel Scavenge是关注减少GC发生次数,提高吞吐量;
Parallel Scavenge减少了GC次数但让用户线程的停顿时间加长了,所以不适合交互频繁的场景;
不过它适用于后台计算这种提高吞吐量的场景;
另外自适应调节优化也是Parallel Scavenge的特色。
SerialOld:
SerialOld是相对于SerialNew的老年代收集器,使用标记-清理算法;
另外SerialOld的用途是作为CMS收集器的备用方案,在CMS并发收集发生Concurrent Mode Failure时使用。
ParallelOld:
Parallel Old是相对于Parallel Scavenge的老年代收集器,使用“标记-整理”算法。
与SerialOd不同的是,ParallelOld使用了多线程,比起SerialOld在Server模式下作为老年代收集器更具优势;
另外可以与ParallelScavenge组合成吞吐量优先收集器;
Concurrent Mark Sweep(并发标记清除):
CMS 是一个并发收集器,采用标记-清理算法实现的老年代收集器;
由于CMS是并发的,所以对CPU要求很高;
另外CMS无法处理浮动垃圾(GC阶段产生的新垃圾称为浮动垃圾),所以CMS提供了阈值设置老年代的预留空间,当达到了阈值便会出现“Concurrent Mode Failure”失败而导致另一次Full GC(FullGC可能伴随)的产生,不过此时会启用SerialOld备用方案;
还有CMS是基于标记清理算法实现,所以会产生碎片空间问题,CMS也提供了参数在GC之前来整理碎片空间;
不过CMS进行GC的时候不会停止用户线程的工作,所以交互过程的响应很快,非常适合BS架构的服务端;
GI:
GI是当今收集器技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器;
由于现在市场还没有大面积商业使用,暂时不做介绍。
总结:
GC收集器种类众多,和其他更具一样,没有万能的,只有理解各种GC收集器的特性,才能在不同场景根据实际环境选择;
GC的策略大体上分为响应速度优先和吞吐量优先的两种组合:
响应速度目前最成熟的老年代、新生代搭配是CMS + ParNew,因为CMS是基于并发实现,不会停止用户线程,所以响应很快,这种组合也是一种限制,因为CMS可搭配的只有ParNew;
吞吐量的组合则是ParallelOld + ParallelScavenge,因为ParallelScavenge目的是为了减少GC次数,从而提高吞吐量,ParallelOld由于使用了标记整理算法所以没有碎片空间这一问题,另外对于SerrialOld而言是多线程的,在多核CPU环境下性能更好。
哪些内存需要GC
在了解GC具体回收哪些对象的时候,要先弄清楚可达性算法。
可达性算法
可达性算法的思路是以一系列的GCRoots对象为起点,然后从这些节点往下搜索,当一个对象从GCRoots搜索不到,则证明这个对象是可以被回收的;
如下图的object5、6、7是可以回收的:
总结
GC回收的是超出作用域,在GCRoots搜索不到,且经过一次回收标记仍旧没有复活的对象。
GC什么时候进行回收
在JVM内存空间不足的时候、调用System.gc()的时候会触发;
另外可以通过调优,控制新生代进入老年代的年龄、设置Eden与Survivor的比例、设置老年代预留空间,来触发CMS进行FullGC(老年代的GC,另外MinorGC是新生代的GC,一般FullGC会伴随至少一次的MinorGC)。
GC是怎么回收的
GC会对新生代、老年代两部分进行回收动作:
如果是新生代内存空间不够,会通过复制算法执行MinorGC,即先把Eden还存活的对象复制到ToSurvivor,再清理掉Eden和FromSuvivor;
如果是老年代的内存空间不够则使用标记清理算法执行FullGC,首先对回收对象进行标记,标记过后仍未使用的对象清理掉,之后再整理碎片空间,一般FullGC会伴随至少一次的MinorGC。