这篇文章记录的是:深入理解java虚拟机(第二版)第三章与第四章的内容
1. GC要做的三件事:
Q1: 哪些内存需要回收?
GC回收的一般是堆(只用在程序运行期间才能知道会创建哪些对象)和方法区(一个方法中的多个分支需要的内存可能不一样)中存放的已“死去的”java对象,那怎么判断对象已经“死去”了呢?1. 引用计数算法:给对象添加一个计数器,每当在一个地方引用它时,计数器就加1,当引用失效时,计数器就减1,任何时候计数器为零的对象是不可能再被使用的。问题:很难解决对象之间相互循环引用的问题。2. 可达性分析算法:以一系列被称为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何的引用链相连,则证明此对象不可用,即可回收。在Java中,可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象
Q2:什么时候回收?
系统决定,无法预测
Q3:如何回收?
对于不同的内存采用不同的收集算法。比如:Java一般把堆分为新生代和老年代,新生代(特点:朝生夕死)一般采用复制算法,老年代(特点:存活率高)采用“标记-清除”或者“标记-整理”
2. 垃圾收集算法
2.1 标记-清除算法
如同它的名字一样,算法分为“标记”和“清除两个阶段”:首先标记出所有要回收的对象,在标记完成后统一回收所有被标记的对象。不足:1. 标记和清除两个过程的效率都不高。2. 标记清除之后会产生许多大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程中需要分配较大的对象时,无法找到足够连续的内存而不得不提前触发另一次垃圾收集动作。
2.2 复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后把使用过的内存空间一次清理掉。不足:内存缩小为原来的一般。
根据IBM公司研究表明:98%的对象都是“朝生夕死”的,所以内存划分为一块较大的Eden和两块较小的Survivor,比率:8:1:1,每次使用Eden和其中一块Survivor。当垃圾回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间,当然,我们不能保证每次回收时只有不多于10%的对象存活,当Survivor空间不够用时,进行分配担保。
2.3 标记-整理算法
复制算法在对象存活率较高的时候就要就行较多的复制操作,效率将会变低。标记-整理算法与“标记-清除”算法一样,但是后续步骤并不是直接对可回收的对象进行清理,而是让所有存活的对象都像一端移动,然后直接清理掉边界以外的内存。
2. 垃圾收集器
2.1 Serial 收集器
单线程的收集器。单线程的意义并不仅仅说明它只会是有一个CPU或一条收集线程去完成垃圾收集的工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop The World),直到它收集结束。很明显,“Stop The World” 使得用户体验度不好。
- 使用场景:是HotSpot在Client模式下默认的新生代收集器
2.2 ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其他的控制参数与Serial收集器几乎完全一样。
- 使用场景:在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作(因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现),但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
2.3 Parallel Scavenge 收集器
使用复制算法的收集器,也是并行的多线程收集器。特点:达到一个可控制的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码/(运行用户代码+垃圾收集时间)。该收集器提供了两个参数用于精确地控制吞吐量,分别是控制垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。不过,不要认为把停顿时间设置小一点就能使系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更加频繁,原来10秒收集一次,每次停顿10毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-
使用场景:高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间; 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;
2.4 Serial Old 收集器
Serial Old是Serial收集器的老年代版本,它是使用“标记-整理”算法的单线程收集器。这个收集器的主要意义在于给Client模式下的虚拟机使用。如果在Server模式下,主要有两大用途:1. JDK1.5之前的版本中与Parallel Scavenge收集器搭配使用。2. 作为CMS收集器的后备预案。
- 使用场景:主要用于Client模式;Server模式:作为CMS收集器的后备预案
2.5 Parallel Old 收集器
Parallel Old 是Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。在JDK1.6之前,Parallel Scavenge收集器,老年代除了Serial Old之外别无选择,由于Serial Old 收集器在性能上的“拖累”,使得Parallel Scavenge 收集器未必能获得吞吐量最大化的效果。知道Parallel Old 收集器出现之后,“吞吐量优先”才有了名副其实的组合。
- 使用场景:JDK1.6及之后,在Server模式,多CPU的情况下用来代替老年代的Serial Old收集器;
2.5 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用“标记-清除”算法,总共有4个步骤:
- 初始标记
- 并发标记
- 重新标记
-
并发清除
初始标记和重新标记需要“Stop The Word”。初始标记仅仅只是标记GC Root能直接关联的对象,速度很快,并发标记阶段进行GC Roots Tracing过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那部分对象的标记记录。优点:并发收集、低停顿。缺点:1. CMS收集器对CPU资源非常敏感(占用CPU,导致应用程序变慢,总吞吐量降低)。2. CMS收集器无法处理浮动垃圾(清理的同时,程序还在连续不断的产生新的垃圾)。3. “标记-清除”算法会产生大量的空间碎片。
- 使用场景: 与用户交互较多的场景,希望系统停顿时间最短,注重服务的响应速度,以给用户带来较好的体验, 如常见WEB、B/S系统的服务器上的应用
2.6 G1 收集器
G1(Garbage-First)收集器
G1收集器是当今收集器技术的最前沿成果之一。用来替换JDK1.5中发布的CMS收集器。特点:
- 并行与并发:充分利用多CPU、多核环境下的硬件优势。
- 分代收集:G1不需要配合其他的收集器就能独立管理整个GC堆,但它会采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象。
- 空间整合:与CMS的“标记-清除”算法不同,G1是基于“标记-整理”算法实现的,所以不会产生空间碎片。
- 可预测停顿:能指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1收集器大致分为以下几步: - 初始标记
- 并发标记
- 最终标记
-
筛选回收
可以看出来,过程与CMS差不多,最终标记阶段会把Remembered Set Logs的数据合并到Remembered Set中。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
-
使用场景: 面向服务端应用,针对具有大内存、多处理器的机器,最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;