Java GC算法


垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。这对比与C或者C++的程序有着巨大优势,C或C++需要程序员编写程序去释放内存,而Java的垃圾回收交由Java虚拟机去执行了。有个段子解释的很生动:说在食堂里吃饭,吃完把餐盘端走清理的,是C++程序员;吃完直接就走的,是Java 程序员。

对象存活判断算法

既然是垃圾回收机制,那么得判断某对象是不是垃圾,当对象不存活的情况下,就可以被定义为垃圾。如何定义呢?

引用计数算法(Reachability Counting)

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

可达性分析算法(Reachability Analysis)

基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
在这里插入图片描述
通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与GC Root建立直接或间接的连接,系统就会判定你为可回收对象。
那么,哪些属于GC Root?在Java语言中,可作为GC Root的对象包括以下4种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

垃圾收集算法

标记-清除算法

顾名思义,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
标记-清除算法执行过程如图:
在这里插入图片描述

优点

1.实现简单,与其他算法的组合也就相应地简单。
2.与保守式GC算法兼容
后面介绍的保守式GC算法中,对象是不能被移动的。因此保守式GC算法跟把对象从现在的场所复制到其他场所的GC复制算法与标记-压缩算法不兼容。
而标记-清除算法因为不会移动对象,所以非常适合搭配保守式GC算法。事实上,在很多采用保守式GC算法的处理程序中也用到了标记-清除算法。

缺点:

1.产生内存碎片化
标记-清除算法是整块分配内存,从而容易导致内存碎片的产生
2.分配速度慢
标记-清除算法中分块不是连续的,因此每次分配都必须遍历空闲链表,找到足够大的分块。最糟的情况就是每次进行分配都得把空闲链表遍历到最后。
3.与写时复制技术不兼容


复制算法

复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。
在这里插入图片描述

优点

1.优秀的吞吐量
GC 标记 - 清除算法消耗的吞吐量是搜索活动对象(标记阶段)所花费的时间和搜索整体 堆(清除阶段)所花费的时间之和。
另一方面,因为 GC 复制算法只搜索并复制活动对象,所以跟一般的 GC 标记 - 清除算 法相比,它能在较短时间内完成 GC。也就是说,其吞吐量优秀。
尤其是堆越大,差距越明显。GC 标记 - 清除算法在清除阶段所花费的时间会不断增加, 但 GC 复制算法就不会产生这种消耗。毕竟它消耗的时间是与活动对象的数量成比例的。
2.可实现高速分配
GC复制算法不使用空闲链表。这是因为分块是一个连续的内存空间。比起 GC 标记 - 清除算法和引用计数法等使用空闲链表的分配,GC 复制算法明显快得多。
3.不会发生碎片化
基于算法性质,活动对象被集中安排在 From 空间的开头对吧。像这 样把对象重新集中,放在堆的一端的行为就叫作压缩。在 GC 复制算法中,每次运行 GC 时 都会执行压缩。
因此 GC 复制算法有个非常优秀的特点,就是不会发生碎片化。也就是说,可以安排分 块允许范围内大小的对象。
4.与缓存兼容
在 GC 复制算法中有引用关系的对象会被安排在堆里离彼此较近的位置。这种情况有一个优点,那就是 mutator 执行速度极快。这也是借助压缩来完成的,通过压缩来把有引用关系的对 象安排在堆中较近的位置。

缺点

1.堆使用效率低下
GC复制算法把堆二等分,通常只能利用其中的一半来安排对象。也就是说,只有一半堆能被使用。相比其他能使用整个堆的GC算法而言,可以说这是GC复制算法的一个重大的缺陷。
通过搭配使用GC复制算法和GC标记-清除算法可以改善这个缺点。
2.不兼容保守式GC算法
GC复制算法必须移动对象重写指针,所以有着跟保守式GC算法不相容的性质。
3.递归调用函数
在这里介绍的算法中,复制某个对象时要递归复制它的子对象。因此在每次进行复制的 时候都要调用函数,由此带来的额外负担不容忽视。
此外,因为在每次递归调用时都会消耗栈,所以还有栈溢出的可能。


标记整理算法

标记整理算法(Mark-Compact)标记过程仍然与标记 — 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
在这里插入图片描述
标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从下图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。
在这里插入图片描述

优点

1.可有效利用堆
在标记-整理算法中会执行压缩,和其他算法相比而言,堆利用效率高。而且标记-整理算法不会出现GC复制算法那样只能利用半个堆的情况。
另一方面,尽管标记-清除算法也能利用整个堆,但因为没有整理(压缩)的过程,所以会产生碎片化,不能充分有效地利用堆。

缺点

1.压缩花费计算成本
在标记-清除算法中,清除阶段也要搜索整个堆,不过搜索1次就够了。但标记-整理算法要搜索3次,这样就要花费约3倍的时间,这是一个相当巨大的缺陷,特别是堆越大,所消耗的成本也就越大。


分代收集算法

严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。

参考:
https://www.jianshu.com/p/47ff2bff20dc
https://www.cnblogs.com/ityouknow/p/5614961.html
https://mp.weixin.qq.com/s/C8465IvIm-kMYpIT0YeRmg
https://mp.weixin.qq.com/s/SbXb3UbQet6NOcvTO2T7fw

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值