目录
根据虚拟机规范,Java虚拟机管理的运行时数据区域可以分为两大类:第一类是线程私有,依赖用户线程的启动和结束而建立和销毁;第二部分是线程共享区域不依赖用户线程。
通过上面的介绍可以知道线程私有的数据区是不需要回收的,它是随着用户现场的启动而创建,随着用户线程的结束而销毁。垃圾回收主要回收的是线程共享数据区
在现实生活中什么是垃圾?垃圾就是我们已经不被使用的东西。在JVM中也同样是这个道理,那么哪些是垃圾呢?就是这个对象不被使用。
从GC Roots开始,根据引用关系向下搜索,如果某个对象可以被搜索到则称之为GC Roots到这个对象可达,如果某个对象从GC Roots到这个对象不可达时,说明这个对象不再被使用,可以被回收。
三色标记主要用在并发的可达性分析场景,将遍历对象图按照以下规则标记为三种颜色
白色表示对象尚未被垃圾收集器访问过。显然在可达性分析开始阶段所有对象都是白色,如果可达性分析结束对象还是白色,就表示这个对象从GC ROOT不可达,即改对象属于垃圾需要被回收
黑色表示对象已经被垃圾回收器访问过且这个对象所引用的对象也已经被访问过。它表示从GC Root对象可达,它是存活的对象。
表示这个对象已经被垃圾回收器访问过,但是这个对象至少有一个引用还没有被扫描过
标记清除算法分为标记和清除两个阶段,第一阶段,标记出所有需要回收的对象,第二阶段,在标记完成后,回收掉所有被标记的对象。该算法存在的主要问题是空间碎片化,标记清除后产生大量不连续的内存碎片。
复制算法是为了解决标记清除算法内存碎片化问题,它将内存分为两个相等的空间,每次只使用其中一个空间,在回收过程中将存活的对象复制到另外一个空间,复制完成中针对当前空间整体清除。主要存在的问题是浪费空间。
标记整理算法是针对标记清除算法的优化,解决了标记清除算法内存空间碎片化问题;第一阶段是标记所有存活的对象,第二家阶段是将存活的对象想内存空间的一端移动,第三阶段完成存所有存活对象移动后,清除边界以外的空间。
新生代垃圾回收器:
老年代垃圾回收器:
Serial,翻译成中文的意思是“串行”,顾名思义,这就是个单线程的收集器。仅仅使用一个线程去执行垃圾收集任务,而且收集任务期间,必须停掉其他的工作线程,直到垃圾收集完成。垃圾回收时停掉其他的线程的现象,就称为“Stop The World(STW)”。打个比方,我清扫房间的时候,任何人都不能在家里活动,以免给我捣乱,不然清扫工作怎么也没法做完。STW就是这么个意思,至于暂停应用多久,得看具体垃圾的情况了。
Serial收集器是收集新生代的收集器,而Serial Old收集器是收集老年代的,上图也看到了它们之间有连线可搭配使用,看如下它们搭配使用的运行图:
①:新生代使用Serial收集器,采用复制算法,会暂停其他用户线程(STW)专心做垃圾回收。
②:老年代使用Serial Old收集器,采用标记整理算法,会发生STW。
ParNew其实就是Serial的多线程版本,在新生代中使用多条线程进行垃圾回收。看如下逻辑图就一目了然了:
①:新生代使用ParNew收集器,可以看到有多条GC线程在进行垃圾回收,采用复制算法,会暂停其他用户线程(STW)专心做垃圾回收。
②:老年代使用Serial Old收集器,采用标记整理算法,会发生STW。 Parallel Scavenge 收集器
看上去和ParNew收集器差不多,但是Parallel Scavenge最大的特点是更关注吞吐量。
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值: 吞吐量 = 运行用户代码时间 / (运行用户代码时间) + 垃圾收集时间 打个比方,虚拟机运行了100分钟,垃圾回收用了2分钟,那么吞吐量就是98%。
按照公式来看,吞吐量越高的虚拟机,自然是垃圾收集时间也越短,理所当然的用户体验也要更好。Parallel Scavenge收集器会根据当前系统的运行情况,动态调整某些参数来提供最合适的停顿时间或最大的吞吐量,这就是GC的自适应调节策略,这也是其与ParNew收集器最明显的区别。
Parallel Old 是 Parallel Scavenge收集器的老年代版本,运用多线程和标记整理算法收集。从最上面的搭配图也可以看到,Parallel Old 只能与Parallel Scavenge配对使用。这样的组合,在注重吞吐量和CPU资源的场合使用比较合适。如下是逻辑运行图:
①:新生代使用Parallel Scavenge收集器,可以看到有多条GC线程在进行垃圾回收,采用复制算法,会暂停其他用户线程(STW)专心做垃圾回收。
②:老年代使用Parallel Old收集器,使用多线程采用标记整理算法,会发生STW。
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。在B/S架构模型的网站上,运用CMS收集器十分广泛,因为网站上更希望停顿越短越好,用户体验才能更好。
CMS收集器是基于标记清除算法实现的,但是其运行过程相对来说更复杂了,整个过程分成下图4个步骤:
①:初始标记(initial mark)
在图中可以看出这个步骤是单线程处理的,并且用户线程并未运行,是因为出现了STW。这个过程只是标记一下GC Roots能直接关联到的对象,速度很快。
②:并发标记(concurrent mark) 标记从初始化标记对象可达的存活对象。
③:重新标记(remark)
重新标记阶段是为了修正并发标记期间,因用户线程继续运行导致标记产生变动的那一部分对象的标记。看起来有点绕,其实意思就是在并发标记时,用户线程也会产生需要标记的对象,这部分对象不能漏了标记,所以就需要重新标记过程。在图中可以看到,没有用户线程在运行,说明需要STW。
④:并发清除(concurrent sweep)
并发清除这个阶段看图也能类比了,有GC线程与用户线程并发运行,GC线程清理掉那些标记的对象,用户线程正常运行。 第一,第三步的初始标记(Initial Mark)和重新标记(Remark)依然会引发STW 整体来看,CMS收集器的垃圾回收过程是与用户线程一起并发执行的。
但是CMS收集器还是有一下三个缺点:
G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量
G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区 。一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)
G1回收器大致可以分为以下四个步骤