深入理解jvm之垃圾收集器

判断对象是否存活

可达性分析算法

通过一系列称为"GC Roots"的对象作为起点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(从GC Roots到此对象不可达),则证明此对象是不可用的。
可作为GC Roots的对象包括:

  • 虚拟机栈中所引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

引用的分类

  • 强引用(Strong Reference): 在代码中普遍存在的,类似"Object obj = new Object()"这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用(Sofe Reference): 有用但并非必须的对象,可用SoftReference类来实现软引用,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存异常异常。
  • 弱引用(Weak Reference): 被弱引用关联的对象只能生存到下一次垃圾收集发生之前,JDK提供了WeakReference类来实现弱引用
  • 虚引用(Phantom Reference):也称为幽灵引用或幻影引用,是最弱的一种引用关系,JDK提供了PhantomReference类来实现虚引用。

不要使用finalize()方法来挽救对象。

JVM判定无用的类的条件:

  • 该类的所有实例已经被回收,java堆中不存在该类的任何示例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用

垃圾收集算法

标记-清除算法

即先标记所有需要回收的对象,在标记完成后统一进行回收。该算法的两个不足:一个是效率问题,标记和清除两个过程效率不高;另一个是空间问题,标记清除后产生大量不连续的内存碎片,空间碎片太多导致为较大对象分配内存时,找不到足够大的连续内存。

复制算法

将可用内存按容量划分为大小相对的两块,每次只使用其中一块,当这一块内存使用完,将还存活的对象复制到另一块,然后将已使用过的内存一次清理掉,很明显这种方式虽然解决了内存碎片问题,但是可用内存缩小为原来的一半,太浪费了。现在大部分的虚拟机都使用这种方式来回收新生代,即Eden区与两个Survivor区,默认Eden:一个Survivor=8:1

标记-复制算法

标记过程如标记-清除算法一样,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

垃圾收集器

这里主要列举现在常用的收集器

CMS收集器

现在常用的一种垃圾收集器,基于标记-清除算法,收集过程包含4个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC Roots Trancing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动那一部分对象的标记记录,这个阶段的停顿时间比初始标记稍长一些,但远比并发标记时间短。
由于整个过程耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以从整体上看,CMS收集器的内存回收过程是与用户线程并发执行的。

CMS收集器的缺点:

  • 对CPU资源敏感 虽然不会导致用户线程停顿,但会占用一部分线程导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4
  • 无法处理浮动垃圾 可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。浮动垃圾是回收的过程与用户线程并行时用户线程产生的垃圾。
  • 产生内存碎片 使用标记-清除算法

G1收集器

一款面向服务端应用的比较新的垃圾收集器,具备以下特点:

  • 并行与并发 G1能够充分利用多CPU、多核环境下的硬件优势,使用多个CPU核心来缩短Stop The World的停顿时间
  • 分代收集
  • 空间整合 同时使用标记-整理与复制算法不会产生内存空间碎片
  • 可预测的停顿 能让使用者指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

G1的垃圾收集步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

内存分配与回收策略

  1. 对象优先在Eden分配,当Eden没有足够的空间时,虚拟机发起一次Minor GC。可以通过JVM参数:-XX:+PrintGCDetails打印GC日志查看垃圾收集情况。

    示例:
    JVM Args: -Xms20m -Xmx20m -Xmn10m
    堆大小共20m,其中新生代10m,老年代10m,默认-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1

  2. 大对象直接进入老年代,如长字符串及数组,jvm提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配内存,避免在Eden与Survivor之间发生大量内存复制。

  3. 长期存活的对象进入老年代 虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳,将被移动到Survivor空间,并且对象年龄设为1,对象在Survivor空间每熬过一次Minor GC,年龄就增加1岁,当年龄到达一定长度(默认15),将会被晋升都老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到PretenureSizeThreshold中要求的年龄。

  4. 空间分配担保,在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果是则Minor GC是安全的。如果不是虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于或者HandlePromotionFailure设置不允许冒险,那这是要进行一次Full GC。因为在极端的情况下,即新生代所有对象都存活,就需要把Survivor无法容纳的对象直接放入老年代。

参考

《深入理解java虚拟机 JVM高级特性与最佳实践》

你可能感兴趣的:(深入理解jvm之垃圾收集器)