第三章 垃圾收集器与内存分配策略(一)

3.2 对象已死?

如何确定哪些对象“存活”及“死亡”。

3.2.1 引用计数法

定义:在对象中添加一个引用计数器,每当有一个地方引用他,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

问题:无法解决循环引用

3.2.2 可达性分析

定义:通过一系列称为“ GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。

GC Roots包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中可用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

  • 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。

  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

  • 在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。

  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象
    (比如 NullPointExcepiton、OutOfMemory Brror) 等,还有系统类加载器

  • 所有被同步锁(synchronized关键字)持有的对象

  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

目前,最新的几款垃圾收集器无一例外都具备了局部回收的特征。

3.2.3 再谈引用

JDK1.2之后把引用细分为:强引用(日常使用的引用)、软引用、弱引用和虚引用。

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

3.2.4 生存还是死亡

在可达性分析中:

1.如果对象在进行分析后发现没有与GC Roots相连接的引用链,那么它将会被第一次标记

2.finalize()方法是对象逃离死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,重新建立则能活过来。

3.2.5 回收方法区

3.3 垃圾回收算法

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。

本书主要是追踪式垃圾收集

3.3.1 分代收集理论

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

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

3.跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
第三章 垃圾收集器与内存分配策略(一)_第1张图片

3.3.2 标记-清除算法

第三章 垃圾收集器与内存分配策略(一)_第2张图片
优点:

简单

缺点:

1.执行效率不稳定

2.内存空间的碎片化问题

3.3.3 标记-复制算法

第三章 垃圾收集器与内存分配策略(一)_第3张图片
优点:不会产生碎片

缺点:空间浪费太多
第三章 垃圾收集器与内存分配策略(一)_第4张图片

3.3.4 标记-整理算法

第三章 垃圾收集器与内存分配策略(一)_第5张图片
该算法在对象存活率较高时就要进行较多的复制操作,效率将降低。

具体做法:标记过程和标记-清除一样,但是不是直接对内存进行回收,而是让所有的存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。这样就会引起Stop-The-World

3.4 HotSpot的算法细节实现

3.4.1 根节点枚举

​ **寻找GCRoots节点,这一步都是必须暂停用户线程的,因此毫无疑问根节点枚举是有STW的。**但是查找引用链的过程可以与用户并发进行。

​ 查找节点也无需遍历所有的引用,使用了一组称为OopMap的数据结构达到了这个目的。

3.4.2 安全点

**1.作用:**用户程序到达安全点才能够进行垃圾回收。

​ 实际上 HotSpot 也的确没有为每条指令都生成 OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。

2.如何选择

是否具有让程序长时间执行的特征”为标准进行选定的

例如:方法调用、循环跳转、异常跳转等都属于指令序列复用,

3.如何在垃圾收集发生时让所有的线程停下来

1)抢占式中断

先中断,如果不能中断的线程再恢复这条线程,跑到安全点再中断

2)主动式中断

线程主动区轮询是否要休息

轮询点:

  1. 和安全点重合的点
  2. 创建对象和其他需要再Java堆上分配内存的地方

轮询操作已经优化至一条汇编指令的程度了

3.4.3 安全区域

​ 安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

3.4.4 记忆集与卡表

为了解决跨代引用产生的问题,引入记忆集。

记忆集是一种抽象的数据结构:

  • 字长精度: 每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的 32位或 64 位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。

  • 对象精度: 每个记录精确到一个对象,该对象里有字段含有跨代指针。

  • 卡精度: 每个记录精确到一块内存区域,该区域内有对象含有跨代指针

卡表

是记忆集的一个具体实现,定义了记忆集的记录精度与对内存的映射关系。

如何工作?

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为 1,称为这个元素变脏(Dirty),没有则标识为 0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入 GC Roots 中一并扫描。

3.4.5 写屏障

​ 卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏。

写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的 AOP 切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。
第三章 垃圾收集器与内存分配策略(一)_第6张图片
**开销:**每次只要对引用进行更新,就会产生额外的开销,不过这个开销与 Minor GC 时扫描整个老年代的代价相比还是低得多的。

3.4.6 并发的可达性分析(重要)

三色标记法

  • 白色: 表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

  • 黑色: 表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。

  • 灰色: 表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没
    有被扫描过。
    第三章 垃圾收集器与内存分配策略(一)_第7张图片
    当且仅大满足以下条件,会产生对象消失的问题,即原本是黑色的对象被误标为白色:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;

  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

解决这个问题的两种方案:

1.增量更新(Incremental Update)

黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

2.原始快照(Snapshot At TheBeginning, SATB)

无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

以上两种方案都是通过写屏障实现的。

你可能感兴趣的:(深入理解Java虚拟机,jvm)