阅读 17

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

3.2 对象已死吗

3.2.1 引用计数算法

  • 给对象中添加一个引用计数器,
    • 每当有一个地方引用它时,计数器值就加1;
    • 当引用失效时,计数器值就减1;
    • 任何时刻计数器都为0的对象就是不可能再被使用的
  • 缺点:很难解决对象之间相互循坏的问题

3.2.2 可达性分析

  • 根搜索算法:通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
  • 在Java语言里,可作为GC Roots的对象包括下面几种:
    • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
    • 方法区中的类静态属性引用的对象。
    • 方法区中的常量引用对象。
    • 本地方法栈中的JNI的引用的对象。

3.2.3 再谈引用(四种引用)

  • 强引用
    • 类似于Obejct obj = new Obejct()这类的引用
    • 只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用
    • 用来描述一些还有用,但并非必要的对象。
    • 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围并进行二次回收。
    • 可以使用SoftReference类来实现软引用。
  • 弱引用
    • 比软引用更弱一些,被弱引用关联的对象只能生产到下一次垃圾收集发生之前。
    • 当垃圾收集器工作室,无论内存是否足够,都会回收弱引用关联的对象
    • 可以使用WeakReference类来实现弱引用
  • 虚引用
    • 也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。
    • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
    • 为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。
    • 可以使用PhantomReference类来实现虚引用。

3.2.4 生成还是死亡

  • 要真正宣告一个对象死亡,需要两个标记过程:
    • 若对象在进行可达性分析后发现没有与GC Roots相连接的引用链,会被第一次标记并筛选。筛选的条件是此对象是否有必要执行finalize()方法
      • 当对象没有覆盖finalize()方法或者finalize()方法已经由虚拟机调用过,都视为finalize()没有必要执行
    • finalize()方法是对象逃脱死亡的最后一次机会,稍后GC会对对象进行第二次标记
      • 这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
  • 原书作者不建议大家这样使用

3.2.5 回收方法区

  • 不只是Java堆,方法区也有垃圾收集机制
  • 方法区(永久代)的垃圾收集主要回收两种内容:废弃常量和无用的类
  • 回收废弃常量与Java堆的对象类似,即该常量没有在任何地方被引用
  • 类需要同时满足一下三个条件才能算是“无用的类”:
    • 该类所有的实例都已经被回收,即Java堆中不再有该类的实例
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被调用,无法在任何地方通过反射访问该类的方法
  • 在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

3.3 垃圾收集算法

3.3.1 标记-清除算法

  • 算法分为标记和清除两个阶段,首先标记出所有需要回收的对象,然后回收所有已标记的对象。

  • 这个算法主要有两个缺点:

    • 一个是效率不高,标记和清除的效率都不高;
    • 二是标记清除之后会产生大量不连续的碎片。分配较大对象时,无法找到足够的连续内存空间,会提前触发GC

    img

3.3.2 复制算法

  • 把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 优点:内存分配时不用考虑内存碎片等复杂情况,实现简单,运行高效。
  • 缺点:这种算法的代价是将内存缩小为原来的一半,利用率低。
  • 新生代的GC使用复制算法
  • 因为新生代98%的对象都是“朝生夕死”,所以不需要1:1来划分内存。而是划分一块较大的Eden空间和两块较小的Survivor空间
  • HotSpot虚拟机默认的Eden和Survivor比例是8:1,只有10%的内存被“浪费”
  • 如果另一块Survivor空间没有足够空间存放上一次新生代手机下来的对象,对象将通过分配担保知己进入老年代

img

3.3.3 标记-整理算法

  • 复制算法在对象存活率高的情况(比如老年代)要进行较多的复制操作,效率低。
  • 标记过程仍然与“标记——清除”算法一样,后续步骤是将所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存

img

3.3.4 分代收集算法

  • 根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代
  • 新生代:每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 老年代:因为对象存活率高、没有额外空间对它进行分配担保,就必须使用**“标记——清除”“标记——整理”**算法来进行回收。

3.4 HotSpot的算法实现

3.4.1 枚举根节点

  • GC链逐个检查引用,会消耗比较多时间

  • GC停顿,为了保持“一致性”,需要“Stop the world”

  • 目前主流的Java虚拟机使用的都是准确式GC,当执行系统停顿下来后并不需要一个不漏的检查完所有执行上下文和全局的引用变量,虚拟机应当有办法直接得知哪些地方存着对象的引用

  • HotSpot使用一组称为OopMap的数据结构来记录哪些地方存着对象的引用

  • 在类加载过程中,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中会在特定的位置记录下栈和寄存器中哪些位置是引用

3.4.2 安全点

  • HotSpot没有为每条指令都生成OopMap,只是在特定位置记录了这些信息,这些位置称为安全点
  • 程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停
  • 给多线程记录安全点时有两种方案:抢先式中断 (几乎不再使用)和 主动式中断
  • 主动式中断的思想是当GC需要中断线程时不直接对线程进行操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起

3.4.3 安全区域

  • 为安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域内的任何地方进行GC都是安全的。可以看成是扩展的安全点

3.5 垃圾收集器

3.5.1 Serial收集器

  • Serail收集器是“单线程”的,他在进行垃圾收集时必须暂停其他的所有线程。“Stop the world ”
  • 对于单个CPU坏境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集,可以获得很高的单线程收集效率

3.5.2 ParNew收集器

  • ParNew收集器是Serial收集器的多线程版本
  • ParNew收集器能与CMS收集器配合工作
  • 在垃圾收集器中“并发”与“并行”的概念:
    • 并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
    • 并发:用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上

3.5.3 Parallel Scavenge收集器

  • Parallel Scavenge收集器是一个新生代收集器,采用复制算法
  • Parallel Scavenge收集器的特点是他的关注点与其他收集器不同。其他收集器的目标是尽可能的缩短用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控的吞吐量
  • 高吞吐量可以高效的利用CPU时间,尽快得完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
  • GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的
  • Parallel Scavenge收集器拥有自适应调节策略,在对收集器不太了解,手工优化困难的时候可以使用

3.5.4 Serail Old收集器

  • Serial Old收集器是Serail收集器的老年代版本
  • Serail Old收集器主要用于Clinet模式下
  • Serail Old收集器另一种用途是作为CMS收集器的后备预案

3.5.5 Parallel Old收集器

  • Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法

3.5.6 CMS收集器

  • CMS收集器是一种以获取最短的回收停顿时间为目标的收集器
  • CMS收集器基于标记-清除算法实现,分为四个步骤:初始标记、并发标记、重新标记、并发清除
  • 步骤详解:
    • 初始标记:标记一下GC Roots能直接关联到的对象,速度很快
    • 并发标记:进行GC Roots Tracing
    • 重新标记:是为了修正那些在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,在这一阶段的停顿时间会比初始标记阶段稍长一点
  • CMS收集器的缺点:
    • CMS收集器对CPU资源非常敏感,面向并发设计的程序都对CPU资源敏感
    • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full FC的产生
    • 由于CMS收集器采用了标记-清除算法,所以在回收结束时会有大量空间碎片产生,碎片过多时,在给大对象分配内存时会有很大麻烦

3.5.7 G1收集器

  • G1收集器是一款面向服务端应用的垃圾收集器
  • G1收集器具备以下特点:
    • 并行与并发,缩短停顿时间
    • 分代收集
    • 空间整合:从整体上来看是基于“标记-整理”算法实现的,在局部上是基于复制算法实现的
    • 可预测的停顿
  • G1收集器将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老生代的概念,但新生代和老生代不再是物理隔的了,他们是一部分Region的集合
  • G1收集器可以有计划地避免在整个Java堆中进行全区域的垃圾收集:跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
  • 在G1收集器中,使用Remembered Set来避免全堆扫描,提高效率

3.6 内存分配与回收策略

3.6.1 对象优先分配在Eden分配

  • 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

3.6.2 大对象直接进入老年代

  • 所谓大对象就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。

3.6.3 长期存活的对象将进入老年代

  • 虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁
  • 当它的年龄增加到一定程度(默认为15岁)时,就会晋升到老年代中。对象晋升老年代的年龄阀值,可以通过参数-XX:MaxTenuringThreshold来设置。

3.6.4 动态对象年龄判定

  • 虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于改年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold要求的年龄。

3.6.5空间分配担保

  • 在发送Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否小于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC
关注下面的标签,发现更多相似文章
评论