《深入理解Java虚拟机》读书笔记——垃圾收集器与内存分配策略

时间:2023-01-02 12:50:49

一、判断对象是否存活的算法:

  1. 引用计数算法。
    • 给对象添加一个引用计数器,每一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器为0的对象就是不可能再被使用的。
    • 优缺点:实现简单,效率高;但很难解决对象之间相互循环引用的问题。
  2. 可达性分析算法
    • 通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
    • 可作为GC Roots的对象包括下面几种;
      • 虚拟机栈(栈帧中的本地变量表)中引用的对象
      • 方法区中类静态属性引用的对象
      • 方法区中常量引用的对象
      • 本地方法栈中JNI(即一般说的Native方法)引用的对象
    • 引用分类:
      • 强引用。在程序代码之中普遍存在的,类似“Object obj = new Object()”之类的引用;只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
      • 软引用。描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收,若回收之后还没有足够的内存,才会抛出内存溢出异常。
      • 弱应用。描述非必需对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
      • 虚引用(也称幽灵引用或幻影引用)。最弱的一种引用关系,不影响对象的生存时间,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
    • 软引用与弱引用的区别:
      • 只具有弱引用的对象拥有更短暂的生命周期,可能随时被回收。而只具有软引用的对象只有当内存不够的时候才被回收,在内存足够的时候,通常不被回收。

二、 回收对象过程

  1. 采用“缓刑”手段。
  2. 要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都是为“没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,则这个对象将会放置在一个F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。
  3. finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果还没有逃脱,那基本上它就死定了。

三、 回收方法区

  1. 永久代中的垃圾收集主要回收两部分:废弃常量和无用的类
  2. 判断无用的类(需同时满足下面三个条件):
    i. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
    ii. 加载该类的ClassLoader已经被回收
    iii. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

四、垃圾收集算法

  1. 标记-清除算法
    1. 分为“标记”和“清除”两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
    2. 不足:
      • 效率问题,标记和清除两个过程的效率都不高;
      • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够多的连续内存而不得不提前触发另一次垃圾收集动作。
  2. 复制算法
    1. 将可用容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
      1. 实现简单,运行高效,不用考虑内存碎片等问题,但是内存缩小为原来的一遍。
      2. 现在的商业虚拟机都采用这种收集算法来回收新生代,IBM研究表明,新生代中的对象98%是“朝生夕死”的,故将内存分为一块较大的Eden孔家和两块较小的Survivor空间,HotSpot虚拟机中为8:1.
      3. 当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。
  3. 标记-整理算法
    • 老年代使用的算法
    • 标记过程与“标记-清理”算法一样,清理过程是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  4. 分代收集算法
    • 当前商业虚拟机都采用的。将java堆分为新生代和老年代,新生代采用复制算法,老年代采用“标记-清理”或“标记-整理”算法

五、内存分配与回收策略

  1. Minor GChe Full GC的区别:
    1. 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快。
    2. 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parall Scavenge收集器的收集器策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
  2. 内存分配规则:
    1. 对象优先在Eden分配。
      • 大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
    2. 大对象直接进入老年代。
      • 大对象指需要大量连续内存空间的java对象,典型的就是那种很长的字符串以及数组。
    3. 长期存活的对象将进入老年代。
      • 每一次Minor GC,对象年龄加1,当增加到一定情况时(默认15)就可晋升到老年代中。
    4. 动态对象年龄判定。
      • 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
    5. 空间分配担保。
      • 在发生Minor GC之前,虚拟机会先先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若大于,则开始;否则,检查HandlePromotionFailure是否为true,若是,则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,尝试Minor GC(尽管有风险),否或设置为false,则进行Full GC。