首页 > 代码库 > 简单谈谈JVM中的GC(中)
简单谈谈JVM中的GC(中)
书接上文,在了解JVM的分代模型后,接着来简单聊聊JVM中GC算法和不同的GC收集器【求关注】
GC回收算法
一个GC回收算法通常会做这么几件事:
1、遍历内存,找到被引用的对象
2、清理掉这些未被标记对象的内存
3、被清理掉的内存放回内存中,供其他地方使用
上文也提及过,目前JVM中的搜索引用对象是用的根搜索方式,再重复引用下:
所有的Java对象构成一颗近似“搜索树”的结构,有一个root根节点,每次从root出发向下搜索,当整个树遍历完成后,那些不在其中的变量则视为"垃圾"。
但,不同的年代,其清理未引用对象内存的方式是不一样的。
复制算法(young代GC算法)
该算法会将内存区域分为两个大小一样的区域。GC回收时,遍历当前使用区域,只将正在引用的对象复制到另一个区域,因此复制成本较低,且复制过程中还会进行内存整理,不会出现“碎片”问题。缺点就是:需要两个大小一样的内存区域和生命周期短的对象。所以该算法不适合大内存对象和长生命周期的对象,适用于young代的SO/S1
标记-清除算法
该算法分为两个阶段,标记:找到所有正被引用的对象,清除:清除没有被标记的对象;该算法很简单,同时也有很多弊端:标记时会挂起整个Java应用、单独清除每个内存,会造成内存碎片
标记-整理算法
该算法与上个算法类似,分为两个阶段,标记:找到所有正被引用的对象,整理:将所有的标记的对象按照内存地址依序整理放到一起,清理掉末端内存地址之后的内存;该算法的效率虽比不上复制算法,但它不需要额外一样大小的内存区域,同时也不用担心长生命周期的对象来回拷贝的繁琐。
众所周知,JVM通过分代模型来区分对待不一样的对象,所以在不同的分代中,其GC算法是不一样的。在young代中,JVM采用的是复制算法,而在old代和pem代中,JVM采用的标记-整理/标记-清除算法。
GC收集器
GC收集器是GC的具体实现,目前每个厂商的GC收集器不一样,下图是HotSpot 1.6版本使用的GC收集器如下图(上面是young代可用,下面是old代可用,连线表示可配套用,图来自网上)
-
Serial(串行GC)收集器
新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停(使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收这也是虚拟机在Client模式下运行的默认值)
-
ParNew(并行GC)收集器
新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间(使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数)
-
Parallel Scavenge(并行回收GC)收集器
新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(说白了就是其核心关注点是吞吐量,所以适合吞吐量大且不关注用户体验的,关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)
-
Serial Old(串行GC)收集器
老年代收集器,单线程收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存 的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用
-
Parallel Old(并行GC)收集器
老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集
-
CMS(并发GC)收集器
老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程。
整个过程分为四步:
-
初始标记(CMS initial mark)
-
并发标记(CMS concurrenr mark)
-
重新标记(CMS remark)
-
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS,当用户线程内存不足时,采用备用方案Serial Old收集
但CMS收集器也有一些缺点:
CMS收集器对CPU资源非常敏感(会消耗额外的CPU和内存)。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,
即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。
在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
G1是jdk 1.7才出来的收集器,目前笔者这边也未有应用案例,后续在做相关分享
接下来,会简单讲解下JVM的参数配置
简单谈谈JVM中的GC(中)