Java虚拟机(二):垃圾回收算法

一、如何确定垃圾?

  在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中那些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)了。

1. 引用计数法:

  •  在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关

联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。

  • 使用者有微软COM(Component Object Model)技术、使用ActionScript3的FlashPlayer、Python语言以及在游戏脚本领域得到许多应用的Squirrel;
  • 但在Java领域,至少主流的Java虚拟机里面都没有选用引用计算法来管理内存。

实现:

  1> 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;

  2> 当引用失效时,引用计数器就减1;

  3> 只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

缺点:

  • 引用和去引用伴随加法和减法,影响性能
  • 很难解决对象之间相互循环引用的问题。

2. 可达性分析:

  • 为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC  roots”对象作为起点搜索。如果在“GC  roots”和一个对象之间没有可达路径,则称该对象是不可达的。
  • 要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

 

3. 四种引用类型:

  • 强引用:是指在程序代码中普通存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2版之后提供了SoftReference类来实现软引用。
  • 弱引用:也是用来描述那些非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用。
  • 虚引用:也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。虚引用完全不会对一个对象的生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象呗收集器回收时收到一个系统通知。在JDK1.2版之后提供了PhantomReference类来实现虚引用。

二、垃圾收集算法

1. 分代收集理论

  当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

    1) 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。

    2) 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

  这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

  在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”、“Major GC”、“Full GC”这样的回收类型 的划分;也才能针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记——清除算法”、“标记——整理算法”等针对性的垃圾收集算法。

2. 标记-清除(Mark-Sweep)算法

  • 标记-清除算法是现代垃圾回收算法的思想基础。
  • 标记-清除算法将垃圾回收分为两个阶段:标记和清除。
  • 实现:
    • 在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。
    • 在清除阶段,清除所有未被标记的对象。

具体过程如下图所示:

 

缺点:

  1)执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;

  2) 会产生大量不连续的内存碎片。可能会导致在需要分配较大对象时,无法找到足够的连续内存,而不得不提前进行另一次的垃圾收集动作。

3. 标记-复制算法

  • 与标记-清除算法相比,复制算法是一种相对高效的回收方法
  • 不适用于存活对象较多的场合 如老年代
  • 将原有的内存空间分为两块,每次只使用其中一块
    • 在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,
    • 之后,清除正在使用的内存块中的所有对象,
    • 最后,交换两个内存的角色,完成垃圾回收

具体过程如下图所示:

 

缺点:

  • 空间浪费,内存缩小为原来的一半。

 

4. 标记-整理算法

  • 标记-整理算法适合用于存活对象较多的场合,如老年代。
  • 它在标记-清除算法的基础上做了一些优化。
    • 和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。
    • 但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。
    • 最后,清理边界外所有的空间。

具体过程如下图所示:

 

三、Stop-The-World

  • Java中一种全局暂停的现象
  • 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
  • 多半由于GC引起
    •   Dump线程
    •   死锁检查
    •   堆Dump

GC时为什么会有全局停顿?
  类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。

危害:

  • 长时间服务停止,没有响应
  • 遇到HA系统,可能引起主备切换,严重危害生产环境。
posted @ 2018-06-03 00:06  时间-海  阅读(335)  评论(0编辑  收藏  举报