本文对JVM垃圾收集进行说明,包括三种不同算法(标记复制、标记清除、标记整理),以及七种不同的垃圾收集器(Serial,ParNew,Serial Scavenge, CMS, Serial Old, Parallel Old, G1)
持续更新中… …
说到垃圾收集,首先得确定哪些是可回收的对象,这里涉及到java的四种引用方式,即强、软、弱、虚四类引用。
前文中已经对运行时数据区域进行了简单的说明,这里要说的是垃圾收集主要涉及的区域,也就是堆区。堆区之所以还要细分为新生代和老年代,是为了垃圾回收的方便,把一些不经常回收或者体积比较大,转移起来比较麻烦的对象放在老年代,把一些经常会被回收的对象放到新生代,然后对不同的区域采用不同的垃圾回收器,更能提高垃圾回收的效率。
通常情况下,判断一个对象实例是否应该被回收主要使用的是可达性分析算法,即通过GC Roots的引用关系进行遍历,能否到达该对象实例。其中,能够作为GC Roots的对象主要由以下:
垃圾回收算法主要分为三种,即标记清除、标记复制、标记整理。
垃圾收集器目前来看主要有7种(Serial,ParNew,Serial Scavenge, CMS, Serial Old, Parallel Old, G1),从新老生代的划分来看,如图所示:
位于上方Young Generation部分的为新生代收集器,位于下方Tenured Generation部分的为老年代收集器,其中连线关联的收集器代表这两个可以一起协同使用。
串行收集器,需要停止所有其他线程开始收集垃圾,对新生代的对象采用标记复制的收集方式,即将Eden区和From Survivor区的存活对象复制到To Survivor区,如果满了就朝来年代复制。
串行收集器,和Serial类似,不过是对老年代进行收集,对老年代的收集方式为标记整理。
新生代的并行收集器,采用标记复制算法,实际上是Serial的多线程版本,在垃圾回收阶段启用多线程进行收集。
新生代的并行收集器,和ParNew类似,也采用了标记复制算法,不过它的关注点在于使CPU运行用户代码时间和总的消耗时间的比值也就是吞吐量达到一个可控的量,既可以控制吞吐量为百分之几,然后让jvm自行确定各类GC以及VM参数
CMS全称为Concurrent Mark Sweep收集器,专注于最短停顿时间,即给用户最好的体验。这个收集器总体而言是采用的标记清除算法,总共分为四个步骤:
从CMS的步骤可以看出,它优点就是减少用户的停顿时间,能够达到好的用户体验度,缺点就是无法处理浮动垃圾,浮动垃圾指在并发清除阶段因为用户线程还在运行所产生的垃圾。因为垃圾收集与用户线程并发执行,所以需要预留一些空间给用户线程使用,不能像其他收集器一样等到内存快满的时候才开始使用,预留的大小通过配置实现,如果预留的空间太小,会造成concurrent mode failure错误,这个时候就会启用serial old收集器重新进行垃圾收集。
除此之外,CMS还有个缺点就是采用的是垃圾清除算法,所以会产生不少内存碎片,可以通过设置让内存整理过程进行到一定次数后进行一次碎片整合,整合过程是不能与用户线程并发的,所以阈值设定也需要根据情况设置。
即采用并行线程对老年代进行垃圾回收,采用的标记整理算法。
G1收集器与其他收集器有个显著的不同就是,垃圾回收不再是整个新生代或者老年代,它将Java堆内存布局划分成了多个大小相等的Region。G1会跟踪每个Region的垃圾价值,即对这个Region进行回收所获得的空间大小和时间的大小的比值,然后维护一个优先队列,每次根据允许的收集时间,回收一个价值最大的Region。
为了避免各个Region之间互相引用所造成的垃圾回收方面的麻烦,G1的每个Region会有一个Remembered Set,用于存储其他Region对这个Region的对象的引用。内存回收时,枚举GC Roots时也将这一部分加入即可。
G1收集器的步骤如下:
- 初始标记:标记GC Roots关联对象,修改Next Top Mark Start的值,使得用户线程在后续并发执行时能在正确的Region中创建对象。
- 并发标记:和CMS类似,从GC Roots开始进行可达性分析。
- 最终标记:用户线程并发执行过程中会生成Remembered Set Logs,记录对象的变化记录,在最终标记阶段需要把这个记录合并到Remembered Set中。
- 筛选回收:对各个Region的回收价值与成本进行评估,按照用户所希望停顿的GC时间制定回收计划。
[1]《深入理解Java虚拟机》第二版,周志明著
[2]GC其他:引用标记-清除、复制、标记-整理的说明