JAVA虚拟机–垃圾收集

前言

Java的自动管理主要是针对对象内存的回收和对象内存的分配。同时Java自动内存管理最核心的功能是内存中对象的分配与回收(Java堆是垃圾收集器管理的最主要区域)

如何判定对象死亡

  1. 引用计数法

    给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

    这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

  2. 可达性分析法

    这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

    不可达不一定是非死不可的,真正宣告对象死亡至少要经过 两次标记。第一次标记且进行一次筛选(此对象是否有必要执行finalize方法,当对象没有覆盖finalize方法,或finalize方法以及被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行;被判断需要执行的对象将会被房子一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收)

如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类”

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

垃圾收集算法

  1. 标记-清除算法(Mark-Sweep):分标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。为什么叫它是最基础的算法:是因为后续的收集算法都是基于这种思路,对其不足进行改进的

    不足主要有两个:一是效率问题,标记和清除两个过程的效率都不高另一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

  2. 复制算法:将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的着的对象复制到另外一块上面,然后再将已经使用过的内存空间一次清理掉。

    这样使得每次是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了之前的一半,代价太高了,不足点还有,在对象存活率较高时就要进行较多的复制操作,效率将会降低,如果不想浪费一半的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以老年代一般选用第三种算法。

    现在商业虚拟机用这种算法来回收新生代

  3. 标记-整理算法(Mark-Compact):标记过程与标记-整理算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  4. 分代收集算法(Generational Collection):不算新的算法,只是根据对象存活周期的不同将内存划分为几块。一般就是java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的算法

    比如,新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集

    老年代中因为对象存活率高,没有额外的空间对他进行分配担保,只能使用标记-清理或者标记-整理算法来进行回收。

垃圾收集器

  1. Serial收集器

    • 单线程收集器,工作时必须暂停其他工作线程(Stop the World)
    • 依然是虚拟机运行在客户端模式下的默认新生代收集器
    • 简单高效(相比其他收集器的单程相比)
  2. ParNew收集器

    • Serial的多线程版本,控制参数,收集算法,stop the world,对象分配规则,回收策略与其一样
    • 虚拟机运行在服务器模式下的首选新生代收集器
    • 在单CPU中不比Serial收集器效果好,甚至还有线程交互的开销
  3. Parallel Scavenge收集器

    • 新生代收集器,复制算法,多线程
    • 关注点和ParNew不一样,目的是达到一个可控制的吞吐量,也被称为“吞吐量优先”收集器
    • 两个参数用于精确控制吞吐量:-XX:MaxGCPauseMillis参数用于控制最大垃圾收集停顿时间(缩短该时间是以牺牲吞吐量和新生代空间来换取的);-XX:GCTimeRatio直接设置吞吐量大小的
    • 还有个参数-XX:+UseAdaptiveSizePolicy,当该参数打开后,不需要手工指定新生代大小,Eden与存活区的比例,虚拟机会根据当前系统的运行系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。这个是这种收集器与ParNew收集器的一个重要区别
  4. Serial Old收集器

    • Serial收集器的老年代版本,也是单线程收集器,使用标记-整理算法,主要意义也是在于给客户端模式下的虚拟机使用。
    • 如果在服务器模式下,其还有两大用途:一个是在JDK1.5以前的版本与Parallel Scavenge收集器搭配使用,二是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
  5. Parallel Old收集器

    • 时Parallel Scavenge收集器的老年版本,多线程,使用标记-整理算法,在JDK1.6版本后提供
  6. CMS收集器(ConCurrent Mark Sweep)

    • 是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,它的运作过程相对前面几种收集器来说更复杂一些

    • 运作四个过程:

      • 初始标记:仅标记一下GC Roots能直接关联到的对象,速度很快
      • 并发标记:进行GC Roots Tracking的过程
      • 重新标记:为了修正并发标记期间因用户继续运作而导致标记产生变动的那一部分对象的标记记录,会比初始标记时间长点,但远比并发标记时间短
      • 并发清除

      前两个步骤仍然需要“Stop the World”

    • 三个明显缺点:

      • CMS收集器堆CPU资源非常敏感
      • 无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生
      • 标记-清除算法会结束时产生大量空间碎片,碎片过多时,会给大对象分配带来很大麻烦,即使老年区有很大的内存剩余,但是依然没法分配,不得不提前进行一次Full GC
  7. G1收集器( Garbage First )

    • 面向服务端应用的垃圾收集器,目的是替换掉JDK1.5中发布的CMS收集器
    • 有以下特点:
      • 多线程,并行与并发
      • 分代收集
      • 空间整合:整体上是标记-整理,局部是复制
      • 可预测的停顿:这是相对于CMS的另一个大优势