说到 Java, 一定绕不开 GC, 尽管不是 Java 首创的, 但 Java 一定是使用 GC 的代表. GC 就是垃圾回收, 更直接点说就是内存回收. 是对内存进行整理, 从而使内存的使用尽可能大的被复用. 一直想好好写一篇关于 GC 的文章, 可是却发现要写的东西太大了, 不是一篇博客能简单的介绍完的. 所以打算拆分成若干篇博客, 一点点的总结下来. 本篇主要介绍的是 GC 中的常用算法. 这些算法被广泛的应用于各个内存管理语言的虚拟机中, 或者是各大常用的操作系统中. 说到 GC, 也就是垃圾回收, 那么需要做的事有两件:
第一件是查找到哪些内存中的对象是已经废弃掉的.
第二件事是如何清理掉这些已经废弃掉的数据.
本文先来说说后者, 如何清除掉这些内存中的废弃数据.
1, 标记清除算法 Mark-Sweep
这种算法是最简单最直接的算法, 也是其它算法的一些最初思路. 标记清除算法其实就是对内存中的对象依次的进行判断, 如果对象需要回收那么就打一个标记, 如果对象仍然需要使用, 那么就保留下来. 这样经过一次迭代之后, 所有的对象都会被筛选判 (防盗连接: 本文首发自 http://www.cnblogs.com/jilodream/) 断一次. 紧接着会对内存中已经标记的对
象依次进行清除. 这个算法比较简单粗暴, 实现起来比较简单.
但是会留下两个比较麻烦的问题:
(1)标记和清除需要两遍循环内存中的对象, 标记本身也是一个比较麻烦的工作, 因此这种算法的效率不是特别的高.
(2)对于分配的内存来说, 往往是连续的比较好, 因为这样有利于分配大数据的对象. 倘若当前内存中都是小段的内存碎片, 会知道需要分配大段内存时, 没有可以放置的位置, 而触发内存回收. 也就是空间不足而导致频繁 GC 和性能下降.
2, 复制算法 Copying
我在使用数据库的过程中, 曾经遇到这样一个问题, 表中的数据量相对来说比较大, 大概 30 万行, 需要使用多个苛刻的条件删除其中的大部分数据 (因此无法使用索引), 而只保留其中的较少数据. 常规的 delete 语法使用起来是超时的. 于是我查看维护人员的 sql, 发现一个很有意思的逻辑. 首先查出数据库表中需要保留的数据, 放到一张临时表中. 然后彻底删除掉原有的数据表. 然后把这张临时创建表的表名, 改为当初的表名. 这是一种典型的空间换取时间的方法. 而复制算法就是这样一个思路. 复制算法中, 会将内存划分为两块相等大小的内存区域 A/B, 然后生成的数据会存放在 A 区, 当 A 区剩余空间不足以存放下一个新创建的对象时, 系统就会将 A 区中的有效对象全部复制到 B 区中, 而且是连续存放的. 然后直接清空 A 区中的所有对象. 由于编程语言中的对象, 大部分在创建后很快就(防盗连接: 本文首发自 http://www.cnblogs.com/jilodream/ ) 会被回收掉, 所以我们需要复制的对象其实并不多. Java 中的实现是这样的: Java 中将 Eden 和 Survivor 区同时作为复制算法的使用区域. Survivor 又分为 From 区和 To 区. 这块内容可以参考我的另外一篇博客, 博客中有详细的介绍: http://www.cnblogs.com/jilodream/p/6147791.html. 每次 GC 的时候都会将 Eden 和 Survivor 的 From 区中的有效对象进行标记, 一同复制到 Survivor 的 To 区. 然后彻底清除原来的 Eden 区和 From 区的内存对象. 与此同时 To 区就是下一次回收的 From 区.
复制算法的缺点: 算法使用了空间换取时间的思路, 因此需要一块空白的区域作为内存对象要粘贴的区域. 这无疑会造成一种浪费. 尤其是内存较小时. 算法每次清除无效对象时, 都要进行一次复制粘贴的对象转移, 因此对使用场景是有限制的. 只有在有效对象占据总回收内存是非常小的时候, 这种算法的性价比才会达到最高. 否则大量的复制动作所浪费的时间可能要远远大于空间换取时间得到的收益. 因此这种算法在 Jvm 中, 也只被用来作为初级的对象回收. 因为这时的有效对象比例最低, 算法的性价比是最高的.
3, 标记整理算法 Mark-Compact
复制算法需要一块额外的内存空间, 用于存放幸存的内存对象. 这无疑造成了内存的浪费. 我们还可以在原有的标记清除算法的基础上, 提出了优化方案. 也就是标记到的可用对象整体向一侧移动, 然后直接清除掉可用对象边界意外的内存.(防盗连接: 本文首发自 http://www.cnblogs.com/jilodream/ )这样既解决了内存碎片的问题. 又不需要原有的空间换时间的硬件浪费. 由于老年代中的幸存对象较多, 而且对象内存占用较大. 这就使得一旦出现内存回收, 需要被回收的对象并不多, 碎片也就相对的比较少. 所以不需要太多的复制和移动步骤. 因此这种方法常常被应用到老年代中.
标记整理算法的缺点: 标记整理算法由于需要不断的移动对象到另外一侧, 而这种不断的移动其实是非常不适合杂而多的小内存对象的. 每次的移动和计算都是非常复杂的过程. 因此在使用场景上, 就注定限制了标记整理算法的使用不太适合频繁创建和回收对象的内存中.
4, 分代收集算法 Generational Collection
这种算法就是将内存以代的形式划分, 然后针对情况分别使用性价比最高的算法进行处理. 在 Java 中, 一般将堆分为老年代和新生代. 新创建的对象往往被放置在新生代中. 而经过不断的回收, 逐渐存活下来的对象被安置到了老年代中. 越新的对象越可能被回收, 越老的对象反而会存活的越久. 因此针对这两种场景, 新生代和老年代也会分别采用前文所述的两种算法进行清理.
ps: 话说现在写篇博客越来越难了, 今天以为画个图就可以发布了, 结果画图画了一个小时. 哎
来源: https://www.cnblogs.com/jilodream/p/9038853.html