虚拟机系列:JVM 有哪些垃圾回收算法

垃圾即是无用的东西,不仅无用的了,而且占用地方,不丢留着干嘛。jvm也想把自己的垃圾给丢掉,毕竟jvm的内存空间还是很贵的。

那么就有两个问题:

一,什么是垃圾,怎么判定一个对象是不是垃圾呢?

二,怎么丢垃圾,什么姿势丢垃圾优雅(算法)?

 

一,怎么判断某对象是不是垃圾

对于jvm来说垃圾就是一个没有价值的对象,对象没有被使用就是没有价值的垃圾对象。在jvm这个内存世界中对象太多了,总要有规则来管理判断每个对象是不是垃圾。目前主要有两种方法,一个是引用计数法 还有一个是可达性分析算法也叫根搜算法

1. 引用计数法(Reference Counting)

引用计数法比较简单,就是字面意思。给对象添加计数器,当对象每被引用一次,计数器就+1;相反,当对象引用每被失效一次,计数器就-1 ,当对象计数器为0的时候就可以判断为垃圾。

引用计数算法简单高效,但是java中却没有使用,原因就是他不行,无法处理循环引用的问题。按照此算法的逻辑,只有计数器是非0就不是垃圾,如果存在A,B两个对象互相引用(计数器各自+1),但是有没有被别的对象引用,实际上这两个对象没有被使用,但是也不会被回收,就像两个连着的小船漂在内存的大海中一样。当这种互相引用的对象越来越多,内存就扛不住了OOM异常。

 

2. 可达性分析算法(GC Roots Tracing)

在主流的商用程序语言中(Java和C#),都是使用根搜索算法(GC Roots Tracing)判断对象是否存活的。这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,如下图:

上面说的GC Roots对象的包括如下几种:

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象(线程栈的变量)

  • 方法区中的类静态属性引用的对象

  • 方法区中的常量引用的对象

  • 本地方法栈中JNI(即一般说的Native方法)的引用的对象

 

二,什么姿势丢垃圾优雅(算法)?

上文知道怎么判定某个对象是否是垃圾,现在来看看有几种丢垃圾的姿势:jvm中垃圾回收算法分别是标记-清楚算法复制算法标记-整理算法,以及分代收集算法。下面分别说下每个具体的算法以及各自的优缺点。

1. 标记清楚算法(Mark-Sweep)

该算法是最基础的算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所有说它是最基础的算法是因为后续的收集算法都是基于这种思路并对其缺点进行改进得到的。

它的缺点主要有两个:

  • 一个是效率问题,标记和清除过程效率都不高;

  • 另外一个是空间问题,标记清除后会产生大量不连线内存碎片,内存碎片太多导致当程序运行进需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集操作。

执行过程如下:

2. 复制算法(Copying)

为了解决效率问题,“复制”收集算法出现了,它将可用内存按容量分为大小相等的两块,每次只使用其中一块。当这一块内存使用完了,就将还存活着的对象复制到另外一块上,然后再把已使用过的内存空间一次性清理空。这样使得每次都是对其中一块进行内存回收,内存分配时也不用考虑内存碎片的问题,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价太高了一点。

复制算法执行过程如下图:

现在商业虚拟机都是采用这种算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,生命周期很短,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间与两块较小的Survivor空间,每次使用Eden与其中一块Survivor空间。当回收时,将Eden与Survivor中还存活的对象一次性地拷贝到另外一块Survivor空间中,最后清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor空间比例为8:2,也就是每次新生代中可用内存为整个新生代容量的90%(80%+10%),只有10%的新生代内存是“浪费”的。当然,98%的对象可回收只是一般场景下的数据,但没有办法保证每回收都只有不多于10%对象存活,当Survivor空间不足时,需要依赖其它内存(老年代)进行分配担保。

3. 标记-整理算法(Mark-Compact)

复制算法在对象存活率较高时就要执行较多的复制操作,效率将会变低,如果不想浪费50%的空间,就需要有额外的空间进行担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点,“标记-整理”算法被提出,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清除,而是让所有存活对象都向一端移动,然后直接清除掉端边界以外的内存。

“标记-整理”算法执行示意图如下:

 

4. 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,该算法将根据对象存活周期不同将内存划分为几块。一般把Java堆分为新生代与老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

  • 在新生代中,每次垃圾收集都发现有大量对象死去,只有少量对象存活,就选得复制收集算法,只要付出少量存活对象的复制成本就可以完成收集。

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

 

本文参考《深入理解Java虚拟机》

 

 

 

关注不迷路

除了虚拟机系列 还有MySQL高级相关更多内容,如事务,锁,MVCC,读写分离,分库分表等还在持续更新中,欢迎关注催更。

我是阿纪,用输出倒逼输入而持续学习,持续分享技术系列文章,以及全网值得收藏好文,欢迎关注公众号,做一个持续成长的技术人。

 

虚拟机系列的历史文章

1. 虚拟机系列:jvm运行时堆内存如何分代;

 

原文地址:https://www.cnblogs.com/sunjiguang/p/15784499.html