深入理解 Java 虚拟机读书笔记2

垃圾回收

通常垃圾回收是针对 Java 堆和方法区所做的操作,其他部分由于线程私有并且本身所占空间不大不用太关心,垃圾回收器要做的三件事:

  • 哪些内存需要回收
    在 Java 堆中,肯定是对那些不再被引用的对象实例进行回收,而如何判断对象不再被任何地方引用是个关键。通常有两种方式:

    1. 引用计数法
      实现简单,高效,但存在着相互循环引用的问题。
    2. 可达性分析
      会有一个起始点,从该点出发进行搜索,能走到的对象就是可达的,也就是有效的引用,否则就是无效可回收的。
      在 Java 中能作为起始点的有:Java 虚拟机栈的本地变量表中的引用对象,方法区中类静态属性引用对象,方法区中常量引用对象,本地方法栈中 JNI 引用对象。

    在方法区中,主要回收废弃常量和无用的类。废弃常量的确定和确定 Java 堆中不再引用的对象实例类似。对于无用的类,则要满足 3 个条件才会被回收:

    1. 该类所有实例都已被回收
    2. 加载该类的 ClassLoader 已被回收
    3. 该类对应的 java.lang.Class 对象没有被任何地方引用,无法通过反射访问该类的任何方法。

    虽说只要没有引用就会被回收,那怎么做到完全没有引用呢?Java 通过划分引用类型,控制着对象引用的不同。

    • 强引用(Strong Reference)
      默认的引用方式,通常用的也最多,回收器永远不会回收这类引用。
    • 软引用(Soft Reference)
      介于强引用和弱引用之间,在内存不足时会优先考虑。这种方式我觉得适用于 MVP 中的 V 层。
    • 弱引用(Weak Reference)
      引用对象只能生存到下次垃圾回收之前,不论内存是否足够,只要触发了垃圾回收,他们就会被回收掉。
    • 虚引用(Phantom Reference)
      用处不大。
  • 什么时候回收
    对象真正可回收需要经过两次标记。第一次是引用计数或可达性分析,第二次是对象如果有必要执行 finalize() 方法会进行第二次标记。如果在执行 finalize() 时没有救活自己,就准备被回收了。

  • 怎么回收
    几种基本的回收算法

    • 标记-清除算法
      算法分为两个步骤,标记和清除,标记的过程就如上面所说,标记完之后,就会对标记区进行内存回收,这样内存空间就出来了。
    • 复制算法
      该算法是针对「标记-清除算法」效率低的问题进行的优化,将内存空间对半,每次仅用一半,当用完一半时,就将活着的对象复制到另一半去,然后回收这一半。
      不过这是算法理论,实际上 IBM 将内存划分为 Eden 和 Survivor(该区域有两块),每次用 Eden 和其中一块 Survivor,另一块用来在回收时保存活着的对象。而HotSpot 虚拟机明确了 Eden 和 Survivor 的比例,8 : 1,即新生代的有效可用空间为 90 %,剩下 10 % 的空间用来保存对象。
      但是 10 % 真的一定够吗?不够了怎么办,系统的设计是让老年代来做担保(类似于透支)
    • 标记-整理算法
      该算法又考虑到「复制算法」在存活率较高的情况下,效率较低,而且很有可能出现担保现象的问题,对此进行了优化。这种算法更适合老年代(不然老年代找谁担保)。
      这个算法分三个步骤,标记,整理,回收。标记过程和前面类似,标记完之后,会对存活的对象进行位置整理,使他们整齐排在一起,然后回收其他部分。

    Java 堆根据对象存活周期的不同,划分为了新生代和老年代,不同代采用不同回收算法是比较合适的。通常新生代用「复制算法」,老年代用「标记-整理算法」或「标记-清除算法」

    HotSpot 的算法实现

    具体的算法实现肯定很复杂,这里仅引入几个概念。

    在做可达性分析时会存在耗时和线程停顿执行的问题。为了解决这两个问题,引入了 OopMap 的数据结构,在类加载完时就存储好对象引用链相关的位置引用,这就大大提高了效率。

    但 OopMap 不会都记下来,只有在「安全点」的时候才会存,「安全点」在我的理解是指线程执行指令到了某个逻辑划分的小任务完结点,此时存储一些数据是比较合适,并且这个时候做 GC 操作也是合适的。

    那如何让线程走到「安全点」是个问题。系统采用「抢断式中断」和「主动式中断」对线程进行控制,现在主要以「主动式中断」为主,即在将要 GC 时,设置一个标记,各线程会主动查询这个标记,如存在这个标记,就把自己挂起。

    「安全点」对醒着的线程是可靠的,但对于那些睡着的或者阻塞的线程,他们没办法知道那个标记点,于是设立一个「安全区域」扩大安全范围,线程进入「安全区域」时标记一下,当要出去时先看下外面安不安全,安全的话才能出去,否则就要等待可以出去的信号。

你可能感兴趣的:(深入理解 Java 虚拟机读书笔记2)