vince's blog JAVA学习总结

当处在低谷时一定要清醒,要忍耐,要淡定。这是生活给你一个难得的自省机会,利用这些时间多学习、暗地里提高自己,为即将到来的高峰做准备!...

深入理解java虚拟机(二)---GC标记清除算法与垃圾回收器总结

 接上一篇的java内存模型,这一篇记录一下GC垃圾回收的算法,说道垃圾回收,首先说说什么情况下会被回收。

一 垃圾回收算法

1.可达性分析算法

一般认为GC回收采用可达性分析算法::从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。

 

在java中可以作为GC Roots的对象有以下几种:

虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈JNI引用的对象

虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象 不一定会被回收。当一个对象不可达GC Roots时,这个对象并不会马上被回收,而是处于一个死缓的阶段,若要被真正的回收需要经历两次标记。如果对象在可达性分析中没有与GC Roots的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已经被虚拟机调用过,那么就认为是没必要的。

2.标记/清除算法

 标记清除算法是垃圾处理器最初的清理算法,也是最基础的清理算法,了解了这个后边两个基本就清楚了。

标记,清除算法,字面意思就是先标记,再清除

标记阶段:标记所有GC roots不可达的对象,进行标记

清除阶段:对上面标记过的对象进行清除回收

回收前状态:

回收后状态:

缺点:

    1.回收时需要先停止虚拟机的工作,就是stop the world.

    2.标记清除要遍历整个堆,效率低

  3.清除后会产生大量碎片区域,无法为大对象分配连续的内存


3 复制算法

复制算法是在标记清除的基础上改进的,把内存区域平分为两个部分,每次新建对象只在一个区域内操作,

分为两个阶段

1.复制阶段:当堆内存不足时,先停掉jvm工作,把GCRoots可达的对象(即不可回收的对象)复制到另一半内存空间,在新的区域按顺序排列,重新分配对象的引用。

2.清楚阶段:原来的内存区域都是需要回收的对象,对整个区域进行一次full gc


优点:不会产生大量的碎片空间

缺点:内存使用率只有一半,确实挺浪费的,垃圾回收时同样要 stop the world,如果老年对象多,则每次要复制大量对象。

改进:

现代的商用虚拟机一般都采用这种方法回收新生代,IBM的研究表明,新生代有98%的内存都是需要回收的,大部分都是朝生夕死。内存不必1:1分配,将内存划分为一块较大的Eden空间和两块较小的Survivor空间每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间,最后清理掉Eden和刚才用过的Survivor空间,清理完成后,刚刚被清理的Eden和另一块在回收时放入存活对象的Survivor空间作为使用内存,刚被清理的Survivor作为保留空间,以便后面用来回收之用。

这种改进的收集算法也有一个问题,就是在回收时,那块空的Survivor空间能否放得下Eden和使用的Survivor空间中还存活的对象,如果Survivor空间不够存放上一次新生代收集下来的存活对象,此时就需要向老年代“借”内存,那些剩余未放下的对象就通过分配担保机制进入老年代。
复制算法的执行过程如下:
回收前的状态:

回收后的状态:



4.标记整理算法

复制算法如果在对象存活率较高时,就需要进行较多次的复制操作,效率也会变低。而对于老年代中的对象,一般存活率都较高,因此需要选用其他收集算法:标记 - 整理算法。标记过程仍然与“标记-清除”算法中一样,但是在标记完成后并不直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。算法示意图如下:
回收前状态;

回收后状态:

5.分代收集算法

当前商业虚拟机都采用这个“分代收集”算法(Generation Collection),它根据对象存活周期的不同将内存划分为几块,一般是把java堆分为新生代和老年代,根据各个年代的特点选用不同的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,因此可以选用“复制算法”,此时只需要付出少量存活对象的复制成本即可;对于老年代,因为对象存活率较高、也没有额外空间为期分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。


二 垃圾收集器总结

这部分摘自网络

如果说上面介绍的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,按照上面的介绍,目前垃圾收集器基本都采用分代收集,因此一个垃圾收集器中一般都存在多种垃圾回收算法。不同的虚拟机提供的垃圾收集器也有很大差异,如下是HotSpot虚拟机基于JDK1.7版本所包含的所有垃圾收集器:

HotSpot中共有7中不同的垃圾收集器,如果两个收集器之间存在连线,说明它们之间可以搭配使用,其中,Serial、ParNew、Parallel Scavenge属于新生代收集器,CMS、Serial Old、Parallel Old属于老年代收集器,G1是最新的一种收集器,在新生代和老年代中都可使用。

3.1 Serial(串行)收集器

最基本、发展历史最悠久的一种收集器。看名字就知道,这个收集器是一个单线程的收集器,只使用一个CPU或一条收集线程去完成垃圾收集工作,最重要的是,在它进行垃圾收集的时候,必须暂停其他所有的工作线程,知道它收集结束。虽然有这个缺点,但是依然是虚拟机运行在Client模式下的默认新生代收集器。优点是:简单而高效,没有线程交互的开销。运行过程如图:

新生代采用的是“复制算法”,老年代采用的是“标记-整理”算法。

3.2 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其他行为和Serial收集器一样。ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因,除了Serial收集器外,目前只有ParNew能与老年代的CMS收集器配合使用。ParNew是一种并行的收集器。在垃圾回收中,并行是指:多条垃圾收集线程并行工作,用户线程处于等待状态;并发是指:用户线程和垃圾收集线程同时执行(不一定并行,可能交替执行)。

3.3 Parallel Scavenge收集器

Parallel Scavenge收集器使用的是复制算法,也是一个并行的多线程收集器。和ParNew相似,但是Parallel Scavenge的关注点不同,CMS收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。

上面三种都是新生代收集器,下面介绍老年代收集器。

3.4 Serial Old收集器

Serial Old收集器是新生代Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法,Serial Old的主要意义也是在于给Client模式下的虚拟机使用。

3.5 Parallel Old收集器

Parallel Old是新生代收集器Prarllel Scavenge的老年代版本,使用多线程和“标记-整理”算法。运行流程如下:


3.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。对于互联网站或者B/S系统的这种注重响应速度的服务端来说,CMS是很好的选择。从名字Mark Sweep可以看出,CMS是基于“标记-清除”算法实现的,分为四个步骤:
(1)初始标记(CMS initial mark):仅仅标记一GC Roots能直接关联到的对象,这个步骤需要“stop the world”;
(2)并发标记(CMS concurrent mark):就是GC Roots进行可达性分析阶段,可并发执行;
(3)重新标记(CMS remark):修正并发标记期间发生变动的那一部分对象,这个步骤需要“stop the world”;
(4)并发清除(CMS concurrent sweep):执行清除阶段。
执行过程如下:

可以看到,初始标记和重新标记阶段都是并行的,需要暂停用户线程(过程比较短);在并发标记和并发清除阶段是并发的,可以和用户线程一起工作。

CMS的优点:并发收集、低停顿
CMS的缺点:
(1)对CPU资源非常敏感,面向并发设计程序的通病,虽然不至于导致用户线程停顿,但是会降低吞吐率;
(2)无法清理“浮动垃圾”,由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断出现,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次的GC;
(3)会产生大量空间碎片,因为CMS是基于“标记-清除”算法,这种算法的最大缺点就是会产生大量空间碎片,给分配大对象带来麻烦,不得不提前触发Full GC。为了解决这个问题,CMS提供了一个“-XX:+UseCMSCompaceAtFullCollection”的开关参数(默认开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程

3.7 G1收集器

G1收集器是最新的一款收集器,JDK1.7才发布,是一种面向服务端应用的垃圾收集器,有如下特点:
(1)并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间;
(2)分代收集:分代概念在G1中依然得以保留。虽然G1可以不需其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果;
(3)空间整合:与CMS的“标记-清理”算法不同,G1从整体看来是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上看是基于“复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存;
(4)可预测的停顿时间;

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1的收集过程分为以下几个步骤:
(1)初始标记(Initial Marking)
(2)并发标记(Concurrent Marking)
(3)最终标记(Final Marking)
(4)筛选回收(Live Data Counting and Evacuation)
前几个步骤和CMS有很多相似之处。运行示意图如下:


(以上图片来源于:


阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hewenya12/article/details/80688169
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭