说明:在阅读本篇之前,需要知道怎么判断对象的存活与否,见《第三章 JVM内存回收区域+对象存活的判断+引用类型+垃圾回收线程》
注意:本文主要参考自《分布式Java应用:基础与实践》,与《深入理解Java虚拟机(第二版)》中的一些说法有一些不同,但是原理一致
1、三种垃圾回收算法
- 标记-清除(年老代)
- 标记-整理(即标记-压缩)(年老代)
- 复制(年轻代)
1.1、标记-清除算法
原理:
- 从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)
适用场合:
- 存活对象较多的情况下比较高效
- 适用于年老代(即旧生代)
缺点:
- 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
- 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)
注意:
1.2、标记整理算法
原理:
- 从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)(可以发现前边这些就是标记-清除算法的原理),清除完之后,将所有的存活对象左移到一起。
适用场合:
- 用于年老代(即旧生代)
缺点:
- 需要移动对象,若对象非常多而且标记回收后的内存非常不完整,可能移动这个动作也会耗费一定时间
- 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)
优点:
- 不会产生内存碎片
注意:
- 在该情况下,内存规整,对象的内存分配采用"指针碰撞法",见《第二章 JVM内存分配》
1.3、复制算法
原理:
- 从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉
适用场合:
- 存活对象较少的情况下比较高效
- 扫描了整个空间一次(标记存活对象并复制移动)
- 适用于年轻代(即新生代):基本上98%的对象是"朝生夕死"的,存活下来的会很少
缺点:
- 需要一块儿空的内存空间
- 需要复制移动对象
注意:
- 在该情况下,内存规整,对象的内存分配采用"指针碰撞法",见《第二章 JVM内存分配》
- 以空间换时间:通过一块儿空内存的使用,减少了一次扫描
2、垃圾回收机制
根据《第一章 JVM内存结构》所说,年轻代分为Eden区和survivor区(两块儿:from和to),且Eden:from:to==8:1:1
1)新产生的对象优先分配在Eden区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入年老代);
2)当Eden区满了或放不下了,这时候其中存活的对象会复制到from区(这里,需要注意的是,如果存活下来的对象from区都放不下,则这些存活下来的对象全部进入年老代),之后Eden区的内存全部回收掉;注意:如果是Eden区没有满,但是来了一个小对象Eden区放不下,这时候Eden区存活对象复制到from区后,清空Eden区,之后刚才的小对象再进入Eden区
3)之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和from区存活下来的对象复制到to区(同理,如果存活下来的对象to区都放不下,则这些存活下来的对象全部进入年老代),之后回收掉Eden区和from区的所有内存;
4)如上这样,会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了
5)当年老代满了或者存放不下将要进入年老代的存活对象的时候,就会发生一次Full GC(这个是我们最需要减少的,因为耗时很严重)
总结:
- 年轻代:复制算法
- 年老代:标记-清除或标记-整理(前者相较于后者会快一些但是会产生内存碎片,后者相较于前者不会产生内存碎片但是由于要移动存活对象所以会慢一些)
- 以上这种年轻代与年老代分别采用不同回收算法的方式称为"分代收集算法",这也是当下企业使用的一种方式
- 每一种算法都会有很多不同的垃圾回收器去实现,在实际使用中,根据自己的业务特点做出选择就好
补充:卡表(《实战java虚拟机》)