一、前言
明确垃圾收集器关注的部分:堆和方法区。着重学习如何确定哪些垃圾需要回收、垃圾回收算法以及GC触发条件。
二、如何确定哪些垃圾需要回收
1、引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
2、可达性分析算法
当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain)。如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
在虚拟机栈(栈帧中的本地变量表)中引用的对象;在方法区中类静态属性引用的对象;在方法区中常量引用的对象;在本地方法栈中JNI(即通常所说的Native方法)引用的对象;
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。譬如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。
注:即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓 刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
三、四种引用类型
1、强引用
强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
2、软引用
软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
3、弱引用
弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
4、虚引用
虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用。
四、垃圾收集算法
1、标记-清除算法(Mark-Sweep)
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
缺点:(1)面对大量可回收对象时执行效率低;
(2)标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
2、标记-复制算法(Copying)
标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低 的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存 活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复 制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有 空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷 也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一 点。
缺点:可用内存变少,且如果存活对象较多,则复制的效率会大大降低
3、标记-整理算法(Mark-Compact)
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为“标记-整理算法”。标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。
4、分代收集算法(Generational Collection)
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活周期的不同将内存划分为几块。根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。将Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域。新生代又分为Eden区、From Survivor和To Survivor。
(1)新生代
新创建的对象基本都会存放在Eden区(大对象直接放在老年代),而这部分对象大部分都会“朝生暮死”,使用后被快速回收。常规一次回收可回收70%-95%的空间,效率非常高。
新生代采用复制算法进行回收,新建对象总是在 eden 区中被创建,当 eden 区空间已满,就触发一次 Minor GC,将还被使用的对象复制到 s0 区,这样整个 eden 区都是未被使用的空间,可供继续创建对象。
当 eden 再次用完,再触发一次 Minor GC,Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间(包括垃圾对象),这个步骤称为“空间分配担保”;
Minor GC时,如果 s1 区能够容纳,则使用复制算法将 eden 区还在被使用的对象复制到 s1 区,s0 区还在被使用的对象会根据他们的年龄值来决定去向,年龄达到某个值 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 )的对象就会进入老年代。没有达到阈值的会被复制到 s1 区,然后清除所使用的 eden 区和 s0 区,并将这些对象的年龄设置为1,s1 与 s0 的角色互换(保证名为To的Survivor区域是空的),以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄+1,不过对于一些较大的对象则是直接进入老年代;如果s1区不能容纳,则多余的部分对象存入老年代;
(2)老年代
新生代与老年代默认比例为1:2,老年代用来存放存活时间较长,但还是会死的对象信息(如缓存对象、单例对象等)。老年代采用标记-整理算法进行回收。老年代对象来源有以下几个方向:
①大部分来自于新生代,对象在新生代存活时间过阀值,就会被复制到老年代。
②新生代中部分对象虽然未过阀值,但是因为survivor区已满,由担保机制复制到老年代。
③部分大对象直接在老年代创建,不经历新生代,如长字符串、长数组等需要大量连续空间的对象。
五、GC什么时候触发
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GC和Full GC。
1、Minor GC(普通GC)
Minor GC触发条件:Eden区满时。
2、Full GC
Full GC触发条件:
(1)调用System.gc() 时,系统建议执行Full GC,但是不必然执行;
(2)老年代空间不足;
(3)方法区(持久代)空间不足;
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小;
六、什么是空间担保机制
在发生 Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次Minor GC是安全的。如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
七、为什么要进行空间担保?
是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。
七、参考
1、《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》周志明著
2、JVM垃圾回收机制 - 、、、、、、、 - 博客园
3、【JVM】空间分配担保机制 - 听风是雨 - 博客园