概述
说到垃圾收集(Garbage Collection,GC),大部分人都会把这项技术当做 Java 语言的伴生产物。事实上,GC的历史比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期时,人们就在思考GC需要完成的3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
经过半个多时间的发展,目前内存的动态分配与内存回收技术已经相当的成熟,一切看起来都进入了“自动化”时代,那为什么我们还要去了解GC和内存分配呢?答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。
在 深入理解JVM虚拟机 - JVM运行时数据区 中,介绍了JVM内存运行时区域的各个部分,其中 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,因此在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随者回收了。而Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存,本文后续讨论中的“内存”分配与回收也仅指这一部分内存。
如何判定对象已死亡
对象“存活”判定算法
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死亡”(即不可能被任何途径使用的对象)。
1、引用计数算法
原理:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。 缺点:很难解决对象相互循环引用的问题(两个对象相互循环引用,但其实他们都已经没有用了)。
2、可达性分析算法
在主流的商用程序语言(Java、C#、Lisp)的主流实现中都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。
原理:通过一些列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
再谈引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种应用强度依次逐渐减弱。
强引用
强引用就是指在程序代码之中普遍存在的,类似“Object object = new Object()
”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用
软引用是用来描述一些还在用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收完成还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
弱引用
弱引用也是用来描述非必需对象的,但是它的强度比软引用要更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
虚引用
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
回收方法区
很多人认为方法区(或者Hotspot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的"性价比" 一般比较低:在堆中,尤其是新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
垃圾收集算法
1、标记-清除算法(Mark-Sweep)
原理:
标记-清除算法(Mark-Sweep)分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:
1.效率问题,标记和清除两个过程的效率都不高;
2.空间问题,标记清除后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2、复制算法
原理:
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:
需要复制,效率降低、浪费空间。
现在的商业虚拟机都采用这种收集算法来回收新生代。IBM公司的专门研究表明,新生代中对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
3、标记-整理算法(Mark-Compact)
原理: 标记过程任然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法(Generational Collection)
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
4、分代收集算法
当前的商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection) 算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把 Java 堆分为 年轻代 和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在年轻代中,每次垃圾收集时都发现有大批的对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者 **“标记-整理” **算法来进行回收。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商,不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于JDK1.7 Update 14之后的HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器,之前G1仍处于试验状态),这个虚拟机包含的所有收集器如下图所示:
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。收集器所处的区域,则表示它是属于新生代收集器还是老年代收集器。
在介绍这些收集器各种的特性之前,我们先来明确一个观点:虽然我们是在对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到目前为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。
1、Serial收集器
它是一个单线程的收集器,但它的“单线程”的意义并不仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
2、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。
3、Parallel Scavenge收集器
Parallel Scavenge收集器是一个新时代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程暂停的时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughout)。所谓吞吐量就是CPU运行用户代码的时间和总耗时的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
4、Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
5、Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本。
6、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
7、G1收集器
G1收集器