一文读懂系列-JVM垃圾收集

之前的一篇文章介绍了JVM的内存区域划分和每个内存区域的功能,今天就介绍下JVM中的垃圾收集的相关算法和常用垃圾收集器。

哪些对象要收集

进行垃圾回收的前提是要先判断出JVM中哪些对象需要回收,那么我们先介绍下判断对象已经没有引用的常用算法。

1)引用计数法:这个算法很简单,就是给每个对象增加一个引用计数器,在引用它的地方计数器就加1,当引用失效就减1,当计数器为0的时候就可以进行回收了。但是这个算法的最大缺点就是无法处理循环引用的问题,会导致对象一直无法回收。

2)GC Roots 根搜索法: 这个算法的是通过定义为GC Roots的对象作为起点,从这些节点向下搜索,搜索走过的路径为引用链,当一个对象到GC Roots没有任何引用链时,则证明这些对象时不可用的,GC可以进行回收。

下列4种对象可作为GC Roots:

  • 虚拟机栈帧中的变量表中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • native方法栈中引用的对象

垃圾收集算法

JVM通过GC Roots进行失效对象查找,对于不在引用链上的对象会根据垃圾回收算法进行回收,JVM常用的垃圾回收主要有三种标记清除算法、标记整理算法、复制算法,对于JVM实际使用过程中使用的其实是按照堆中不同的内存区域特性(新生代、老年代)采用不同的算法进行回收,也就是常说的分代回收算法,下面分别介绍下这几个算法。

1)标记清除算法
标记清除算法是用的比较多的一种垃圾收集算法,原理也很简单,根据GC Roots自上而下的搜索到的对象引用链,在堆中进行查找,找出不在引用链上的对象,然后进行标记,图中用黄色标出,标记后由GC统一对这些标记内存区域进行数据清除。
标记清除算法虽然很简单,但是缺点也很明显,那就是内存中存在较多的碎片,后续再分配堆空间的时候无法有效分配连续的内存空间,导致一些大对象可能无法申请内存,总体内存利用率较低,优点是回收速度快。


一文读懂系列-JVM垃圾收集_第1张图片
Screenshot 2018-05-18 08.57.09.png

2)标记整理算法
标记整理算法是标记清除算法的改进版,和标记清除算法一样先进行根据引用链进行标记,再由GC对标记后的内存区域进行清除,下面就是和标记清除法不一样的地方了,标记整理算法为了提高连续内存的利用率会进行使用内存区域的整理,将使用的内存区域进行连续性压缩,类似PC上的磁盘整理,一方面可以提高内存的连续读性能,另一方面提高了内存利用率,缺点就是性能会略低于标记清除算法。

一文读懂系列-JVM垃圾收集_第2张图片
Screenshot 2018-05-18 08.57.20-w300

3)复制算法
复制算法是目前JVM新生代常用的算法,其实原理也很简单,将内存分配为两块同等大小的区域,分配内存的时候使用区域一,区域二空着,当要进行内存回收的时候,将区域一中已经使用的内存全部复制到区域二中,最后再将区域一中的内存全部清除,这样清理的速度是非常快的,GC回收性能是最好的,但是缺点是内存可使用空间少了一半。


一文读懂系列-JVM垃圾收集_第3张图片
Screenshot 2018-05-18 08.57.42-w300

因为内存空间可使用率低的问题,IBM和Sun都对垃圾回收的复制算法进行了改良。IBM和Sun在研究中发现新生代中的98%对象都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。

当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。

4)分代收集算法
介绍了上面三种常用的垃圾收集算法,其实在JVM中根据不同的内存区域,这几种垃圾回收算法都有用到,JVM聪明的按照每个内存区域的特性,选用了不同的垃圾收集算法。之前的一篇JVM内存区域的文章中介绍了堆中分为新生代和老年代,新生代中的对象存活时间非常短,但是对垃圾回收时间要求非常高,需要高性能回收,所以才用了复制算法,并将新生代分为了Eden、 From Survior、To survior三个区域;对于老年代因为数据存活时间较长,对收集性能没那么敏感,所以采用了标记清除和标记整理算法,这就是我们常说的JVM分代收集算法。

垃圾收集器

1)Serial收集器
Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

这个收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。收集器的运行过程如下图所示:


一文读懂系列-JVM垃圾收集_第4张图片
Screenshot 2018-05-18 09.58.20

JVM启动参数设置:-XX:+UseSerialGC
新生代、老年代都会使用串行回收
新生代复制算法
老年代标记-整理

2)ParNew收集器
ParNew收集器其实就是Serial收集器新生代的并行版本。


一文读懂系列-JVM垃圾收集_第5张图片
Screenshot 2018-05-18 09.59.45

JVM启动参数设置:-XX:+UseParNewGC
新生代并行
老年代串行

3)Parallel Scavenge收集器
类似ParNew,但更加关注吞吐量。
JVM启动参数设置:-XX:+UseParallelGC 新生代并行
老年代串行

4)Serial old收集器

5)Parallel Old收集器
Parallel Old收集器是Parallel Scanvenge收集器的老年代版本。
JVM启动参数设置:-XX:+UseParallelGC
新生代并行
老年代并行
[图片上传失败...(image-cb5b43-1526614128569)]

6)CMS收集器
上面介绍的所有收集器,当执行GC时,都会stop the world,但是下面的CMS收集器却不会这样。
CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。

JVM启动参数设置:-XX:+UseConcMarkSweepGC
老年态并行标记-清除算法

一文读懂系列-JVM垃圾收集_第6张图片
Screenshot 2018-05-18 10.07.09

整个过程中初始标记和重新标记时,需要stop the world,但是这两个标记时间都非常短,可以忽略不计。

  • 初始标记

GC Roots可以直接关联到的对象

速度快

  • 并发标记(和用户线程一起)

主要标记过程,标记全部对象

  • 重新标记

由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正

  • 并发清除(和用户线程一起)

基于标记结果,直接清理对象

CMS收集器看着很完美,但是也存在一些缺点:

  • 系统的吞吐量会下降,因为要拿出一部分cpu资源去进行垃圾收集
  • 清理不彻底,因为用户线程和GC收集线程是同步进行的虽然有两次标记的过程,但还是会产生新的垃圾

总结

本文分别介绍了垃圾识别算法、垃圾收集算法、常用的垃圾收集器,理解了这些内容后,当我们再遇到OOM时可以根据heap dump中纪录的GC情况进行分析,为什么GC没有成功收集到失效内存,从而找出内存泄漏的根因,这对于我们理解JVM和分析生产事故dump都有很大的帮助,同时这部分也是面试官经常问的问题,起码我是经常会问面试者这些问题来考察面试者对GC的理解深度。

附录:

JVM GC相关参数
-XX:+UseSerialGC:在新生代和老年代使用串行收集器

-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例

-XX:NewRatio:新生代和老年代的比

-XX:+UseParNewGC:在新生代使用并行收集器

-XX:+UseParallelGC :新生代使用并行回收收集器

-XX:+UseParallelOldGC:老年代使用并行回收收集器

-XX:ParallelGCThreads:设置用于垃圾回收的线程数

-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器

-XX:ParallelCMSThreads:设定CMS的线程数量

-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发

-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理

-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩

-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收

-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收

-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

你可能感兴趣的:(一文读懂系列-JVM垃圾收集)