目录
1.死亡对象的判断算法
1.1.引用计数算法
1.2.可达性分析算法(主流)
PS:强引用、软引用、弱引用、虚引用
2.垃圾回收算法
2.1.标记-清除算法
2.2.复制算法
2.3.标记-整理算法
2.4.分代算法(主流)
PS:哪些对象会进入新生代?哪些对象会进入老年代?
PS:(面试题)请问了解Minor GC和Full GC么,这两种GC有什么不一样吗?
3.垃圾收集器
PS:并行、并发、吞吐量
PS:为什么会有这么多垃圾收集器?
3.1.Serial收集器(新生代收集器,串行GC)
①特性:
②应用场景:
③优势:
3.2.ParNew收集器(新生代收集器,并行GC)
①特性:
②应用场景:
③对比分析:
3.3.Parallel Scavenge收集器(新生代收集器,并行GC)
①特性:
②应用场景:
③对比分析:
④GC自适应的调节策略:
3.4.Serial Old收集器(老年代收集器,串行GC)
①特性:
②应用场景:
3.5.Parallel Old收集器(老年代收集器,并行GC)
①特性:
②应用场景:
3.6.CMS收集器(老年代收集器,并发GC)(主流)
①特性:
②优点:
③缺点:
3.7.G1收集器(唯一一款全区域的垃圾回收器)(主流)
①年轻代垃圾收集
②老年代垃收集
4.总结:一个对象的一生
之前讲了Java运行时内存的各个区域。对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性, 因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此此篇文章有关内存分配和回收关注的为 Java 堆与方法区这两个区域。
Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法:
内存 & 对象
在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。
引用计数描述的算法为——给对象增加一个引用计数器:
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计数法进行内存管理。
但是,在主流的 JVM 中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题:A对象引用了B对象,B对象又引用了A对象,其实是没有任何一个线程引用它俩,但它俩相互引用了,从而造成垃圾不能回收。
范例:观察循环引用问题
/**
* JVM参数 :-XX:+PrintGC
*/
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
// 强制jvm进行垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
[GC (System.gc()) 6092K->856K(125952K), 0.0007504 secs]
从结果可以看出,GC日志包含" 6092K【垃圾回收前内存】->856K(125952K)【垃圾回收后内存】",意味着虚拟机并没有因为这两个对象互相引用就不回收他们。即JVM并不使用引用计数法来判断对象是否存活。
在上面我们讲了,Java并不采用引用计数法来判断对象是否已"死",而采用"可达性分析"来判断对象是否存活(同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言)。
此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的:
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。
在Java语言中,可作为GC Roots的对象包含下面几种:
从上面可以看出“引用”的功能,除了最早使用引用来查找对象,现在还可以使用引用来判断死亡对象了。
所以在 JDK1.2 时,Java 对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。
PS:强引用、软引用、弱引用、虚引用
- 强引用(gc的时候不会被回收):强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
- 软引用(在内存溢出之前被回收):软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
- 弱引用(下一次gc就会被回收):弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内存是否够用,都会回收掉只被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。
- 虚引用(通过引用得不到对象,只是在gc的时候收到一个通知):虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
通过上面的学习可以将死亡对象标记出来了,标记出来之后就可以进行垃圾回收操作了。
来看下垃圾回收机器使用的几种算法(这些算法是垃圾收集器的指导思想):
"标记-清除"算法是最基础的收集算法。
算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
后续的收集算法都是基于这种思路并对其不足加以改进而已。
"标记-清除"算法的不足主要有两个:
"复制"算法是为了解决"标记-清理"的效率问题。
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。
这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。
算法的执行流程如下图:
现在的商用虚拟机(包括HotSpot)都是采用这种收集算法来回收新生代。
新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1:1的比例来划分内存空间,而是将新生代内存分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块 Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
HotSpot默认Eden与Survivor的大小比例是8 : 2,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
HotSpot实现的复制算法流程如下:
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略, 从而实现更好的垃圾回收。这就好比中国的一国两制方针一样,对于不同的情况和地域设置更符合当地的规则,从而实现更好的管理,这就是分代算法的设计思想。
当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。
PS:哪些对象会进入新生代?哪些对象会进入老年代?
- 新生代:一般创建的对象都会进入新生代;
- 老年代:大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代。
PS:(面试题)请问了解Minor GC和Full GC么,这两种GC有什么不一样吗?
- Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
- Full GC 又称为老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC, 经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行 Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
如果说上面讲的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。
以下这些收集器是 HotSpot 虚拟机随着不同版本推出的重要的垃圾收集器:
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们之间可以搭配使用。所处的区域,表示它是属于新生代收集器还是老年代收集器。
PS:并行、并发、吞吐量
- 并行(Parallel):指多条垃圾收集线程并行工作,用户线程仍处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个CPU上。
- 吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。
吞吐量 = 运行用 户代码时间 /(运行用户代码时间 + 垃圾收集时间)
例如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
PS:为什么会有这么多垃圾收集器?
- 自从有了 Java 语言就有了垃圾收集器,这么多垃圾收集器其实是历史发展的产物。
- 最早的垃圾收集器为 Serial,也就是串行执行的垃圾收集器,Serial Old 为串行的老年代收集器。
- 而随着时间的发展,为了提升更高的性能,于是有了 Serial 多线程版的垃圾收集器 ParNew。
- 后来人们想要更高吞吐量的垃圾收集器,吞吐量是指单位时间内成功回收垃圾的数量,于是就有了吞吐量优先的垃圾收集器 Parallel Scavenge(吞吐量优先的新生代垃圾收集器)和 Parallel Old(吞吐量优先的老年代垃圾收集器)。
- 随着技术的发展后来又有了 CMS(Concurrent Mark Sweep)垃圾收集器,CMS 可以兼顾吞吐量和以获取最短回收停顿时间为目标的收集器,在 JDK 1.8(包含)之前 BS 系统(以浏览器操作,不能让用户等太长时间)的主流垃圾收集器。
- 而在 JDK1.8 之后,出现了第一个既不完全属于新生代也不完全属于老年代的垃圾收集器 G1(Garbage First),G1 提供了基本不需要停止程序就可以收集垃圾的技术。
Serial 收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)。
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 实际上到现在为止,它依然是虚拟机运行在 Client 模式下的默认新生代收集器。
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
Serial 收集器的多线程版本。
ParNew 收集器是许多运行在 Server 模式下的虚拟机中首选的新生代收集器。
作为 Server 的首选收集器之中有一个与性能无关的很重要的原因是:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
在JDK 1.5时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS 收集器,这款收集器是HotSpot 虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
不幸的是,CMS 作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者 Serial收集器中的一个。
与Serial收集器对比:ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器使用两个参数控制吞吐量:
直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每 次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到 一个可控制的吞吐量(Throughput)。 由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。
Parallel Scavenge收集器有一个参数- XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了, 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
注:工作流程图同ParNew。
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与 Parallel Scavenge收集器搭配使用;另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。 这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。 CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4 个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器 不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供 并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一 次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集 器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产 生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
G1(Garbage First)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的 region块,然后并行地对其进行垃圾回收。 G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。 G1垃圾回收器回收region的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的。
一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured(最终的,老年代)内存区域。图中空白的表示未使用的内存空间。 G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象。
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的 Survivor区域。
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:
G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。
如果你的应用追求低停顿,G1可以作为选择;
如果你的应用追求吞吐量,G1并不带来特别明显的好处。