JVM_5_垃圾搜集算法

来源:互联网 发布:java后台开发框架 编辑:程序博客网 时间:2024/05/20 00:13

垃圾搜索算法

 

参考资料:

《JVM内存管理------GC简介》

《Java虚拟机垃圾回收(二)垃圾回收算法:标记-清除算法 复制算法 标记-整理算法 分代收集算法 火车算法》

《Java Platform, StandardEdition HotSpot Virtual Machine Garbage Collection Tuning Guide》

 

Java虚拟机规范中文版上传了,点击下面链接,即可下载

Java虚拟机规范SE7中文版


 

在根搜索算法的基础上,现代虚拟机的实现中,垃圾搜集的算法主要有三种

  • 标记 -清除算法
  • 复制算法
  • 标记 -整理算法

这三种算法都扩充了根搜索算法。


 

 

 

标记——清除算法

 

最基础的收集算法是"标记-清除"算法,如同它的名字名字一样,算法分为"标记""清除"两个阶段:

  1. 首先标记出所有需要回收的对象
  1. 在标记完成之后,统一回收所有被标记的对象。

 

之说以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路,并对其不足进行改进而得到的。

 

它的不足之处主要有两个:

  1. 效率问题:标记和清除两个过程的效率都不高
  2. 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致程序后续运行时需要分配较大对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。

如果有较大内存的对象,需要分配内存,没有地方存放,会导致Full GC,增加GC次数。

(这里使用书中的原图)


 

---------------------------------------------------------------------------------------------------------


(以上是书中原文,感觉说的比较精炼、一些细节没有明白....网上找了些比较好的资料...我们来看看)

 

发现网上很多资料上对于"标记"的说明存在一定的差异。

 

《深入理解Java虚拟机——JVM高级特性与最佳实践》中对于标记的说法:标记所有需要回收的对象

 

网络上很多博客对于标记的说法:标记所有存活对象。

 

这里疑问的就是:

被标记的,到底是存活对象,还是待清除对象??

 

个人比较倾向于周志明书中对标记 的说明。

 

---------------------------------------------------------------------------------------------------------

 

根搜索算法告诉我们 哪些对象可以被回收,但他还不能承担垃圾收集的重任。

因为我们在程序运行期间如果想进行垃圾回收,那必须让GC线程与程序中的线程互相配合,才能在不影响程序运行的前提下,顺利的将垃圾进行收集。

 

为了达到这个目的,标记/清除算法就应运而生了。

它的做法是当堆中的有效内存空间被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项是标记,第二项则是清除

 

标记标记的过程其实就是,遍历所有的GC Roots,然后将所有的GC Roots不可到达的对象标记位存活对象

 

清除清除的过程就将遍历堆中所有的对象,将所有标记的对象全部清除掉

 

其实这两个步骤并不是特别复杂,也很容易理解。

简单解释下标记/清除算法

就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将需要清除对象标记一遍,最终再将堆中没有被标记的对象全部清除掉,接下来便让程序恢复运行。

 

 

下面我们用图的方式来看看标记清除算法是怎么做的...


这张图代表的是程序运行期所有对象的状态(标记之前),他们的标志位全部是0(也就是未标记,以下默认0就是未标记,1表示已标记)

假设现在有效内存空间耗尽了,JVM将会停止应用程序的运行并开启GC线程;

 

然后开始进行标记工作,按照根搜索算法,标记完成之后:


可以看到,按照根搜索算法,所有从root对象不可到达的对象都被标记了,此时已经完成了第一阶段。

 

接下来,执行第二阶段清除,那么清除完成之后,剩下的对象以及对象状态如下图 :


如图可以看到,被标记的对象会被回收清理掉,而没标记的对象将会留下。

 

接下来,就是唤醒停止的程序线程,让程序继续运行即可。

 

这一过程并不复杂,不过有一个疑问:为什么非要停止程序的运行呢?

 

让我们来举个生活中的小栗子:

"你妈妈在给你打扫房间的时候,肯定会让你老老实实的坐在椅子上或者在房间外面待着;如果她一边打扫,你一边乱扔垃圾,这房间还能打扫完?"

哈哈哈,蛮贴切的对不对...

 

 

标记/清除算法缺点:

1.效率比较低下(递归与全栈对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲

 

2.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。这种方式清理出来的空闲内存不是连续的,这点不难理解,我们的死亡对象都是随机出现在内存的各个角落,现在把它们清除之后,内存的布局自然会乱七八糟。


 






复制算法

 

 

为了解决效率问题,一种称为"复制"的收集算法出现了。

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

当这一块的内存使用完了,就将还存活着的对象复制到另一块上面,然后将已使用过的内存空间一次清理掉。

这样使得每次都是对整个半区的内存进行回收,内存分配时也就不用考虑内存碎片等复杂情况。

 

复制算法执行过程图:


值得提一下,现在商业虚拟机都采用这种搜集算法来回收新生代。(来自原书,权当了解)

 

-------------------------------------------------------------------------------------------------------

 

复制收集算法,为了解决标记-清除算法的效率问题。

 

算法思路

  1. 把内存划分为大小相等的两块,每次只使用其中一块
  2. 当一块内存用完了,就将还存活着的对象复制到另一块内存上(然后使用这一块)
  3. 再把已使用过的那块内存空间一次清理掉。

 

执行过程如下图


 

 

优点

每次都是只对一半内存进行回收。

内存分配时不用考虑内存碎片等问题。

实现简单,运行高效。

 

 

缺点

空间浪费:可用内存缩减为原来的一半,太过浪费。

效率随对象存活率升高而变低:当对象存活率较高时,需要进行较多复制操作,效率将会变低。


 





 

标记整理算法

 

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。

更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保。

 

于是有人提出"标记-整理"算法,标记的过程与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,

然后直接清理掉边界以外的内存。

 

执行过程如下图:


 

-----------------------------------------------------------------------------------------------------------

 

标记-整理算法是根据老年代的特点提出来的。(不知道年老代没关系,暂时放着)

 

算法思路

标记:标记过程与"标记-清除"算法一样,将所有需要清除的对象进行标记。

整理:让所有存活的对象都向一端移动,然后直接清理端边界以外的内存。

 

执行过程如下图:


 

优点

不会像复制算法,效率随对象存活率升高而变低。

年老代的特点:对象存活率高,没有额外的空间可以分配担保。所以年老代一般不能直接选用复制收集算法。而选用标记-整理算法。

 

不会像标记-清除算法,产生内存碎片因为清除前,进行了整理,存活对象都集中到了空间的一侧。

 

缺点

主要是效率问题,除像标记-清除算法的标记过程之外,还多了需要整理的过程,效率更低。




 

 

 

分代收集算法

 

当前商业虚拟机的垃圾收集都采用"分代收集"算法;

这种算法只是根据对象存活周期的不同,将内存划分为几块;

 

一般是Java堆分为新生代年老代,这样子就可以根据各个年代的特点采用最适合的收集算法。

 

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

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

 

-------------------------------------------------------------------------------------------------------

 

我们这里先暂停一下,我们先来看下"HotSpot虚拟机的改良算法"

这节内容,本来是应该是复制算法中的;

我这里将它放在这里,因为我觉得,在还没有看分代算法之前,看到那些什么年轻代、年老代,会一脸懵逼..至少我一开始的时候是这样的...

 

HotSpot虚拟机的改良算法

 

Eden:伊甸园(年轻对象快乐玩耍的空间)

Survivor:幸存空间

Tenured:年老代

 

弱代理论

分代垃圾收集基于弱代理论,具体描述如下:

  1. 大多数分配了内存的对象并不会存活太长时间,在处于年轻代的时候就会死掉
  2. 很少有对象从年老代变为年轻代

 

其中IBM研究表明:新生代中98%的对象都是"朝生夕死";

所以并不需要按1:1比例来划分内存;

 

 

Hotspot虚拟机新生代内存布局计算法

  1. 新生代内存划分为一块较大的Eden空间和两块较小的Survivor空间。
  2. 每次使用Eden和其中一块Survivor
  3. 当回收时,将Eden和使用中的Survivor空间中还存活的对象,一次性复制到另外一块Survivor
  4. 而后清理掉Eden和使用过的Survivor空间。
  5. 后面就是使用Eden和复制到另一块的Survivor空间。

 

默认EdenSurvivor = 81,即每次可以使用90%的空间,只有一块Survivor空间被浪费(10%)


上面的图 画的不是很准确,后来去Oracle 官网上找到了这个图片..


Virtual:为动态收缩区域,当垃圾收集获取目标高吞吐量或低停顿后,会尽量缩小区域内存,以获得更好的空间局部性。

 

 

 

补充:

EdenSurvivior是如何配合的?

(摘录原书的解释)

 

年轻代中将内存分为一块较大的Eden空间和两块较小的Survivor空间(survivor1survivor2)

  1. 每次使用Edensurvivor1空间(survivor1,为了方便说明,我默认采用survivor1空间,其实是任意一块)

 

  1. 当发生GC回收时,将Edensurvivor1空间中还存活着的对象一次性复制到另外一块survivor2空间上。

 

  1. 然后清理掉Eden和刚才是用过的survivor1空间。

 

  1. 如果复制到survivior2中的对象太多,survivior2空间放不下,就需要依赖年老代进行内存分配担保。

 

  1. Survivior2没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过内存担保机制进入年老代。

(内存分配担保不展开讲)

 

优点

可以根据各个年代的特点采用最合适的收集算法。

 

 

缺点

仍然不能控制每次垃圾收集的时间。


原创粉丝点击