JVM垃圾收集算法


前言

在jvm堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就 是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对 象)了。


一、如何判断对象的生死

1、引用计数法

 引用计数法判断对象是否存活靠的是,在对象中添加一个计数器,每当有一个地方引用他时,计数器值加一;当引用失效时,计数器值减一。任何时刻计数器为零的对象就是不只能在被使用的。
评价:引用计数法虽然占了些额外的内存空间来进行计数,但其原理简单,判断效率高,多数情况是个不存的选择。
但是java领域,主流的虚拟机都没有采用引用计数法来管理内存,主要原因就是看似简单的算法要考虑很多例外情况,必须配合大量的处理才能保证正确的工作。譬如单纯的引用计数很难解决对象间的相互循环引用问题。

2、可达性分析算法

当前主流语言java、c#、Lisp的内存管理子系统都是可达性分析算法判断对象是否存活。基本思路通过一系列的GC Roots 的根对象作为起始节点集,如果某个对象到GC Roots 间没有任何引用链相连,证明此对象不可能再被使用。
在这里插入图片描述
如上图object 5,object 6,object 7,因为没有和GC Roots有引用连,所以会被判定为可回收对象,即时他们之间有相互引用关系。

可以作为GC Roots 的是

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,譬如java类的引用类型静态变量
  • 在方法区中常量引用的对象,譬如字符串常量池里的引用
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • java虚拟机内部的引用,如基本数据类型对应得Class对象,一些常驻的异常对象(如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(syncchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • 除了固有的GC Roots集合外,根据用户所选的垃圾收集器以及当前回收区域不同还有可能其他对象临时性加入,共同构建GC Roots集合,比如新生代中的对象可能被老年代中的对象引用。

二、分代收集理论

1.分代理论内容

分代理论建立在两个假说上的
- 弱分代假说:绝大多数对象朝生夕死
- 强分代假说熬过多次垃圾收集的过程的对象,难以被消亡
根据这两个假说共同奠定了多款常用垃圾收集器的一致实际原则:收集器应将Java堆划分出不同区域,然后将回收对象依据其年龄(年龄即熬过垃圾回收的次数)分配到不同的区域中储存。
具体的java虚拟机中一般至少分为新生代、老年代两个区域

2.跨带引用假说

内容:跨代引用相对于同代引用仅占极少数。
根据这条假说,我们不应该为了少量的跨代引用去扫描整个老年代,也不必专门浪费空间记录每一个对象是否存在跨引用。只需在新生代建立一个全局的数据结构—记忆集
记忆集:这个结构将老年代划分成若干小块,标识出老年代那块内存会存在跨代引用。此后发生Minor GC 时,只有包含跨代引用的小块内存里的对象才被加入到GC Roots进行扫描。
评价:虽然记忆集方法需要在对象改变引用关系时,维护记忆集记录数据的准确性,会增加一些运行时开销,但比起收集是扫描整个老年代来说依然是划算的。

三、垃圾收集算法

1、标记-清除算法

标记-清楚也是最早出现的最基础的垃圾收集算法,算法过程分为标记清除两个阶段:
- 首先标记出所有需要清除的对象。当然也可以反过来标记存活的对象
- 标记完成后,统一回收掉标记好的对象。
- 标记的过程就是判断对象是否存活的过程,一般通过可达性分析算法
标记-清除算法存在的优缺点:
- 优点:算法简单,容易实现,后续算法都以标记清楚算法为基础,对其改造
- 缺点:1、执行效率不稳定。如果Java堆中包含大量的对象,并且大部分对象是需要回收的,此时就需进行大量的标记清楚动作,导致标记 清除两个过程随着对象的数量增长而降低。2、内存空间碎片化问题。标记、清楚之后会产生大量的不连续的内存碎片,空间碎片太多导致以后程序运行过程要分配较大对象时无法找到足够的连续内存而不得不再次进行垃圾回收动作。
在这里插入图片描述

2、标记-复制算法

为解决标记清楚算法面对大量可回收对象时执行效率低的问题,首先出现了半区复制算法。它将内存按容量会分为两块等大的区域,每次使用其中的一块,当这一块内存用完了,就将存活的对象对象复制到另一块上面去,然后把已经使用玩得内存块一次性清除干净。
优缺点
- 优点:1、实现简单、运行高效。2、解决了内存碎片问题
- 缺点:1、如果大多数对象是存活的话或有很高的复制开销。2、将可用内存缩短了原先的一半,空间浪费严重。
在这里插入图片描述
目前商用的java虚拟机大多优先采用这种收集算法去回收新生代。因为新生代对象98%熬不过第一轮的收集,因此不需要按照1:1的比例来划分新生代的内存空间。
1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策 略,现在称为“Appel式回收”。Appel式回收具体是把新生代划分为一块较大的Eden空间 和两块较小的Survivor空间。每次分配内存时只使用Eden和其中的一块Survivor。发生垃圾收集时,将Eden和Survivor中存活的对象一次复制到另一块Survivor空间中,然后一次性清理掉Eden和Survivor空间。
额外情况:当存活的对象大于Survivor空间时,就会依赖其他内存区域(实际上大多是老年代)进行分配担保。直接进入老年代。

3、标记-整理算法

针对老年代对象存亡特征,1974年出现了标记-整理算法。标记过程与标记-清除算法一致。但后续步骤不是直接对可回收对象进行清理,而是让存活对象向内存空间一端移动,然后直接清理掉边界以外的内存。
评价:标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动 式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:
比较标记-清除算法和标记-整理算法。前者收集时效率更高,但收集结果存在大量内存碎片,为以后的给对象分配内存带来了很大的负担。后者收集时操作麻烦,在整理的时候还会出现“Stop The World”现象,即全程暂停用户程序,但是将来的给对象分配内存非常简单。
出于吞吐量(吞吐量指的是赋值器与收集器的效率总和)的考虑的话,标记-整理算法更优秀。
出于低延迟考虑的话,标记清除算法更优秀。
出于综合考虑的话,有一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担,做法是让虚 拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经 大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值