JVM 知识点整理:GC垃圾收集器及相关算法

判断哪些对象需要回收

Java 堆里存放着几乎所有的对象实例,因此在回收前需要判断哪些对象是 “存活” 的,这些对象不需要回收,只回收已经 “死去” 的对象(即不可能再被任何途径使用的对象)。

引用计数器算法

算法原理:
给对象添加一个引用计数器,每当有一个地方引用它时,计算器 +1;当引用失效时,计数器 -1;任何时刻计数器为 0 的对象就是不可能再被使用的,可以被回收。

优点: 实现简单,判断效率高
缺点: 无法解决对象间相互引用的问题
应用: Python 语言,游戏脚本领域使用的 Squirrel 等

可达性分析算法

算法原理:
从一系列称为 “GC Roots” 的对象为起点,沿着引用链向下搜索,当一个对象到 GC Roots 没有任何引用链相连,则证明此对象可以被回收(即 GC Roots 无法到达此对象)。

GC Roots 的对象:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    对象在创建时候,会在堆上开辟一个空间用于分配实例,之后把堆的地址作为引用存放在中,在对象生命周期结束后,引用就会从虚拟机栈中出栈

  2. 方法区(永久代)中类静态属性引用的对象。
    即被 static 修饰的静态对象,存放在方法区

  3. 方法区(永久代)中常量引用的对象。
    即被 staticfinal 修饰的对象,存放在方法区

  4. 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
    JNIJava Native Interface,主要用于帮助 Java 与 别的语言进行通信(主要 C,C++)提供接口。

缺点: GC停顿,为了保证一致性,导致GC进行时必须停顿所有的 Java 执行线程。可以想象下,系统运行一半,突然像被人按了暂停键一样突然卡住了,然后 GC 结束才继续。

在这里插入图片描述

引用还有分类(了解)

目的: 我们希望描述这样的一类对象,当内存空间足够时,则保留在内存中;如果空间在回收后还非常的紧张,则可以抛弃这些对象,很多系统的缓存功能都符合这种场景。

强引用: 程序代码中普遍存在,类似 “Object obj = new Object()” 这类的引用,只要强引用还在,垃圾收集器就永远不可能回收这个对象

软引用: 用来描述一些还有用但非必须的对象。这种对象在系统将要发生内存溢出钱,会把这些对象列进回收范围进行第二次回收,依然没有足够内存,才会抛出内存溢出异常。提供了 SoftReference 类来实现软引用

弱引用: 用来描述非必须对象强度比软引用更弱一些,被引用关联的对象只能生存到下次垃圾收集之前。提供了 WeakReference 类来实现弱引用

虚引用: 也成为幽灵引用或幻影引用,最弱的一张引用关系。这个引用不会对对象的生存时间产生影响,也无法通过虚引用获取一个对象实例,这个引用的唯一目的,就是被收集器回收时收到一条系统通知。提供了 PhantomReference 类来实现虚引用

“缓刑” finalize(了解)

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖 finalize 方法,或者 finzlize 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。
如果对象要在 finalize() 中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。

第二次标记,基本就是被回收了,因为 finalize() 只会执行一次

不建议使用 finalize(),原因是运行代价高昂,不确定性大,无法保证调用顺序,比如使用 try-finally 之类的方式都可以做的更好,更及时

开始垃圾收集

标记 - 清除算法

最基础的收集算法,算法分为 “标记” 和 “清除” 两个阶段。

标记阶段: Java 算法就是上面的 可达性分析算法

回收阶段: 没什么特别的,就是直接把标记的回收了。

缺点:

  1. 效率问题,标记和清楚两个过程效率都不高。
  2. 空间问题,由于时标记后直接就回收了,导致空间会有大量不连续的内存碎片,导致如果分配大内存对象,无法找到足够的连续内存而不提前 GC。

在这里插入图片描述

复制算法

为了解决效率问题,因此出现了称为 “复制” 的收集算法。

原理: 将内存分为大小相等的两块,每次只用其中一块。当内存用完了,把存活的对象复制到另一块上,然后把之前已使用的空间清理掉。

优点:

  • 只对半区的内存进行回收
  • 不需要考虑内存碎片情况,因为移到另一半空闲的上面,按顺序分就行了
  • 实现简单,运行高效

缺点: 内存变成原来的一半
在这里插入图片描述
改进:
现在商用虚拟机都采用这种算法回收新生代,因为 98% 的新生代都死得快(Java 里一个方法跑完,里面的对象基本就都没用了)

内存划分:一块较大的 Eden 空间 和 两块较小的 Survivor 空间,Eden 比 Survivor 大小比例是 8 比 1。

每次使用 1 块 Eden,1 块 Survivor,进行回收时候,会把 Eden 和 Survivor 上存活的对象都复制到另一个 Survivor 上。当 另一个 Survivor 内存不足时,需要依赖其他内存(老年代)进行分配担保

缺点: 老年代都是存活时间较长的对象,因此这种算法不适合于老年代。

标记 - 整理算法

原理: 标记过程与 “标记 - 清除” 算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
在这里插入图片描述

分代收集算法

把 Java 堆分为新生代老年代,然后依照各自的特点采用最适当的收集算法。

规则:
新生代中如果发现有大批量的对象死去,只有少量存活,就采用复制算法

老年代中因为对象存活率高且没有额外的空间对它进行分配担保,就必须使用 “标记 - 清理”“标记 - 整理” 算法来进行回收。

HotSpot 算法

虽然标记算法用的是 可达性分析算法,但是缺点存在 GC 停顿问题,因此 HotSpot 在实现方面做了修改。

枚举根节点

即寻找所有可达性分析中的 GC Roots

为了节省枚举根节点而采用的解决方案:
为了让虚拟机知道哪里存放着对象引用,即 GC Roots,而不是把整个执行上下文和全局引用检查一遍,HotSpot 中使用了一组称为 OopMap 的数据结构来达到目的。

类加载完成的时候,把对象什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样,GC在扫描时就可以直接得知这些信息了。

安全点

在 OopMap 的协助下,HotSpot 可以快速且准确地完成 GC Roots 枚举,但有另一个问题,OopMap 内容变化的指令非常多,如果为每一个条指令都生成对应的OopMap,将需要大量的额外空间

解决方法:
特定的位置 记录信息(即 OopMap 的信息),这些位置被称为安全点

特点:
只在遇到 安全点 才能停顿进行 GC

安全点既不能太少(内容就多了,GC 停顿就会变长),也不能过于频繁以至于增大运行时的负荷(频繁 GC),标准就是 “是否据有让程序长时间执行的特征”

因此最明显的特征就是指令序列复用,例如:方法调用、循环跳转、异常跳转等,才会产生安全点

如何让GC发生时,所有线程(除执行JNI调用的线程)都到最近的安全点停顿下来?

  1. 抢先式中断,不需要线程的执行代码主动配合,在GC发生时,首先把所有线程中断,如果有线程中断的地方不在安全点,就恢复线程,让它执行到安全点

  2. 主动式中断,需要中断线程时,不直接对线程操作,而是设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就中断挂起轮询标志的地方和安全点是重合的

安全区域

由于线程处于 SleepBlocked 的时候,线程无法响应 JVM 的中断请求,这时候就需要 安全区域 来解决问题。

特点:
在一段代码片段中,引用关系不会发生变化。在这个区域的任何地方 GC 都是安全的,可以把安全区域看作是安全点的扩展。

描述:
在线程执行到安全区域代码时,首先标识自己进入安全区域,当这段时间里 JVM 发起 GC,不用管标识为安全区域的线程了。在线程要离开安全区域时,要检查系统是否已经完成了根节点枚举,如果完成,线程继续执行,否则等待直到收到可以安全离开安全区域的信号为止。

发布了107 篇原创文章 · 获赞 414 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_37143673/article/details/105122323
今日推荐