对象存活判定算法

1.引用计数法:
(1)概念:给对象添加一个计数器,每当一个地方引用该对象时,计数器加1;当引用失效时,计数器减1;当计数器的值为0时,表明该对象不可用了!
(2)优缺点:
优点:实现简单,判定效率高。
缺点:很难解决对象间循环引用的问题!(Java虚拟机没有选用引用计数法来管理内存的主要原因)

例子:/** * testGC()方法执行后,objA和objB会不会被GC呢? */
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.instance = objB;
objB.instance = objA;

objA = null;
objB = null;

// 假设在这行发生GC,objA和objB是否能被回收?
System.gc();
}
}

如果JVM采用引用计数算法来管理内存,这两个对象不可能再被访问,但是他们互相引用着对方,导致它们引用计数不为0,所以引用计数器无法通知GC收集器回收它们。

2.可达性分析算法

(1)概念:当一个对象,没有挂在GC Roots对象上,或者没有在引用链将该对象和GC Roots连接起来,那么该对象就会被判定为是可回收 的对象。

JVM 深入理解JVM——GC算法与内存分配策略_引用计数


ps: Java中,可作为GC Roots的对象:

  • 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

看到这里你可能要问,选择这些对象的依据是什么呢?

可以概括得出,可作为GC Roots的节点主要在全局性的引用与执行上下文中。要明确的是,tracing gc必须以当前存活的对象集为Roots,因此必须选取确定存活的引用类型对象。GC管理的区域是Java堆,虚拟机栈、方法区和本地方法栈不被GC所管理,因此选用这些区域内引用的对象作为GC Roots,是不会被GC所回收的。其中虚拟机栈和本地方法栈都是线程私有的内存区域,只要线程没有终止,就能确保它们中引用的对象的存活。而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活,因此,也可能是GC roots的一部分。

(2)注意事项:
可达性分析算法判断为可以回收的对象只是判了缓刑。

​ 两次标记过程:
第一次:可达性分析法判断之后没有与GC Roots 相连接的引用链,那么将会进行第一次标记,并且进行一次筛选。如果这个对象被判定为需要执行finalize()方法,那么这个对象将会被放在 F - Queue的队列之中。
第二次:GC 将会对F-Queue之中的对象进行第二次小规模的标记,还没有逃脱那么将会回收。

finaliza()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue 队列中的对象进行第二次小规模的标记。如果对象想在finaliza()方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,例如把自己(this关键字)赋值给某个类变量或者对象的成员变量,这样在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,基本上它就真的被回收了。

垃圾收集算法

1、Mark-Sweep(标记-清除算法):

(1)思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。

(2)缺点:

​ a.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。

b. 效率问题,因为内存碎片的存在,操作会变得更加费时,因为查找下一个可用空闲块已不再是一个简单操作。

JVM 深入理解JVM——GC算法与内存分配策略_引用计数_02

2、Copying(复制清除算法):

(1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。

(2)优缺点:这样做使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效只是这种算法的代价是将内存缩小为原来的一半,代价可能过高了

JVM 深入理解JVM——GC算法与内存分配策略_老年代_03

(3)改良:因为新生代中的对象98%都是”朝生夕死“,所以可以将内存分为一块较大的Eden,和两块较小的Survivor空间。

Eden : Survivor = 8 : 1

每次回收的时候,将Eden 和 一块Survivor 中还存活的对象,一次性的复制到另一块Survivor之中。这样只会浪费10%,但是无法保证每一次只有不多于10%的对象存活,因此还需要内存担保

ps:空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

3、Mark-Compact(标记-整理算法):

(1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。

(2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下

JVM 深入理解JVM——GC算法与内存分配策略_引用计数_04

4、分代收集算法:(目前大部分JVM的垃圾收集器所采用的算法):

思想:把堆分成新生代和老年代。(永久代指的是方法区)

新生代:每次都有大量的对象死去,只有少量存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

老年代:**对象存活率高,**没有额外的空间进行空间担保,采用标记—清除,或者采用标记—整理算法。

内存分配策略

Minor GC 和 Full GC
Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。

Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
内存分配策略

  1. 对象优先在 Eden 分配
    大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
  2. 大对象直接进入老年代
    大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

  1. 长期存活的对象进入老年代
    为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

  1. 动态对象年龄判定
    虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
  2. 空间分配担保
    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

Full GC 的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

  1. 调用 System.gc()
    只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
  2. 老年代空间不足
    老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

  1. 空间分配担保失败
    使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。
  2. JDK 1.7 及以前的永久代空间不足
    在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

  1. Concurrent Mode Failure
    执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。