一. JVM垃圾回收算法
1.引用计数器算法:
引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。
引用计数器实现简单,效率高;但是
(1)不能解决循环引用问问题
(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时
(2)每次计数器的增加和减少都带来了很多额外的开销
,所以在
JDK1.1
之后,这个算法已经不再使用了。
2.根搜索方法
根搜索方法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为
引用链
(Reference Chain),
当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的。
GC Roots对象包括:
a) 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b) 方法区域中的类静态属性引用的对象。
c) 方法区域中常量引用的对象。
d) 本地方法栈中JNI(Native方法)的引用的对象。
了解了JVM是怎么确定对象是“垃圾”之后,进入正题,让我们来看看垃圾回收的算法。
1.复制算法(Copying)
复制算法是
把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。
复制算法
实现简单
,
运行效率高
,但是由于每次只能使用其中的一半,造成内存的
利用率不高
。
现在的JVM用
复制方法收集
新生代
,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存默认大概是8:1。
垃圾回收前:
垃圾回收后:
2.标记—清除算法(Mark-Sweep)
标记—清除算法包括两个阶段:“标记”和“清除”。
在标记阶段,确定所有要回收的对象,并做标记。
清除阶段,将标记阶段确定不可用的对象清除。
标记—清除算法是基础的收集算法,标记和清除阶段的
效率不高
,而且清除后回
产生大量的不连续空间
,
这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。
垃圾回收前:
垃圾回收后:
3.标记—整理算法(Mark-Compact)
标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。
标记—整理算法
提高了内存的利用率
,并且它适合在收集对象存活时间较长的
老年代
。
垃圾回收前:
垃圾回收后:
4.分代收集(Generational Collection)
分代收集是根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。
新生代
采用
复制算法,老年代
采用
标记—整理算法
(个人感觉也不一定,如果老年代采用
CMS收集器
,算法就是
标记-清除
算法)
。
垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。上面介绍的只不过是基本思想。
二.
JVM垃圾收集器
垃圾收集器就是收集算法的具体实现,不同的虚拟机会提供不同的垃圾收集器。并且提供参数供用户根据自己的应用特点和要求组合各个年代所使用的收集器。
本文讨论的收集器
基于Sun Hotspot虚拟机
1.6版。 下图中展示了jdk1.6中提供的6种作用于不同年代的收集器,两个收集器之间存在连线的话就说明它们可以搭配使用。没有最好的收集器,也没有万能的收集器,只有最合适的收集器。
从Serial收集器到Parallel收集器,再到CMS收集器, G1收集器,
用户线程的停顿时间在不断缩短,但是仍然没有办法完全消除。

1. Serial收集器
单线程收集器,使用
复制收集算法,
收集时会暂停所有工作线程(我们将这件事情称之为Stop The World),直到收集结束,虚拟机运行在Client模式时的默认新生代收集器。
收集过程:
暂停所有线程
算法:
复制算法
优点:
简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器没有现成交互的开销。
应用:
Client模式下的默认新生代收集器
场景:
在堆比较小的情况下,一般停顿时间很短,是可以使用这种收集器的。
新生代
Serial
与年老代
Serial Old
搭配垃圾收集过程图:
2. ParNew收集器
ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。
ParNew收集器是许多运行在
server模式下的虚拟机中首选的新生代收集器(一个原因是在除了serial收集器外,目前只有它能与CMS收集器配合使用)。
ParNew收集器是使用-XX:+UseConcMarkSweepGC选项的默认新生代收集器;也可以用-XX:+UseParNewGC选项来强制指定它。
ParNew收集器在单CPU环境中不比Serial效果好,甚至可能更差,两个CPU也不一定跑的过,但随着CPU数量的增加,性能会逐步增加。
默认开启的收集线程数与CPU数量相同。在CPU数量很多的情况下,可以使用
-XX:ParallelGCThreads参数来限制线程数。
收集过程:
与用户线程并发
算法:
复制算法
优点:在CPU多的情况下,拥有比Serial更好的效果。单CPU环境下Serial效果更好
应用:
许多运行在
Server模式下的虚拟机中首选的新生代收集器
3. Parallel Scavenge收集器
同ParNew一样是使用复制算法的新生代并行多线程收集器。
Parallel Scavenge的特点是它的关注点与其他收集器不同,
CMS等收集器
的关注点
尽可能地缩短垃圾收集时用户线程的停顿时间
,
而Parallel Scavenge
收集器的目标则是
达到一个可控制的吞吐量,
也被称为
吞吐量优先收集器
。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
高吞吐量和停顿时间短的策略相比,主要强调任务更快完成,适用于后台运算而不需要太多交互的任务;而后者强调用户交互体验。
Parallel Scavenge提供两个参数精确控制吞吐量,
-XX:MaxGCPauseMillis
控制最大垃圾收集停顿时间
和-XX:GCTimeRatio
设置吞吐量大小
1).MaxGCPauseMillis允许的值是一个大于零0的毫秒数,
收集器将尽力保证内存回收花费的时间不超过设定值
。
GC停顿时间缩小
是以
牺牲吞吐量
和
新生代空间
来换取的,也就是要使停顿时间更短,需要使新生代的空间减小,这样垃圾回收的频率会增加,吞吐量也降下来了。
2).GCTimeRatio的值是一个大于0小于100的整数,也就是
垃圾收集时间占总时间的比率
。
默认为
99
,则允许最大GC时间就占总时间的1/(1+99).
3).
-XX:+UseAdaptiveSizePolicy,
打开GC自适应调节策略
后
会自动设置新生代大小、调整Eden与Survior区的比例、晋升老年代对象年龄,新生代大小等细节参数。
这个参数也是Parallel Scavenge和ParNew的重要区别。
算法
:
复制算法
应用:
适合在后台运算而不需要太多交互的任务
新生代
ParNew
/
Parallel Scavenge
与年老代
Serial Old
搭配垃圾收集过程图:
4. Serial Old收集器
是Serial收集器的老年代版本,也同样是一个
单线程
的收集器,使用
标记-整理
算法。主要是client模式下的虚拟机使用。参考上面图Serial/Serial old.
两大用途
:
(1)
在
JDK1.5及之前
的版本中与Parallel Scavenge搭配使用
;
收集过程:
暂停所有用户线程,
单线程
算法:
标记-整理算法
应用:主要意义是
Client模式下的收集器
,如果在Server模式下:参看上面的两大用途。
5. Parallel Old收集器
是Parallel Scavenge收集器的老年代版本, 在
JDK1.6中才开始使用
。
由于之前的版本中,
Parallel Scavenge
只有使用Serial Old作为老年代收集器,其吞吐量优先的设计思路不能被很好的贯彻.
在
Parallel Old
收集器出现后,
Parallel Scavenge
和
Parallel Old
的配合主要用于贯彻这种
吞吐量优先的设计思路
。
收集过程:
多线程
算法:
标记-整理算法
应用:
在注重吞吐量及CPU资源敏感的场合,可以优先考虑Parallel Scavenge加Parallel Old收集器
新生代
Parallel Scavenge
和年老代
Parallel Old
收集器搭配运行过程图:
6. CMS收集器
Concurrent Mark Sweep 以获取最短回收停顿时间为目标的收集器,比较理想的应用场景是B/S架构的服务器。
CMS收集器工作过程:
基于
标记-清除算法
实现,运行过程分成4个步骤:
a)初始标记(需要stop the world),标记一下GC Roots能直接关联到的对象,速度很快
b)并发标记,进行GC Roots Tracing的过程。
c)重新标记(需要stop the world),为了修正并发标记时用户继续运行而产生的标记变化,停顿时间比初始标记长,远比并发标记短。
d)并发清除
缺点:
1).
CMS收集器对CPU资源非常敏感
。在并发阶段,它虽然不会导致用户线程停顿,但是因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量就会降低。
CMS默认启动的回收线程数为
(CPU数量+3)/4
。为了解决这一情况,有一个变种i-CMS,但目前并不推荐使用。
2) .CMS收集器
无法处理浮动垃圾(floating garbage),浮动垃圾即在并发清除阶段因为是并发执行,还会产生垃圾,这一部分垃圾即为浮动垃圾,要等下次收集。。
同样由于
CMS GC阶段用户线程还需要运行
,即还需要预留足够的内存空间供用户线程使用,因此CMS收集器
需要预留一部分空间提供并发收集时的程序运作使用
。
默认设置下,CMS收集器在
老年代使用了68%的空间
后就会被激活
。
这个值可以用
-XX:CMSInitiatingOccupancyFraction
来设置。
要是CMS运行期间
预留的内存无法满足程序需要
,就会出现
concurrent mode failure
,这时候就会启用
Serial Old
收集器作为备用进行老年代的垃圾收集
。
3). 空间碎片过多(标记-清除算法的弊端),提供-XX:+UseCMSCompactAtFullCollection参数,应用于在FULL GC后再进行一个碎片整理过程; -XX:CMSFullGCsBeforeCompaction,多少次不压缩的full gc后来一次带压缩的。
7. G1收集器
(jdk1.7后全新的回收器, 用于取代CMS)
HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。其与其它收集器相比,G1具备如下特点:
- 并行与并发:和CMS类似。
- 分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
- 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
-XX:+UseG1GC:使用 G1 回收器。
G1收集器与前面的CMS收集器相比有两个显著的改进:
一是G1收集器是基于
“
标记-整理
”
算法实现的收集器,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。
二是它可以
非常精确地控制停顿
,
即既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1收集器可以
实现
在基本不牺牲吞吐量的前提下完成低停顿的内存回收
,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1
将
整个Java堆划分为多个大小固定的独立区域
(Region),并且
跟踪
这些区域里面的
垃圾堆积程度
,在后台维护一个
优先列表
,每次根据允许的收集时间,
优先回收垃圾最多的区域
(这就是Garbage First名称的来由)
。
区域划分及有优先级的区域回收
,保证了G1收集器在有限的时间内可以获得最高的收集效率
。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用
Remembered Set
来
避免全堆扫描
。
G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序
对Reference类型数据进行写操作时
,会产生一个
Write Barrier
暂时
中断写操作
,
检查
Reference引用的对象
是否处于不同的Region之间
(在分代中例子中就是检查是否老年代中的对象引用了新生代的对象),
如果是便通过
CardTable
把相关引用信息记录到
被引用对象所属的Region的Remembered Set中
。
当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
- 初始标记(Initial Making)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
看上去跟CMS收集器的运作过程有几分相似,不过确实也这样。
初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。
并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。
而
最终标记
阶段需要
把
Remembered Set Logs
的数据合并到
Remembered Set
中,这阶段需要停顿线程,但可并行执行
。
最后
筛选回收
阶段
首先对
各个Region的回收价值和成本进行排序
,根据用户所期望的GC停顿时间来制定回收计划
,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。下图为G1收集器运行示意图:
总结
传统的GC收集器将连续的内存空间划分为
新生代
、
老年代
和
永久代
(
JDK 8去除了永久代,引入了元空间Metaspace
),这种划分的特点是
各代的存储地址是连续的
。
而G1的
各代存储地址是不连续的
,每一代都使用了
n个不连续的大小相同的Region
,
每个Region占有一块连续的虚拟内存地址
。