JVM_GC垃圾回收算法与原理

JVM只会对已经死亡的对象触发垃圾回收机制

触发垃圾回收的方式有两种方式系统自动回收(即堆内存已满)与主动调用System.GC()触发垃圾回收条件是被回收对象没有被引用(使用)才可以被回收。

一.如何判断对象是否存活

在堆内存中存放着大量的 Java 对象,垃圾收集器在对堆进行垃圾回收时,首先要判断 哪些对象还活着,哪些对象已经死去(即不在被引用的对象)

1.1引用计数器算法

引用计数器算法指为对象添加一个引用计数器,当 有一个地方引用该对象时,计数 器+1,当引用失效时,计数器-1,当计数器为 0 时表示该对象不再被引用。 引用计数器算法实现简单、效率高,适用于大多数适用场景,但是主流的 JVM 均没 有采用了这种算法,原因在于它很难解决对象之间的相互循环引用情况;
计数器算法
在这里插入图片描述

1.2 可达性分析算法

在主流的商用虚拟机中都是通过可达性分析算法来判断对象是否存活,这个算法核 心思想就是通过一个“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走 过的路径称为引用链,当一个对象到“GC Roots”没有任何引用链相连,则证明该对象不再 被引用。
在这里插入图片描述

如图:可以理解为一个队列都必须以GC Roots(外部引用变量)为队首的队列中的所有对象都是有效的否则就是Object5、Object6、Object7 虽然相互引用,但到 GC Roots 没有任何引用即没有被外部变量所引用也就没有被使用,所以这三 个对象就被标记为要回收的对象。


在 Java 语言中,可作为 GC Roots 的对象包含以下几种:

  1. 虚拟机栈(栈帧中的局部变量表)中引用的对象。(可以理解为:引用栈帧中的局部变 量表的所有对象)

  2. 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)

  3. 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)

  4. 本地方法栈中(Native 方法)引用的对象(可以理解为:引用 Native 方法的所有对象)
    在这里插入图片描述

(1) 首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。

(2) 第二种是我们在类中定义了全局的静态的对象,也就是使用了 static 关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为 GC Roots 是必须的。

(3) 第三种便是常量引用,就是使用了 static final 关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为 GC Roots。

(4) 最后一种是在使用 JNI 技术时,有时候单纯的 Java 代码并不能满足我们的需求,我们可能需要在 Java 中调用 C 或 C++的代码,因此会使用 native 方法,JVM 内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为 GC Roots。


二、Java 中四种引用分析

无论是通过引用计数器还是通过可达性分析判断对象是否存活都跟引用有关,在 JDK1.2 以前,Java 中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一 块内存的起始地址,就称这块内存代表着一个引用。这种定义有些狭隘,一个对象在这种定 义下只有被引用或者没有被引用两种状态。 我们希望能描述这一类对象: 当内存空间还足够时,则能保存在内存中;如果内存空间 在进行垃圾回收后还是非常紧张,则可以回收这些对象。很多系统中的缓存对象都符合这样 的场景。 在 JDK1.2 之后,Java 对引用的概念做了扩充,将引用分为强引用、软引用、弱引用和 虚引用四种,这四种引用的强度依次递减。

(1) 强引用(Reference) 强引用是代码中普通存在的引用,类似于 User user = new User() 如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚 拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有 强引用的对象来解决内存不足的问题。 强引用是两个极端如果被引用就不会被回收,没有被引用则立即进行回收。

(2) 软引用(SoftReference) 软引用用来描述一些还有用但并非必须的对象,如果一个对象只具有软引用,则内 存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象 的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可用来实 现内存敏感的高速缓存。 JDK1.2 以后使用 java.lang.ref.SoftReference 类来实现软引用
在这里插入图片描述

(3) 弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期,它仅仅 能够生存到垃圾回收之前,当垃圾收集时,无论内存是否足够,弱引用的对象都要 被回收 JDK1.2 以后使用 java.lang.ref.WeakReference 类来实现弱引用
弱引用是用来实现某一个对象使用完,如果再次使用是在执行垃圾回收之前都是可以通过弱引用再次捡回来继续使用的在这里插入图片描述

(4) 虚引用(PhantomReference) “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定 对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在 任何时候都可能被垃圾回收器回收。也无法通过虚引用获得一个实例对象;设置虚 引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者 后续添加进一步的处理。 Java 中允许使用 finalize()方法在对象被清理前做一些清理工作。 注:如果持有虚引用的对象重写了 finalize()方法则表示在对象被垃圾回收器清 理前要做清理工作,此时虚引用的对象直接被清理不进入引用队列 如果持有虚引用的对象没有重写 finalize()方法,则表示在对象被垃圾回收 器清理后要做清理工作,此时虚引用的对象被添加到引用队列中 JDK1.2 以后使用 java.lang.ref.PhantomReference 类来实现虚引用

虚引用不能单独使用只能和引用队列一起联合使用

(5) 引用队列(ReferenceQueue) 1. ReferenceQueue 是用来配合引用工作的,没有 Referencequeue 一样可以运行 2. 创建引用的时候可以指定关联的队列,当 GC 释放对象内存的时候,会将引用加 入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所 引用的对象的内存被回收之前采取必要的行动,这相当于一种通知机制。 3. 当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。 通过这种方式,JVM 运行我们在对象被销毁后,做一些我们自己想做的事情。 JDK1.2 以后使用 java.lang.ref.ReferenceQueue 类来实现引用队列
在这里插入图片描述
检测对象是否已经被清理决定是否处置相关资源,若在此方法前调用finalize方法则说明对象被清理前已处置相关资源无需再处置,poll()实质就是一个通知。

三、垃圾回收算法

1. 标记-清除算法

“标记-清除”算法是最基础的收集算法。 算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回 收所有被标记的对象(标记过程通过可达性分析实现)。后续的收集算法都是基于这种思 路并对其不足加以改进而已。 “标记-清除”算法的不足主要有两个: 效率问题:标记和清除这两个过程的效率都不高 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在 程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾 收集。
在这里插入图片描述
2. 复制算法
“复制”算法是为了解决“标记-清除”的内存碎片问题。它将可用内存按容量划分为 大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域 还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样 做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等 的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。但 由于每次都要预留出 50%的空间不能存放对象,内存利用率低,内存空间严重浪费。
在这里插入图片描述
3. 标记-整理算法
标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清 理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述
三种垃圾回收算法各有优缺点:
在这里插入图片描述

四、JVM 分代垃圾回收机制

JVM 的堆内存是进行分代管理的,JVM 垃圾收集器针对不同的分代使用不同的垃圾回收 算法。

1. 新生代垃圾回收

新生代使用复制算法来进行垃圾回收,由于新生代中 98%的对象都是"朝生夕死"的, 所以并不需要按照 1 : 1 的比例来划分内存空间,而是将内存(新生代内存)分为一块 较大的 Eden(伊甸园)空间和两块较小的 Survivor(幸存者)空间,每次使用 Eden 和其 中一块 Survivor(两个 Survivor 区域一个称为 From 区,另一个称为 To 区域)。当回 收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 空间上,最 后清理掉 Eden 和刚才用过的 Survivor 空间。

当 Survivor 空间不够用时,JVM 使用内存担保的机制将 Survivor 空间存活的对象 转移到老年代。 HotSport 默认 Eden 与 Survivor 的大小比例是 8 : 1,也就是说 Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的 90%, 而剩下的 10%用来存放回收后存活的对象。

HotSpot 实现的复制算法流程如下:

当 Eden 区满的时候,会触发第一次 Minor gc,把还活着的对象拷贝到 Survivor From 区;当 Eden 区再次触发 Minor gc 的时候,会扫描 Eden 区和 From 区,对两个区 域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到 To 区域,并将 Eden 区 和 From 区清空。当后续 Eden 区又发生 Minor gc 的时候,会对 Eden 区和 To 区进行垃 圾回收,存活的对象复制到 From 区,并将 Eden 区和 To 区清空。 部分对象会在 From 区域和 To 区域中复制来复制去,如此交换 15 次(由 JVM 参数 MaxTenuringThreshold 决定,这个参数默认是 15),最终如果还存活,就存入老年代。

2. 老年代垃圾回收

老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-整理"算法

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值