由于垃圾收集算法的实现涉及大量的程序细节, 而且每个平台的虚拟机操作内存的方法又各不相同, 因此博客中不过多的讨论算法的实现, 只是介绍几种算法的思想以及发展.
1, 深入理解 java 虚拟机之 java 内存区域
2, 深入理解 java 虚拟机之对象真的死了吗
1, 标记 - 清除算法
标记清除算法分为 "标记" 和 "清除" 两个阶段, 首先先标记出那些对象需要被回收, 在标记完成后会对这些被标记了的对象进行回收; 如下图:
这种算法的优点在于不需要对对象进行移动操作, 仅对不存活的对象进行操作, 所以在对象存活率较高的情况下效率非常高, 但是从上图模拟的结果来看对象被回收后, 可用的内存并不是连续的, 而是断断续续, 造成大量的内存碎片. 存储对象时要求内存空间时连续的, 所以虚拟机在给新的内存较大的对象分配空间时, 有可能找不到足够大的连续的空闲的空间来存放, 从而引发一次垃圾回收动作, 实际上里面是有大量的空闲空间的, 只是不连续而已.
2, 复制算法
复制算法是将内存分为两块大小一样的区域, 每次是使用其中的一块. 当这块内存块用完了, 就将这块内存中还存活的对象复制到另一块内存中, 然后清空这块内存. 这种算法在对象存活率较低的场景下效率很高, 比如说新生代, 只对整块内存区域的一半进行垃圾回收, 在垃圾回收的过程也不会出现内存碎片的情况, 不需要移动对象, 只需要移动指针即可, 实现简单, 所以运行效率很高. 运行效率是在建立在浪费空间的基础上的, 这是典型的已空间换时间的方法, 因为每次只能是使用北村的一半. 算法示意图如下:
现在商用的 jvm 中都采用了这种算法来回收新生代, 因为新生代的对象基本上都是朝生夕死的, 存活下来的对象约占 10% 左右, 所以需要复制的对象比较少, 采用这种算法效率比较高. hotspot 版本的虚拟机将堆 (heap) 内存分为了新生代和老年代, 其中新生代又分为内存较大的 Eden 区和两个较小的 survivor 区. 当进行内存回收时, 将 eden 区和 survivor 区的还存活的对象一次性地复制到另一个 survivor 空间上, 最后将 eden 区和刚才使用过的 survivor 空间清理掉. hotspot 虚拟机默认 eden 和 survivor 空间的大小比例为 8:1, 也就是每次新生代中可用内存空间为整个新生代空间的 90%(80% 10%), 只会浪费掉 10% 的空间. 当然, 98% 的对象可回收只是一般场景下的数据, 我们没有办法保证每次回收都只有不多于 10% 的对象存活, 当 survivor 空间不够用时, 需要依赖于其他内存 (这里指的是老年代) 进行分配的担保.
3, 标记 - 整理算法
复制算法在对象存活率较高的情况下就要进行较多的对象复制操作, 效率将会变低. 更关键的是, 如果你不需要浪费 50% 的空间, 就需要有额外的空间进行分配担保, 用以应对被使用的内存中所有对象都 100% 存活的极端情况, 所以在老年代一般不能直接选用这种办法.
根据老年代的特点, 有人提出了标记 - 整理的算法, 标记过程仍然与标记 - 清楚算法一样, 但后续步骤不是直接将可回收对象清理掉, 而是让所有存活的对象都向一端移动, 然后直接清理掉端边界以外的内存, 算法示意图如下:
4, 分代收集算法
分代收集算法将 heap 区域划分为新生代和老年代, 新生代的空间比老年代的空间要小. 新生代又分为了 Eden 和两个 survivor 空间, 它们的比例为 8:1:1. 对象被创建时, 内存的分配是在新生代的 Eden 区发生的, 大对象直接在老年代分配内存, IBM 的研究表明, Eden 区 98% 的对象都是很快消亡的.
为了提高 gc 效率, 分代收集算法中新生代和老年代的 gc 是分开的, 新生代发生的 gc 动作叫做 minor gc 或 young gc, 老年代发生的叫做 major gc 或 full gc.
minor gc 的触发条件: 当创建新对象时 Eden 区剩余空间小于对象的内存大小时发生 minor gc;
major gc 触发条件:
1, 显式调用 System.gc()方法;
2, 老年代空间不足;
3, 方法区空间不足;
4, 从新生代进入老年代的空间大于老年代空闲空间;
Eden 区对象的特点是生命周期短, 存活率低, 因此 Eden 区使用了复制算法来回收对象, 上面也提到复制算法的特点是在存活率较低的情况下效率会高很多, 因为需要复制的对象少. 与一般的复制算法不同的是, 一般的复制算法每次只能使用一半的空间, 另一半则浪费掉了, Eden 区的回收算法也叫做 "停止 - 复制" 算法, 当 Eden 区空间已满时, 触发 Minor GC, 清理掉无用的对象, 然后将存活的对象复制到 survivor1 区 (此时 survivor0 有存活对象, survivor1 为空的), 清理完成后 survivor0 为空白空间, survivor1 有存活对象, 然后将 survivor0 和 survivor1 空间的角色对象, 下次触发 Minor gc 时重复上述过程. 如果 survivor1 区剩余空间小于复制对象所需空间时, 将对象分配到老年代中. 每发生一次 Minor gc 时, 存活下来的对象的年龄则会加 1, 达到一定的年龄后(默认为 15) 该对象就会进入到老年代中.
老年代的对象基本是经过多次 Minor gc 后存活下来的, 因此他们都是比较稳定的, 存活率高, 如果还是用复制算法显然是行不通的. 所以老年代使用 "标记 - 整理" 算法来回收对象的, 从而提高老年代回收效率.
总的来说, 分代收集算法并不是一种具体的算法, 而是根据每个年龄代的特点, 多种算法结合使用来提高垃圾回收效率.
参考资料:《深入理解 Java 虚拟机》
来源: https://www.cnblogs.com/rainple/p/10793500.html