文章目录
JVM垃圾回收机制GC
Java的垃圾回收机制是Java虚拟机提供的一种动态存储管理技术,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。也就是说,垃圾收集器回收的是无任何引用的对象占据的内存空间而不是对象本身。GC主要做清理对象、整理内存的工作,它可以有效的防止内存泄露,保证内存有效使用。它使得Java程序员在编写程序的时候不再需要考虑内存管理。
垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
一、Where:针对哪些区域进行垃圾回收?(java堆和方法区)
1、JVM的内存模型/内存布局
运行时内存区域 :虚拟机栈、程序计数器、本地方法栈、Java堆、方法区。
JVM的内存模型/内存布局
2、为什么java堆和方法区需要进行垃圾回收?
程序计数器,虚拟机栈,本地方法栈这三个区域线程私有,随线程而生,随线程而灭。因此这几个区域的内存分配和回收都具有确定性,方法结束或者线程结束时,内存就自然跟着回收了,所以这几个区域不需要过多的考虑回收的问题。
堆和方法区则不太一样,一个接口中的多个实现类需要的内存不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器所关注的就是这部分的内存。
二、Who:哪些对象被回收? (对象存活判定算法、4种引用)
在 JVM 进行垃圾回收之前,首先就是判断哪些对象是垃圾,也就是说,要判断哪些对象是可以被销毁的,其占有的空间是可以被回收的。根据 JVM 的架构划分,我们知道, 在 Java 世界中,几乎所有的对象实例都在堆中存放,所以垃圾回收也主要是针对堆来进行的。
在 JVM 的眼中,垃圾就是指那些在堆中存在的,已经“死亡”的对象。而对于“死亡”的定义,我们可以简单的将其理解为“不可能再被任何途径使用的对象”。那怎样才能确定一个对象是存活还是死亡呢?这就涉及到了垃圾判断算法,其主要包括引用计数法和可达性分析法。
1、判断对象是否存活
(1)引用计数算法(Reference Counting)
引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。给对象中添加一个引用计数器,每当对象被引用时,计数器就加 1;当引用失效,计数器就减 1;若某一个对象引用计数器的值为0,那么表示这个对象没有被其他对象引用,是一个失效的垃圾对象,被当做垃圾回收。
- 优点:实现简单、判定效率高,程序执行受影响较小。(Python使用,微软的COM技术使用)
- 缺点:无法识别对象之间的相互循环引用,容易导致内存泄露。(java语言没有使用)
下面这段代码是用来验证引用计数算法能不能检测出循环引用。testGC ()方法:对象objA和objB都有字段instance,赋值令objB.instance = objA及objA.instance = objB,程序最后将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,如果JVM通过引用计数法来判断对象是否存活,就无法通知GC收集器回收它们,这样容易造成内存泄露。
/**
*teatGC()方法执行后,objA和objB会不会被GC呢?
*@auther zzm
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
*这个成员属性是为了占点内存,以便能在GC日志中看清楚是否被回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
// 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为 0
objB.instance = objA;
objA.instance = objB;
objA = null;
objB = null;
//假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}
运行结果:
[GC (System.gc()) [PSYoungGen: 7168K->792K(35840K)] 7168K->800K(117760K), 0.0024442 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 792K->0K(35840K)] [ParOldGen: 8K->667K(81920K)] 800K->667K(117760K), [Metaspace: 3491K->3491K(1056768K)], 0.0101934 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 35840K, used 307K [0x00000000d8700000, 0x00000000daf00000, 0x0000000100000000)
eden space 30720K, 1% used [0x00000000d8700000,0x00000000d874ce40,0x00000000da500000)
from space 5120K, 0% used [0x00000000da500000,0x00000000da500000,0x00000000daa00000)
to space 5120K, 0% used [0x00000000daa00000,0x00000000daa00000,0x00000000daf00000)
ParOldGen total 81920K, used 667K [0x0000000089400000, 0x000000008e400000, 0x00000000d8700000)
object space 81920K, 0% used [0x0000000089400000,0x00000000894a6ff8,0x000000008e400000)
Metaspace used 3498K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
从运行结果可以看到,GC日志中包含“800K->667K”,意味着JVM并没有因为这两个对象互相引用就不执行垃圾回收,这也说明JVM并不是通过引用计数算法来判断对象是否存活的。
(2)可达性分析算法(根搜索算法)
可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(意味着GC Roots到这个对象不可达),证明该对象是不可用的,被当做垃圾回收。
- 优点:可以解决循环引用的问题,不需要占用额外的空间;
- 缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用;
(3)JVM中4种引用和使用场景
强引用、软引用、弱引用、虚引用,引用强度越来越低,引用强度越弱的对象越容易被垃圾回收,Java垃圾回收器会优先清理可达性强度低(引用强度弱)的对象。
JVM中4种引用和使用场景
(4)判断对象的可达性
若一个对象的引用类型有多个,如何判断他的可达性?(单弱多强)
- 单条引用链的可达性以最弱的一个引用类型来决定;
- 多条引用链的可达性以最强的一个引用类型来决定;
(5)GC Roots对象
Java语言中可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性(静态成员)或常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象。
(6)Stop-The-World(STW)
Stop-The-World:由虚拟机在后台自动发起、自动完成,在用户不可见的情况下把用户正常工作的线程全部停掉,即GC停顿。STW是导致GC卡顿的重要原因之一,所以尽可能减少STW的时间,就是我们优化JVM的主要目标。
可达性分析时为了确保对象之间的引用关系不会被打乱(快照的一致性),需要对整个系统进行冻结,暂停所有工作线程,工作线程不停止的话,会有新对象生成,打乱对象之间的引用关系。不能出现分析过程中对象引用关系还不断变化的情况,如果对象之间的引用关系一直在变化的话是无法真正去遍历它的。
(7)不可达的对象是否是必死之局呢?
在可达性分析算法中不可达的对象,也并非非死不可的,对象可以在被GC时自我拯救,这种自救的机会只有一次。
对象的自我拯救
2、方法区如何判断是否需要回收?
方法区的内存回收目标主要是针对 常量池的回收 和 对类型的卸载。JVM规范不要求在方法区实现垃圾收集,在方法区进行垃圾收集性价比较低,在堆的新生代中,一次可回收70%~90%的空间,永久代的垃圾收集效率元低于此。 是否是废弃常量可以通过该常量是否被其他地方引用来判断,判断是否是无用的类则需要同时满足下面3个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。特别地,在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
三、When:何时触发垃圾回收机制GC?(Minor GC和Full GC)
1、什么时候进行垃圾回收?
- 会在cpu空闲的时候自动进行回收 ;
- 在堆内存存储满了(新生代或老年代)之后;
- 主动调用System.gc()方法后尝试进行回收;
GC本身是会周期性的自动运行的,由JVM决定运行的时机,而且现在的版本有多种更智能的模式可以选择,还会根据运行的机器自动去做选择,就算真的有性能上的需求,也应该去对GC的运行机制进行微调,而不是通过使用System.gc()命令来实现性能的优化。
2、Minor GC (新生代GC)
- Minor GC:新生代GC,是指从新生代空间(包括 Eden 和 Survivor 区域)回收内存。新生代中大多对象朝生夕灭,所以Minor GC发生的非常频繁,回收速度也快。Minor GC速度一般比Full GC快10倍以上。
- 回收过程:复制算法。因为新生代大多对象朝生夕灭,每次会有大批对象死去,只有少量存活,所以选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 触发条件:
(1)当Eden区满时;
(2)新创建的对象的大小大于Eden区剩余的空间时。
3、Full GC (老年代GC)
-
Full GC:又叫Major GC,发生在老年代的垃圾回收动作。Major GC 经常会伴随至少一次 Minor GC。由于老年代中的对象生命周期比较长,因此 Major GC 并不频繁,一般都是等待老年代满了后才进行 Full GC,而且其速度一般会比 Minor GC 慢10倍以上。在进行Full GC前一般都先进行了一次MinorGC,使得有新生代的对象晋升入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
-
回收过程:标记—清除算法,标记—整理算法。老年代对象存活率高,没有额外空间进行分配担保,所以使用标记—清除算法,标记—整理算法。
-
触发条件:
(1)调用System.gc()时,系统建议执行Full GC,但是不必然执行;
(2)老年代空间不足、永久代空间不足;
(3)通过Minor GC后进入老年代对象的平均大小大于老年代的可用内存;
(4)在新生代回收内存时,由Eden区和Survivor From区把存活的对象向Survivor To区复制时,对象大小大于Survivor To空间的可用内存,则把该对象转存到老年代(这个过程称为分配担保),且老年代的可用内存小于该对象大小。即老年代无法存放下新生代过度到老年代的对象的时候,便会触发Full GC。
4、我们可以主动进行垃圾回收吗?
我们可以主动调用System.gc()方法进行垃圾回收,每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime 方法获取当前运行。java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,两者的行为没有任何不同。
补充:System.gc()用于调用垃圾收集器,在调用时,垃圾收集器将运行以回收未使用的内存空间,它将尝试释放被丢弃对象占用的内存。 然而System.gc()调用附带一个免责声明,它只是建议JVM安排GC运行, 还有可能完全被拒绝,无法保证对垃圾收集器的调用,所以System.gc()并不能说是完美的主动进了垃圾回收。
四、How:对象如何被回收?(垃圾回收算法)
在可达性分析法中,对象有两种状态,那么是可达的、要么是不可达的,在判断一个对象的可达性的时候,就需要对对象进行标记。关于标记阶段,有几个关键点是值得我们注意的,分别是:
- 开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便 JVM 可以尽情地进行垃圾回收的这种情况又被称之为安全点(Safe Point),这会触发一次 Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。所以尽可能减少STW的时间,就是我们优化JVM的主要目标。
安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生安全点。对于安全点,另一个需要考虑的问题就是如何在 GC 发生时让所有线程(这里不包括执行 JNI 调用的线程)都“跑”到最近的安全点上再停顿下来。两种解决方案:
- 抢先式中断(Preemptive Suspension):抢先式中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应 GC 事件。
- 主动式中断(Voluntary Suspension):主动式中断的思想是当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志地地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
- 暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
1、标记—清除算法(Maik—Sweep)
(1)基本过程:标记-清除算法是一种最基础的算法,后面的收集算法都是根据这种算法的不足进行改进而得到的。分为标记和清除两个阶段。
- 标记:标记的过程就是遍历所有GC Roots,然后标记出所有需要回收的对象(GC Roots不可达的对象)。
- 清除:遍历堆中所有对象,将标记的对象全部回收掉。
(2)优缺点:
-
优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
-
效率问题:标记和清除效率都不高(标记和清除都需要遍历全堆对象),这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;
-
空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够的连续内存从而不得不再次触发垃圾收集动作。
下图为“标记-清除”算法的示意图:
下图为使用“标记-清除”算法回收前后的状态:
2、标记—整理算法(Mark—Compact)
(1)基本过程:标记-整理算法标记的过程与标记-清除算法中的标记过程一样,但对标记出的垃圾对象的处理情况有所不同。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。
- 标记:标记的过程就是遍历所有GC Roots,然后标记出所有需要回收的对象(GC Roots不可达的对象)。
- 整理:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
(2)优缺点:
- 优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
- 缺点:GC 暂停的时间会增长,因为你需要将所有的存活对象都拷贝到一个新的地方,还得更新它们的引用地址。每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。
下图为“标记-整理”算法的示意图:
下图为使用“标记-整理”算法回收前后的状态:
3、复制算法(Copying)
(1)基本过程:复制算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将可用内存按容量划分为大小相同的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块上面(空闲面),然后再把已使用过的内存空间一次性清理掉。每次都是对整个半区内存回收,没有内存碎片。
(2)优缺点:
- 优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
- 效率问题:当对象存活率较高时,复制次数过多,效率降低;
- 空间问题:可一次性分配的最大内存缩小了一半,浪费了内存资源,需要额外的空间做分配担保(老年代)(当内存中所有对象100%存活的极端情况)。
下图为复制算法的示意图:
下图为使用复制算法回收前后的状态:
事实上,现在商用的虚拟机都采用这种算法来回收新生代。因为研究发现,新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。实践中会将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90% ( 80%+10% ),只有10% 的内存会被“浪费”。
4、分代收集算法(Generational Collection)
分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的算法进行回收可以提高 JVM 的执行效率。
堆结构分代的意义:堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。所以给堆内存分代就是为了提高垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率。
- Java虚拟机根据对象对象存活的周期不同,一般将堆内存划分为新生代(Young Generation),老年代(Tenured Generation),永久代(Permanet Generation)(对HotStop虚拟机而言)。
值得注意的是,在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
方法区是接口,永久代是接口实现类,永久代实现了方法区。
(1)新生代(Young Generation)
一般情况下,所有新生成的对象首先都是放在新生代的。新生代中大多对象朝生夕灭。对象存活率低,所以Minor GC发生的非常频繁,回收速度也快。新生代内存按照 8:1:1 的比例分为一个eden区和两个survivor(survivor0,survivor1)区,大部分对象在Eden区中生成。当新对象生成,Eden 空间申请失败(因为空间不足等),则会发起一次 GC(Scavenge GC)。在进行垃圾回收时,先将eden区存活对象复制到survivor0区,然后清空eden区,当这个survivor0区也满了时,则将eden区和survivor0区存活对象复制到survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后交换survivor0区和survivor1区的角色(即下次垃圾回收时会扫描Eden区和survivor1区),即保持survivor0区为空,如此往复。交替使用、循环往复。
特别地,当survivor1区也不足以存放eden区和survivor0区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。
当对象在 Survivor 区躲过一次 GC 的话,其对象年龄便会加 1,默认情况下,如果对象年龄达到 15 岁,(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),就会移动到老年代中。若是老年代也满了就会触发一次 Full GC。新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制 Eden 和 Survivor 的比例。
(2)老年代(Tenured Generation)
老年代一般存放的是一些生命周期较长的对象,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大很多(大概比例是1:2),当老年代满时会触发Major GC(Full GC),老年代对象存活时间比较长,存活率高,因此FullGC发生的频率比较低。
一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和 JVM 的相关参数。
(3)永久代(Permanet Generation)
用于存放静态文件(class类、方法)和常量等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对永久代(方法区)的回收主要回收两部分内容:废弃常量和无用的类。永久代在 Java SE8 特性中已经被移除了,取而代之的是元空间(MetaSpace),因此也不会再出现java.lang.OutOfMemoryError: PermGen error的错误了。
(4)分代收集
分代收集算法就是根据各个年代的特点选择最适合的收集算法。
- 新生代中,对象的大量死亡,只有少数对象存活,复制算法最合适,只需要付出少量存活对象的复制成本就可以完成垃圾收集。
- 老年代中,对象存活率高,没有额外的空间对它进行分配担保,所以标记—清理算法或者标记—整理算法值是最合适的。
五、垃圾收集器(内存回收的具体实现)
1、垃圾收集器
JVM是一个进程,垃圾收集器就是一个线程,垃圾收集线程是一个守护线程,优先级低,其在当前系统空闲或堆中老年代占用率较大时触发。GC线程与应用线程保持相对独立,当系统需要执行垃圾回收任务时,先停止工作线程,然后命令 GC 线程工作。
下图展示了7种作用于不同分代的收集器,虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器;如果两个收集器之间存在连线,就说明它们可以搭配使用:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1。
(一)垃圾收集器分类
(1)新生代收集器还是老年代收集器:
- 新生代收集器:Serial、ParNew、Parallel Scavenge;
- 老年代收集器:Serial Old、Parallel Old、CMS;
- 整堆收集器:G1
(2)吞吐量优先、停顿时间优先
- 吞吐量优先:Parallel Scavenge收集器、Parallel Old 收集器
- 停顿时间优先:CMS(Concurrent Mark-Sweep)收集器
(3)吞吐量与停顿时间适用场景
- 停顿时间优先:交互多,对响应速度要求高。
- 吞吐量优先:交互少,计算多,适合在后台运算的场景。
吞吐量(Throughput)就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
(4)算法
- 复制算法:Serial收集器、ParNew收集器、Parallel Scavenge收集器、G1收集器
- 标记-清除:CMS收集器
- 标记-整理:Serial Old收集器、Parallel Old收集器、G1收集器
(5)串行并行并发
- 串行:Serial收集器、Serial Old收集器
- 并行:ParNew收集器、Parallel Scavenge收集器、Parallel Old收集器
- 并发:CMS收集器、G1收集器
以串行模式工作的收集器,称为Serial Collector,即串行收集器;与之相对的是以并行模式工作的收集器,称为Paraller Collector,即并行收集器。
- 串行(Serial):使用单线程进行垃圾回收的收集器。通过名字就可以看出来,串行的都带有Serial关键字。
- 并行(Parallel):多条垃圾收集线程并行工作,用户线程处于等待状态。通过名字就可以看出来,并行的都带有Parallel关键字,ParNew的Par也是Parallel缩写。
- 并发(Concurrnet):用户线程与垃圾收集线程同时执行(不一定是并行,可以是交替执行),用户线程在继续执行,而垃圾收集器运行在另一个CPU上。
(二)垃圾收集器
(1)Serial收集器(复制算法)
- Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器,JDK1.3.1前是HotSpot新生代收集的唯一选择。新生代单线程串行收集器,标记和清理都是单线程,简单高效。单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着在它进行垃圾收集时,必须暂停其他所有的工作线程,即 Stop-the-world,直到它收集结束为止。
Stop the World是在用户不可见的情况下执行的,会造成某些应用响应变慢;Stop-the-world 暂停时间的长短,是衡量一款收集器性能高低的重要指标。
- 应用场景:Serial收集器没有线程交互的开销,单线程收集效率高,Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
- 参数:
–XX:+UseSerialGC:串联收集器,在JDK Client模式,不指定VM参数,默认是串行垃圾回收器。
(2)ParNew收集器(复制算法)
- 新生代多线程并行收集器,ParNew收集器就是Serial收集器的多线程版本,采用多个 GC 线程并行收集。它也是一个新生代收集器,除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码,在多核CPU环境下有着比Serial更好的表现。
一般来说,与串行收集器相比,在多处理器环境下工作的并行收集器能够极大地缩短 Stop-the-world 时间。
- 应用场景:ParNew收集器是运行在Server模式下的虚拟机首选的新生代收集器。很重要的原因是:除了Serial收集器之外,目前只有它能与CMS收集器配合工作。
- 参数:
-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器;
-XX:+UseParNewGC:强制指定使用ParNew;
-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
(3)Parallel Scavenge收集器(复制算法)
- 新生代并行多线程收集器,追求高吞吐量,高吞吐量可以高效率的利用CPU,尽快完成程序的运算任务,适合后台应用等对用户交互相应要求不高的场合。
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为吞吐量优先收集器。Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。
- 应用场景:Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;适合那种交互少、运算多的场景,例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
- 参数:
-XX:+MaxGCPauseMillis:控制最大垃圾收集停顿时间,大于0的毫秒数;这个参数设置的越小,停顿时间可能会缩短,但也会导致吞吐量下降,导致垃圾收集发生得更频繁。
-XX:GCTimeRatio:设置垃圾收集时间占总时间的比率,0<n<100的整数,就相当于设置吞吐量的大小。
–XX:+UseAdptiveSizePolicy:GC自适应的调节策略(GC Ergonomiscs)开关参数,开启这个参数后,就不用手工指定一些细节参数,如:新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量。
先垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n);例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%=1/(1+19);默认值是1%=1/(1+99),即n=99;垃圾收集所花费的时间是年轻一代和老年代收集的总时间;
另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。
(4)Serial Old收集器(标记- 整理算法)
- 老年代单线程串行收集器,Serial收集器的老年代版本,除了收集算法不同,两个版本并没有其他差异。
- 应用场景:
Client模式:Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
Server模式:如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;另一种用途就是作为CMS收集器的后备预案,在并发收集发生"Concurrent Mode Failure"时使用。
(5)Parallel Old收集器(标记-整理算法)
- 老年代多线程并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本,除了收集算法不同,两个版本并没有其他差异。
- 应用场景:
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器组合。JDK1.6及之后用来代替老年代的Serial Old收集器,特别是在Server模式,多CPU的情况下。 - 参数:
-XX:+UseParallelOldGC:指定使用Parallel Old收集器;
(6)CMS收集器(标记-清除算法)
- 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,CMS是HotSpot在JDK 1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作,收集过程中不需要暂停用户线程。
CMS(Concurrent Mark Sweep)收集器并不是独占的回收器,也就是说,CMS回收的过程中应用程序仍然在不停的工作,又会有新的垃圾不断产生,所以在使用CMS的过程应该确保应用程序的内存足够可用,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是在某一阀值(默认为68)的时候开始回收,也就是说当老年代的空间使用率达到68%的时候会激活CMS进行垃圾回收。如果内存使用率增长很快,在CMS执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失败,虚拟机将启动老年代 Serial 进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC的停顿时间可能较长,所以阀值的设置要根据实际情况设置。
-
应用场景:与用户交互较多的场景、互联网或者B/S系统的服务端上,这类应用尤其注重服务的响应速度,希望系统停顿时间最短,以给用户带来极好的体验,在重视响应速度和用户体验的应用中,CMS应用很多。
-
参数:
-XX:+UseConcMarkSweepGC:使用CMS收集器;
-XX:+ UseCMSCompactAtFullCollection:Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长;
-XX:+CMSFullGCsBeforeCompaction:设置进行几次Full GC后,进行一次碎片整理;
-XX:ParallelCMSThreads:设定CMS的线程数量(一般情况约等于可用CPU数量), CMS默认启动的回收线程数是(cpu数量+3)/4。 -
优点:并发收集、低停顿、收集线程与用户线程同时工作;总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
缺点:
①对CPU资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
②无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉,这一部分垃圾就称为“浮动垃圾”。
③Concurrent Mode Failure失败,如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样会导致另一次Full GC的产生,停顿时间就更长了,代价会更大。所以 "-XX:CMSInitiatingOccupancyFraction"不能设置得太高,太高容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
④产生大量内存碎片,使用的标记-清除算法会导致收集结束产生大量空间碎片,给大对象分配带来麻烦,老年代还有很大空间剩余,但是无法找到足够答的连续内存来分配当前对象,不得不提前触发一次Full GC。由于空间不再连续,CMS需要使用"空闲列表"内存分配方式,比简单实用的"碰撞指针"内存分配方式消耗大。
浮动垃圾解决方法:
由于在垃圾收集阶段用户线程还需要运行,那就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,也可以认为CMS所需要的空间比其他垃圾收集器大;
-XX:CMSInitiatingOccupancyFraction:触发(启动阈值)百分比,设置CMS预留内存空间;JDK1.5默认值为68%;JDK1.6变为大约92%;当老年代的空间使用率达到设定的值,会激活CMS进行垃圾回收。
内存碎片解决方法:
(1)-XX:+UseCMSCompactAtFullCollection:开关参数,默认开启,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程;内存整理过程无法并发,空间碎片问题没有了,但停顿时间不得不变长。它只用来开启内存碎片的合并整理过程,需要结合CMSFullGCsBeforeCompaction参数使用。
(2)-XX:+CMSFullGCsBeforeCompaction:设置执行多少次不压缩的Full GC后,进行一次压缩整理;默认为0,也就是说每次都执行Full GC,不进行压缩整理;可以通过设置这个参数来减少合并整理过程的停顿时间。
CMS收集器的运作过程:
- 初始标记(initial mark):单线程执行,暂停所有的其他线程Stop The World,标记GC Roots的直接关联可达的对象,由于直接关联对象比较少,所以这里的速度非常快。
- 并发标记(concurrent mark): 对于初始标记过程所标记的初始标记对象,进行并发追踪标记,此时其他线程仍可以继续工作。此处时间较长,但不停顿。并不能保证可以标记出所有的存活对象;因为用户线程可能会不断的更新引用域,GC线程无法保证可达性分析的实时性,所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记(remark):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,此处执行并行标记,与用户线程不并发,所以依然是“Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短。
- 并发清除(concurrent sweep):并发清除之前所标记的垃圾,其他用户线程仍可以工作,不需要停顿。
其中,初始标记和并发标记仍然需要Stop the World,初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段长,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以整体上说,CMS收集器的内存回收过程是与用户线程一共并发执行的。
(7)G1收集器(标记-整理算法)
-
Java堆并行收集器,G1(Garbage - First)收集器是JDK1.7提供的一个新收集器,G1收集器基于标记-整理算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代、老年代),而前六种收集器的范围仅限于新生代或老年代。
-
应用场景:面向服务端应用,针对具有大内存、多处理器的机器;最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案,如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒。
对于打算从 CMS 或者 ParallelOld 收集器迁移过来的应用,按照官方的建议,如果发现符合如下特征,可以考虑更换成 G1 收集器以追求更佳性能:
- 实时数据占用了超过半数的堆空间;
- 对象分配率或“晋升”的速度变化明显;
- 期望消除耗时较长的GC或停顿(超过 0.5 ~ 1 秒)。
- 参数:
-XX:+UseG1GC:指定使用G1收集器;
-XX:InitiatingHeapOccupancyPercent:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
-XX:MaxGCPauseMillis:为G1设置暂停时间目标,默认值为200毫秒;
-XX:G1HeapRegionSize:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
G1 与 CMS 的特征对比如下:
特点:
1、并行与并发
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU来缩短stop-the-world停顿时间(并行),也可以并发让垃圾收集与用户程序同时进行。
2、分代收集,收集范围包括新生代和老年代
G1重新定义了堆空间,打破了原有的分代模型,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。
- 能独立管理整个GC堆(老年代和新生代),而不需要与其他收集器搭配。
- 能够采用不同方式处理不同时期的对象。
G1仍然属于分代收集器。新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1 的 GC 模式可以分为两种,分别为:
- Young GC:在分配一般对象(非巨型对象)时,当所有 Eden 区域使用达到最大阀值并且无法申请足够内存时,会触发一次 YoungGC。每次 Young GC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。
- Mixed GC:当越来越多的对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 Old GC,除了回收整个新生代,还会回收一部分的老年代,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些 Old 区域进行收集,从而可以对垃圾回收的耗时时间进行控制。G1 没有 Full GC概念,需要 Full GC 时,调用 Serial Old GC 进行全堆扫描。
3、空间整合,不产生碎片
G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
- 从整体看,是基于标记-整理算法;
- 从局部(两个Region间)看,是基于复制算法。
4、可预测的停顿:低停顿的同时实现高吞吐量
这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同关注的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型。可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒,在低停顿的同时实现高吞吐量,即 G1 提供了接近实时的收集特性。
G1收集器能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
避免全堆扫描—Remebered Set
G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
G1收集器的运作过程:
-
初始标记(Initial Marking) :仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程STW,但耗时很短。
-
并发标记(Concurrent Marking) :进行GC Roots Tracing的过程,从刚才产生的集合中标记出存活对象(也就是从GC Roots 开始对堆进行可达性分析,找出存活对象),此阶段耗时较长,但可与用户程序并发执行,并不能保证可以标记出所有的存活对象。
-
最终标记(Final Marking) :为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程STW,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,可并行执行。
-
筛选回收(Live Data Counting and Evacuation) :首先对各个Region中的回收价值和成本进行排序,然后根据用户所期望的GC 停顿时间来制定回收计划,最后按计划回收一些价值高的Region中垃圾对象。回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存。此阶段可以与用户程序一起并发执行,降低停顿时间,增加吞吐量,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
(三)为什么只有ParNew能与CMS收集器配合?
CMS作为老年代收集器,但却无法与JDK1.4中已经存在的新生代收集器Parallel Scavenge配合工作,因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而是另外独立实现;而其余几种收集器则共用了部分的框架代码。
2、查看 JVM 使用的默认垃圾收集器
我们知道JVM分Client 和 Server模式。如果启动JVM不指定模式,JDK会根据当前的操作系统配置来启动不同模式的JVM。默认64bit操作系统下都会是Server模式的JVM。在 Mac 终端或者 Windows 的 cmd下 执行如下命令:
- java -XX:+PrintCommandLineFlags -version
由此可知,JDK 8 默认打开了UseParallelGC参数,因此使用了Parallel Scavenge + Serial Old的收集器组合进行内存回收。