• 150403

    文章

  • 892

    评论

  • 13

    友链

  • 最近新加了换肤功能,大家多来逛逛吧~~~~
  • 喜欢这个网站的朋友可以加一下QQ群,我们一起交流技术。

JVM的内存区域划分 + 垃圾回收机制


一、JVM内存区域划分

垃圾回收器主要回收的是堆区中未使用的内存区域并对相应的区域进行整理。在堆区中,又根据对象内存的存活时间或者对象大小,分为“年轻代”和“年老代”。“年轻代”中的对象是不稳定的易产生垃圾,而“年老代”中的对象比较稳定,不易产生垃圾。

根据不同区域的内存块的特点,采取不同的内存回收算法,从而提高堆区的垃圾回收的效率。

二、常见的内存回收算法简介

 

1、引用计数式内存回收

引用计数(Reference Count)式内存回收机制是Objective-C以及Swift语言中正在使用的内存回收机制。

只要有引用,那么引用计数就加1。当引用计数为0时,该块内存就会被回收。这种内存清理方式容易形成“引用循环”

在Objective-C的引用计数中循环引用而造成内存泄露的问题,可以将变量声明成weak或者strong类型。当出现“强引用循环”时,我们将其中的一个引用设置为weak类型即可,然后这种强引用循环就被打破了,也就不会造成“内存泄露”的问题。

在左边的栈中的a、b、c三个引用分别指向堆中的不同区域块。在堆中的内存区域块中,该区域有一个强引用时,其retainCount就会加1。而在弱引用时,就retainCount就不会加1。

先来看看a引用的第1块内存区域,因为该内存块只有a在强引用,所以retainCount=1,当a不在引用该内存区域时,retainCount=0,该内存会理解被回收的。这种情况下是不会造成内存泄露的。

再来看看b指向的内存区域2。b和内存块3都强引用了内存块2,所以2的retainCount=2。而内存块2也强引用了内存块3,所以3的retainCount=1。所以b指向的这块内存区域就存在“强引用循环”,因为当b不再指向这块内存区域时,rc=2就会变为rc=1。因为retainCount不为零,所以这2块内存区域是不会被释放的,2不会被释放,那么自然而然的3块内存区域也不会被释放,但是这块内存区域不会再被使用到了,所以就会造成“内存泄露”的情况。

像c引用的这块情况,就不会引起“强引用循环”,因为其中的一个引用链是是弱引用的。当c不在引用第4块内存时,rc由1变为0,那么该块区域就会被立即释放。而内存块4被释放后,内存块5的rc由1变为0,内存块5也会被释放掉。这种情况下是不会引起内存泄露的。而在Objective-C中正是采用的这种方式来回收内存的,当然了,在OC中除了“强引用”和“弱引用”外,还有自动释放池。也就是说,Autorealease类型的引用,让retainCount = 0时,不会被立即释放掉,而是在出自动释放池时才会被释放掉。

2、复制式内存回收

复制式回收其核心就是“复制”,但前提是有条件复制。在垃圾回收时,将“活对象”复制到另一块空白的堆区,然后将之前的区域一并清除。“活对象”就是指沿着对象的引用链可以到“栈”上的对象。当然在将活对象复制到新的“堆区”后,也要将栈区的引用进行修改。

主要将堆分为两大部分,在进行垃圾回收时,会将一个堆上的活对象复制到另一个堆上。堆1区是目前正在使用的区块,堆2区则是空闲区。

在堆1区中未被标记的那些内存块 2、3是要被回收的垃圾对象。而1、4、5是要被复制的“活对象”。因为沿着栈上的a可到达区块1、沿着c可到达区块4、5。而区块2和3虽然有引用,但是不是来自非堆区,也就是2和3的引用都是来自堆区的引用,所以是要被回收的对象。

 

找到了活对象后,接下来要做的就是将活对象进行复制,将其复制到堆2区。复制到堆2区的对象间的内存地址是连续的,如果要分配新的内存空间的话,直接从堆空闲的一段分配即可。这样在分配内存空间时的效率是比较高的。对象复制后,要修改来自“非堆区”的引用地址。

 

复制完毕后,我们直接将堆2区的中的所有内存空间进行回收即可。堆1区清空后,可以接收复制过来的对象了。当对堆2区进行垃圾回收时,会把堆2区的活对象拷贝到堆1区上。

当内存垃圾特别多 ,垃圾回收的效率还是比较高的,因为复制的对象比较少,清除时直接将旧的堆空间进行清理即可;

当垃圾比较少的时候,复制大量的活对象,效率还是比较低的;

堆的存储空间进行分半,总有一半是空闲的,堆空间的利用率不高

3、标记-压缩回收算法

这种算法在垃圾少时的工作效率比较高,而垃圾多的情况下,工作效率反而不高,这就与“复制式”形成了互补。

第一步就是标记,需要将堆区中的“活对象”进行标记。上图活对象是内存区域1和3,将其进行标记。

标记完成后,开始进行压缩了,将活对象压缩到“堆区”的一段,然后将剩余的部分进行清除。上图将1和3这两个活对象进行了压缩。

压缩后,将下方的空间进行Clean,可以分配新的对象。

 

标记-压缩式垃圾回收可充分利用堆区的空间,当垃圾比较少时,效率还是比较高的

如果垃圾太多碎片化严重时,移动的“活对象”较多,效率比较低。

这种方式可以与“复制式”结合使用,根据当前堆区的垃圾状态来选择哪种回收方式。正好与“复制式”形成优势互补。将“复制式”、“标记-压缩式”的回收方式进行整合的算法,就是“分代式”垃圾回收机制。

4、分代式垃圾回收

“分代”即根据对象易产生垃圾的状态或者对象的大小将其分为不同的代,可分为“年轻代”、“年老代”和“永久代”。“永久代”不在堆中。

在堆中,主要把区域分为“年轻代”、“年老代”。

位于“年轻代”的对象内存创建的时间不长,更新比较快,易产生“内存垃圾”,所以 使用“复制式”回收方式效率比较高。

“年轻代”又可分为Eden Space 和Survivor Space 。Eden Space去主要存放那些初次被创建的对象,而Survivor Sprace存放的是从Eden Space幸存下来的“活对象”。在Survivor Space 中又分为form和to两块,用于相互复制对象来进行垃圾清理

而“年老代”中存放的是一些“大对象”以及从Survivor Sprace中存活下来的“对象”,一般到“年老代”的对象比较稳定,产生垃圾较少,针对这种情况,使用“标记-压缩”式回收效率比较高。

三、分代式垃圾回收的具体工作原理

1、垃圾回收前

上图中,在堆中有些已分配的对象内存并没有被栈上引用,这些就是要被回收的对象。堆,整体上分为“年轻代”和“年老代”,而年轻代,有可细分为Eden Space, From以及To三个区域。

2、分代垃圾回收

 

To区域是空白区,可以接受被复制的对象。由于“年轻代”易产生内存垃圾,所以采用“复制式”内存回收的方式。将Eden Space和From两个堆区块中的“活对象”拷贝到To区。拷贝的同时,我们也要修改被拷贝内存的栈引用地址。而对From或者Eden区域的“大对象”存储空间直接将其复制到“年老代”。因为“大对象”在From与To区多次复制的效率比较低,直接将其加入到“年老代”中以提高回收效率。

对于“年老代”的垃圾回收,就采用“标记-压缩”式垃圾回收。首先,先将活对象进行“标记”。

3、垃圾回收后的结果

Eden Space和From中的活对象都被复制到了To区,而且在“年老代”中多出了从From区复制过来的大对象。


695856371Web网页设计师②群 | 喜欢本站的朋友可以收藏本站,或者加入我们大家一起来交流技术!

0条评论

Loading...


发表评论

电子邮件地址不会被公开。 必填项已用*标注

自定义皮肤 主体内容背景
打开支付宝扫码付款购买视频教程
遇到问题联系客服QQ:419400980
注册梁钟霖个人博客