您的位置:首页 > 其它

JVM的垃圾回收机制与垃圾回收算法

2020-04-23 08:52 471 查看

java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来—《深入理解java虚拟机》

关于垃圾收集(Garbage Collection),需要完成的三件事情:

① 哪些内存需要回收?

②什么时候回收?

③如何回收?

如何判断对象是否存活?

①引用计数法: 给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。

然而,在很多主流的java虚拟机里面并没有选用引用计数法来判断对象是否存活,最主要的原因就是它无法解决对象循环引用的问题,即如果两个对象相互引用对方,但是彼此都没有外部引用指向着它们,也就是这两个对象已经不能再被访问,但是由于引用着对方,导致计算器不为0,所以垃圾收集机制无法回收它。

②可达性分析法: 通过一系列称为“GC Roots”的根对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(即从 GC Roots 到这个对象不可达),证明此对象不可用。

java的四种引用

强引用(Strongly Reference): 类似于"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。

软引用(Soft Reference): 用来描述一些有用,但不是必须的对象;只被软引用关联着的对象,当堆内存空间不够的时候,就会回收这些对象,当内存空间足够的话,就不回收这些对象。

弱引用(Weak Reference): 强度比软引用弱一点;被弱引用关联着的对象只能生存到下一次垃圾收集为止,即当垃圾收集器开始工作的时候,不论当前内存是否足够,都会回收掉只被弱引用关联着的对象。

虚引用(Phantom Reference): 虚引用也称“幽灵引用”或“幻影引用”,他是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生命周期造成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是使得这个对象在被垃圾回收时收到一个系统通知。

生存还是死亡?

即使在可达性分析算法中不可达的对象,也并非"非死不可"的,这时候他们暂时处在"缓刑"阶段。要宣告一个对象的真正死亡,至少要经历两次标记过程: 如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况都视为"没有必要执行",此时的对象才是真正"死"的对象。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。

关于finalize()方法

①finalize()方法是属于Object类的,但是由Object类的finalize()方法源码可知,这个方法是一个空实现,即如果子类要在被垃圾回收时有复活的机会,那就应该去重写该方法,并且在方法体中将自己重新与引用链上的任何一个对象建立关联即可,比如把自己(this)赋值给某个类变量或者对象的成员变量。例如下面这种情况(test是该类的一个静态成员变量):

②finalize()方法是由系统自动调用的,并不需要显示的去调用它,并且一个对象的finalize()方法最多只会被系统自动调用一次,即这种自救的机会只有一次。

③PS:Object类的finalize()方法已经过时了!!!

方法区的回收

方法区的回收主要包括:常量的回收和类型的卸载

①常量的回收: 跟对象的回收类似,以字符串常量池中的字面量为例,当常量池中的一个字符串没有被任何引用指向的话,该字符串就成为废弃的常量,将被垃圾回收。

②类型的卸载: 当一个类不再被使用的时候,该类将会被垃圾回收。其中,一个类不再被使用有以下三种情况:

  • 该类的所有实例都已经被回收(即在Java堆中不存在该类及其任何派生子类的实例)
  • 加载该类的ClassLoader已被回收
  • 该类对应的Class对象没有任何其他地方被引用,无法在任何地方通过反射访问该类的方法

垃圾回收算法

以下是垃圾回收针对不同分代的常见名词:

①部分收集(Partical GC): 指目标不是完整的收集整个java堆的垃圾收集,又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集;因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。(注意:对于Major GC,不同的垃圾收集器有不同的变现,有的垃圾收集器中Major GC指的是老年代的垃圾收集,有的是指整堆收集)
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。

②整堆收集(Full GC): 收集整个java堆方法区的垃圾收集

Minor GC ,Full GC 触发条件

Minor GC触发条件: 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法区空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)由Eden区、From Survivor区向To Survivor区复制时,存活对象大小大于To Survivor区可用内存,则把存活对象转存到老年代,且老年代的可用内存小于该存活对象大小

标记-清除算法(Mark-Sweep)

“标记-清除”算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足加以改进而已。

该算法的缺点:

  • 一是执行效率不稳定,如果java堆中包含大量的对象,而且其中大部分对象是需要被回收的,这时就必须进行大量的标记和清除动作,导致效率降低。
  • 二是内存空间的碎片化问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

标记-复制算法(新生代回收算法)

标记-复制算法是为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。

它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。

好处是:每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。

缺点是:将可用内存缩小为原来的一半,空间浪费多了一点。

新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行空间分配担保。
HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。也就是只有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),最终如果还存活,就存入老年代。

标记-整理算法(老年代回收算法)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。针对老年代的特点,提出了一种称之为“标记-整理算法”。标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

  • 点赞
  • 收藏
  • 分享
  • 文章举报
can_chen 发布了15 篇原创文章 · 获赞 0 · 访问量 559 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: