垃圾收集(Garbage Collection),简称GC,是Java语言一个成名特性,使它摆脱了C、C++那样手动管理内存的痛苦,提到垃圾收集,必然想到它是干什么的?简单来说,它是我们管理堆内存和方法区上的空间的好助手,要想对垃圾收集建立最基本的认识,最起码能够回答:
1 .垃圾收集什么时候发生?
2 .垃圾收集回收什么对象?
3 .垃圾回收时做了什么事情?
回答这些问题必须知道Java的垃圾回收是按代的垃圾回收机制。Java里面没有显示的注销内存的方式,有人可能说Java里面有finalize()方法,但是这个方法绝对不是C++中的析构函数,而且执行的时机也是不确定甚至是否执行也是未知的,也有可能使用System.gc(),但是这个方法会显著的影响系统性能,不建议过多使用。
首先简略的回答下上面的三个问题。1.一般发现空间不够或者其它时机会触发GC,GC又分为minor GC/full GC,下面会详细展开说。 2. Java回收那些从GC roots开始不可达的对象3. 主要做的就是停止线程,标记内存,有的会复制清理,有的会标记清理,取决于具体的垃圾回收算法。
上面只是一个粗浅的印象,下面来说说按代的垃圾回收机制。
按代的垃圾回收机制
我在浅析JVM内存分区中提到过Java堆分为新生代和老年代。
新生代(Young Gen)
新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,大多数对象可谓是朝生夕死,GC的频率也比较高,总的来说它有3个空间。
- 1个Eden空间(伊甸园)
- 2个Survivor空间(幸存者)
对象保存在Eden和from survivor区,minor GC运行时,Eden中的幸存对象会被复制到to Survivor(同时对象年龄会增加1)。而from survivor区中的幸存对象会考虑对象年龄,如果年龄没达到阈值,对象依然复制到to survivor中。如果对象达到阈值那么将被移到老年代。复制阶段完成后,Eden和From幸存区中只保存死对象,可以视为清空。如果在复制过程中to幸存区被填满了,剩余的对象将被放到老年代。最后,From survivor和to survivor会调换一下名字,下次Minor GC时,To survivor变为From Survivor。
Eden空间和Survior空间的空间比例默认是8:1,通过参数-XX: SurvivorRatio=8来控制,也可以设置为别的值。
老年代(Old Gen)
对象没有变得不可达(后面会说到不可达即代表对象还保持使用),并且能够从新生代的多次GC中存活下来,就会被拷贝到老年代,其占用的空间也比新生代要多,所以老年代内发生GC的次数明显要少得多,老年代的GC事件一般是在空间已满时发生,执行的过程根据GC类型的不同而有所区别。老年代满时触发FullGC(Major GC),因为老年代中的对象比较“能活”,所以FullGC触发的频率较低。
具体来说,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并且经历过一次minor GC仍然存活就会被转移到Survivor,这时候它的Age也增加1,对象在Survivor区每熬过一次minor GC,年龄就增加1,当增长到一定程度(默认15岁),就会晋升到老年代中,这个程度可以通过-XX: MaxTenuringThreshold设置。
当然按照年龄进入老年代也不是绝对的,虚拟机还支持动态年龄判定,**当Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold设置的年龄。**还有一个特例是一般大对象直接在老年代分配,避免大对象在各个区域间来回拷贝,造成性能损失,不过最好的方法是不要new过多的大对象。
永久代(Permanent Gen)
在很多地方我们还能看到永久代的说法,其实就是JVM内存分区里的方法区,HotSpot在1.7以前把方法区和堆放在一起做垃圾收集的,所以方法区又叫永久代。主要存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是现如今,例如Spring或者JSP都大量利用反射,动态代理,CGLib生成大量的Class,这时候我们需要设置一个比较大的永久代空间,防止方法区发生内存溢出。至于1.8已经使用Metaspace了。
上面说的是分代垃圾收集的思想,但是有个经常提到却还没有解答的问题,我们在每一次GC都会保留存活的对象,那么如何判断出哪些对象时存活的,哪些对象又是要清理的呢?这也是我们一开始提的问题中的一个,即垃圾收集回收什么对象?
对象判活算法
引用计数法
顾名思义,引用计数法就是在每一个对象上绑定一个计数器,当有一个地方引用该对象时,引用计数值就加1,引用失效时,计数值就会减1,当计数值为0时,说明对象不再被使用,这时候就可以看作无效对象了。就我所知C++的智能指针和Objective C中ARC都利用了引用计数,有关智能指针可以参考我的C++11 智能指针。
但是在Java虚拟机的实现中并没有采用引用计数法,其核心原因就是因为对象间的相互循环引用
class C{
public Object x;
}
C obj1、obj2 = new C();
obj1.x = obj2;
obj2.x = obj1;
obj1、obj2 = null;
obj1和obj2相互持有对方的引用,所以GC收集器无法回收它们。
可达性分析算法
在主流的支持GC的语言中,都是通过可达性分析来判断对象是否存活的。算法思想就是通过一系列的GC Roots作为起始点,然后往下搜索,能够连接到GC Roots的,都证明还是活的,如果断链子了,则证明不可达,也就是对象不是存活的。
如图所示,蓝色的全部能够直接或者间接的链接到GC Roots,Obj6、Obj7、Obj8虽然彼此相连,但是无法链接到GC Roots,所以他们都是不可达的,将会被 判定为可回收的。
那么哪些对象会成为GC Roots呢,可以分为以下几种:
- 虚拟机栈(栈帧的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
知道什么对象需要回收,上面也说了分代回收的思想,那么具体在回收的时候,内存是怎么做的呢?那就不得不整理一下Java垃圾回收的常见算法。
垃圾回收算法
标记清除算法
标记清除即Mark-Sweep,是一种最简单的收集算法。在经历过对象判活以后,我们把需要回收的对象标记出来,然后在统一时刻回收所有被标记的对象。如图所示:
黑色标记的可回收对象在回收后全部变成未使用空间,但是这样回收后有木有发现空间碎片很多,碎片太多就会导致再分配稍微大点的空间时,找不到这样的连续内存,从而导致GC会被频繁调用,所以标记清除是一种基础的垃圾收集算法,其它算法基本都是以它为基础优化产生。
复制算法
复制算法的思想就是把内存分为两块,每次只在一边分配内存,当一边的内存用完了,就把所有还存活的对象复制到另一半去,这时候把原来使用过的这一边的所有空间一次性清理掉,所以也就不存在内存碎片的问题了,基本思路如图:
其实前面提到的分代GC算法在新生代区域就用了复制算法,并且也没有分成1:1,而是8:1,也就是所谓的Eden区和survivor区,大多数对象都是“朝生夕死”的,所以在minorGC时,只把存活下来的对象全部复制到survivor区,具体的赋值过程前文中也提到过,在此不再复述。
标记整理算法
上面提到的赋值算法也有它的弱点,就是当对象存活率很高的时候,就会存在很多的复制操作,从而影响了效率。所以这种算法运用在老年代的话很明显不合适,于是又有了标记整理算法,这种算法的主要思路就是把活跃对象标记出来,之后再向内存的一侧移动,然后直接清理掉端边界以外的内存,具体思路如下:
因为老年代需要清理的对象比较少,所以这种移动也会比较少。
分代收集算法
分代收集算法是前面提到的算法的综合之作,当前的商业虚拟机的垃圾收集都采用分代收集,一般新生代采用复制算法,老年代采用“标记-清理”或者“标记-整理”。
知道这么所垃圾收集算法,它们的实现也是繁多的,这里介绍几种主流实现,很多都是屹立多年的经典垃圾收集器了,当然新的收集器一直不断的在被开发中。
垃圾收集器
目前看主流的垃圾收集器也就下面这些,其中Serial、ParNew、Parallel Scavenge主要应用在新生代,CMS、SerialOld、Parallel Old主要应用在老年代,而G1的回收范围是整个Java堆(包括新生代和老年代)。有连线的说明彼此之间可以结合使用。
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
有空再整理每个垃圾收集器具体的实现。