Skip to content

Latest commit

 

History

History
50 lines (30 loc) · 5.27 KB

2017-12-05.GC 算法.md

File metadata and controls

50 lines (30 loc) · 5.27 KB

一、标记 - 清除算法 (Mark-Sweep)

标记清除算法也是最基础的算法,和它的名字一样,在进行垃圾回收的时候主要分为“标记阶段” 和“清除阶段” : 首先标记出所需要回收的对象,在标记完成后进行统一回收所有被标记的对象。

其他后面的几种算法都是在它的基础上对它做出的改进。标记清除算法在效率是上存在着一定的问题,原因是标记过程与清除过程的效率都不高;另外一方面是空间问题,原因是在标记清除之后会存在大量的不连续的内存碎片,这种情况导致在程序运行过程中分配大对象时因为无法找到连续的足够的内存空间不得不提前进行一次垃圾收集动作。

这里写图片描述

二、复制算法 (Coping)

为了解决回收效率的问题,复制算法出现了。根据对象在 Java 堆中创建的特点: 一般对象在新生代中分配内存,并且新生代中的对象 98% 一般都是“朝生夕死” 的。

根据这个特点将新生代划分成一块较大的 Eden 区和两块小的 Survivor(from 和 to) 区,每次使用 Eden 和其中的一块 Survivor 区。在进行垃圾回收时将 Eden 区和一块 Survivor 中还存活的对象一次性的复制到另一块 Survivor 区上,然后清理掉刚才使用的 Eden 区 和 Survivor 区的内存空间。

但是我们想在每次进行垃圾回收的时候不就有一块空间处于空闲状态了吗,这样不就使其中的一块 Survivor 空间被浪费了吗?事实上 Eden 区和 Survivor 区的大小比例是 8 : 1,也就是 Eden 区占据了新生代中 4/5 的内存空间,而两个 Survivor 区只占到 1/5 的内存空间。

每次在进行垃圾回收的时候使用 Eden 区 和其中的一块 Survivor 区,那么就只有 1/10 的空间被浪费,这点是可以接受的。但是我们也不能保证在进行垃圾回收之后另一块 Survivor 区能够为所有存活下来的对象分配空间,当 Survivor 空间不够用时这时就需要依赖其他的内存空间 (老年代) 进行分配担保。下面是复制算法过程示意图:

这里写图片描述

其中关于 Eden 区域 Survivor 区内存占比的问题我们可以通过打印 GC 日志进行验证。这里我只拷贝了 Java 堆中的内存放分配关系,我们可以看出新生代共有 38400K ,Eden 区占据了 33280K,38400 - 33280 = 5120K。

这 5120K 就是两块 Survivor 区 (其中一块叫 from 区 一块叫 to 区,to 区就是在进行垃圾回收时空闲的一块,在一次回收完成时,from 区与 to 区互换) 总共的内存大小。通过打印结果看出它将两块 Survivor 区的大小算在一起了,不要以为每一块 Survivor 区都有 5120K 空间的大小。下面就来计算一下 Eden 区 是不是占了新生代总内存的 4/5,通过 33280 / 38400 = 86.7%,事实上比 80% 的内存空间还要大一些,博主的 jdk 是 1.8,不知道是不是这个缘故,不过也没有没有关系啦,你只要能理解在使用复制算法进行垃圾回收的时候并没有很大的内存空间被浪费就好了。

Heap
 PSYoungGen      total 38400K, used 998K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)
  eden space 33280K, 3% used [0x00000000d5f00000,0x00000000d5ff9b20,0x00000000d7f80000)
  from space 5120K, 0% used [0x00000000d7f80000,0x00000000d7f80000,0x00000000d8480000)
  to   space 5120K, 0% used [0x00000000d8480000,0x00000000d8480000,0x00000000d8980000)
 ParOldGen       total 87552K, used 4705K [0x0000000081c00000, 0x0000000087180000, 0x00000000d5f00000)
  object space 87552K, 5% used [0x0000000081c00000,0x0000000082098400,0x0000000087180000)

三、标记-整理算法 (Mark - Compat)

通过上面了解我们可以知道使用复制算法时确实效果比标记-清除算法好很多,但是它也有一些局限,就是当处理一些存活率较高的对象时它的效率就会受到影响,当 Survivor 区内存不够时就会在老年代上进行内存分配,所以可见对老年代进行必要的垃圾回收也是很重要的。

根据老年代的点有人提出了一种“标记-整理”的 算法机制,标记的过程与“标记-清除”算法中实现的过程一样,但是后面的步骤不是对可回收对象进行清除而是让所有存活的对象向某一端移动,然后直接清理掉端边界以外的内存。

这里写图片描述

四、分代收集算法 (Generational Collection)

其实这种算法并没有什么其他特别的地方,只是根据对象的存活周期将内存划分为几块。一般还是划分为新生代和老年代,然后根据各个区的特点选择适当的垃圾回收算法进行垃圾回收。由于新生代每次进行垃圾回收的时候都会有大批的对象死去,所以就使用“复制算法”。根据老年代对象存活时间长的特点就必须使用“标记-整理”和“标记-清除”算法进行垃圾回收。

本篇博文中的有关垃圾回收的相关图片均来自 $天使的翅膀$ 这位朋友,感谢!

参考

《深入理解 Java 虚拟机》周志明 著