JVM(六)垃圾回收机制---垃圾回收算法和垃圾分类器种类

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式
  • Young GC:只收集young gen的GC
  • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
  • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:

  • young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
  • full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

垃圾收集算法 

  • 标记清除算法

在上一篇讲过了两次标记的算法,那么标记清除的算法,就是当GC把一个对象两次标记,那么下一次gc就会直接清除这些对象。但是标记和清除是分步的,首先比标记处所有要回收的对象,再统一回收好处就是效率快,两次过程效率都很慢,也会造成内存不连续,那么以后如果堆要被很大的对象分配内存可能会溢出

  • 标记整理

标记清除算法的改进,在标记的过程是一样的,只是后面处理不一样,gc会把当前存活的对象移到一边,然后从边界端开始清除被两次标记过的对象

  • 复制算法

当前商业虚拟机都采用这种方式收集新生代。

1:1:就是每次把可用的内存分成两块,当其中一块不够用时,就把这块中活着的对象复制到另一块中,然后清除另一块剩下的对象。好处效率快,执行简单,但是要牺牲掉一半内存为代价

1:1:8:为了解决上面的问题,复制算法:我们都知道新生代对象一般都是朝生夕亡,所以不用为他分配那么大的内存,而是把内存划分较大的一块,和较小的两块survivor,每次分配只是用一块survivor和较大的那块,然后要gc时,就把活着的对象移到survivor2中,清理比较大的那块内存和survivor1刚才使用的空间。相当与新生代对象浪费10%的资源,但是万一新生代对象比较大,就需要老年代内存空间来给担保,碰到放不下的大新生对象,直接存入老年代,这种方法适合新生代对象,因为在向survivor2复制的时候,对象比较少

  • 分代收集算法

目前jdk内部的gc一般采用分代收集算法,它没有创新点,只是针对不同代的对象采用不同的垃圾收集算法,新生代复制算法,老年代标记整理算法。

垃圾回收器 

垃圾回收器就是垃圾回收的具体算法。没有最好的垃圾回收器,组合使用。

不同版本的虚拟机提供的垃圾回收器都不一样

新生代垃圾回收器

  • serial收集器

jdk1.3之前是唯一用来收集新生代对象的垃圾回收器,采用复制算法,是单线程的垃圾回收器。这里的单线程有两个意义:一个就是该垃圾回收器只有一条线程来回收垃圾。还有一层意义是在它工作的时候,所有其他正常工作的线程都必须停止等待(stop the world),所以等待时间长短是它的限制。当然在用户不可见的时候,停下你的线程 ,当然也可以理解,你妈帮你打扫卫生,你还一边丢垃圾就是不行的

虽然这个垃圾回收器老,也不知道多CPU,但是限定单个CPU的环境,在用户机器上,一般不会给虚拟机分配很大的内存, ,收集的新生代也很少,那么线程等待的时间不会很长,可以接受。所以在client模式下的虚拟机,serial收集器是不错的选择

 是serial收集器的多线程版本。过程中采用的收集算法是一样的。只是它支持多线程来执行收集算法。所以是server模式下的虚拟机中垃圾回收器不错的选择,目前只有它和serial能与CMS收集器配合使用。单CPU性能不如serial收集器,多CPU下,默认启动跟Cpu数量一样的线程数,也可以用XX:ParallelGCThreads来设置启动的线程数

  • parnew收集器

  • parallel Scavenge收集器

又叫吞吐量优先的gc,目的是最大化cpu的吞吐量,不太关注gc停顿时间,适合在后台不需要太多的交互。

cpu吞吐量=(运行用户代码的时间)/(运行用户代码的时间+运行垃圾回收的时间)

提供两个参数,-XX:MaxGCPauseMillis参数,设置gc最大停顿时间

-XX:GCTimeRatio:直接设置最大吞吐量

这两个参数是相互影响的,不能说把-XX:MaxGCPauseMillis设置的小一些,就能使垃圾回收速度变坏,这个时间缩短是以牺牲吞吐量和新生代空间来换去的。那么每次执行gc的时间就太短,一定量的垃圾要分好几次才能执行完,一定时间内的吞吐量就下降了

GC自适应的调节策略。最重要的参数,自动调优参数-XX:UserAdaptiveSizePolicy,默认打开,就不用程序员指定其他参数,或者通过你自己更关注最大停顿时间还是吞吐量来设置虚拟机的优化目标。

老年代垃圾回收器

  • serial old收集器

采用标记,整理算法回收老年代对象

作用:(1)jdk1.5之前与parallel Scavenge收集器搭配使用,但是由于单线程的限制使得parallel Scavenge收集器体现不到价值

 (2)作为CMS收集器的备选,当CMS出现concurrent mode faillure错误时,虚拟机会暂时调用serial old收集器

  • parallel old收集器

采用标记,整理算法回收老年代对象

作用:(1)jdk1.6之后与parallel Scavenge收集器搭配使用,使得parallel Scavenge收集器体现价值

在注重吞吐量及cpu资源敏感可以选择parallel Scavenge+parallel old收集器

  • CMS收集器 

采用改进的标记清除算法收集老年代对象,目标是缩短gc停顿时间。一定意义上的并发回收

收集步骤:

(1)初始标记:需要暂停掉其他线程,然后仅仅标记GC roots能直接关联到的对象,运行时间短

(2)并发标记:回收线程与用户线程并发执行,进行gc roots tracing,运行时间较长

(3)重新标记:需要暂停掉其他线程,重新标记用户线程在并发标记阶段引用改变的对象,运行时间也比较短

(4)并发清除:并发收集被标记的对象,运行时间较长

可以看到在最耗时的两个阶段,回收线程都与用户线程并发执行,所以总体上可以说CMS收集器是和用户线程并发执行的

问题

(1)清理不到漂浮的对象,就是在并发清除阶段,用户线程还在运行就会产生新的垃圾,所以CMS在收集老年代对象时,不能等到老年代对象的内存满了才开始回收,得留一部分空间给给运行的用户线程,CMS提供一个参数-XX:CMSInittiatingOccupancyFaction来决定当老年代使用了百分之多少内存就触发CMS回收器。在jdk1.6,这个参数默认是92%。是一个比较高的数,那么当到达这个容量,收集开始,发现剩余容量不够并发的线程使用,就会出现concurrent mode faillure错误时,虚拟机会暂时调用serial old收集器。这样停顿时间就很长了。

(2)并发阶段还是会占用cpu资源,导致引用程序变慢,总吞吐量降低,CMS默认启动的回收线程数是(cpu数+3)/4

当cpu数大于4时,回收线程占cpu资源的25%以内,可以接受,但是当cpu数小于4,比如两个cpu的情况,回收线程占用50%的cpu资源的,你的程序速度减半,也会很明显的。

(3)使用标记清除算法,造成空间不连续问题。所以CMS还有两个参数-XX:CMSCompactAtFullCollection,就是内存不够分配必须进行full GC(新生代和老年代都回收,System.gc()触发full gc)之前 CMS开始进行整理压缩内存空间,这个参数默认是开的。

第二个参数:-XX:CMSFullGCsBeforeCompaction,就是设置CMS执行多少次full GC不压缩后,来一次压缩整理的。

全能垃圾收集器G1(Garbage first)

最先进的回收器,可以收集老年代新生代,是面向服务器的垃圾回收器。

结构特点

在G1里,虽然继续有新生代和老年代的概念,但他们不在物理上隔离,不是分开存储的,G1收集器,把内存划分为等大的一些regins。

G1优点:

并发与并行:目的也是减少GC停顿时间

分代收集:采用分代收集算法,不需要配合其他收集器

空间整合:整体来看是标记整理算法,运行完不会有空间碎片(后面具体解释),优于CMS

可预测的停顿:优于CMS.能指定在M毫秒的时间段里,垃圾回收停顿最大不超过的毫秒数。

G1是regin层面上的操作,它会评估每个regin包含垃圾的回收价值,即把这些垃圾回收了能释放多少内存,以及回收所需要的时间,那么根据回收价值,G1维护一个优先队列,先收集回收价值高的regin,这样能提高整体效率。

所有回收器都有的问题(新老年代对象互相引用)

在JVM中回收垃圾的思想都是分代收集,只是G1是自己一个收集器完成,其他是需要配合完成,

前面我们说到当进行枚举根节点时使用oopmap记录一些引用位置,这个概念是在新生代收集器或者老年代收集器中进行的

忽略的问题就是,新老年代之间可能存在互相引用,那么不论是新生代收集器还是老年代收集器在做可达性分析,枚举根节点时,还不是要扫描整个堆?当然不会,虚拟机这个时候使用Remembered Set来记录新老年代之间的相互引用,当发生引用关系变化时或者加入新的引用关系时,先判断是不是不同代之间的引用,是就把这个引用记录到Remembered Set,这个Remembered Set是整个堆共享的,那么不同年代的回收器在gc回收,进行各节点枚举时只要把Remembered Set加入,就能保证不扫描全队又不会有遗漏。

所以在回收算法,可达性分析时用到的数据结构有Remembered Set和oopmap

oopmap记录的老年代或者新生代内部的引用,Remembered Set记录新老年代之间的相互引用。

G1回收步骤

(1)初始标记

(2)并发标记

(3)最终标记

(4)筛选回收

1,2,3阶段与CMS相同,4筛选回收就是根据各个regin的回收价值以及用户希望的gc停顿时间来制定回收计划

猜你喜欢

转载自blog.csdn.net/wangdongli_1993/article/details/81177928
今日推荐