一、概述
GC(Carbage Collection)垃圾收集器,由JVM自动回收已死亡的对象垃圾。
这也是Java与C++等语言的主要区别之一。
二、如何确认对象已死
1. 引用计数算法
引用计数法实现简单,效率较高,在大部分情况下是一个不错的算法。
其原理是:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加1,当引用失效时,计数器减1,
当计数器值为0时表示该对象不再被使用。
需要注意的是:引用计数法很难解决对象之间相互循环引用的问题,主流Java虚拟机没有选用引用计数法来管理内存。
1 public class ReferenceCountingGC { 2 public Object instance = null; 3 private static final int ONE_MB = 1024 * 1024; 4 5 private byte[] bigSize = new byte[2 * ONE_MB]; 6 7 public static void main(String[] args) { 8 testGC(); 9 } 10 11 public static void testGC() { 12 ReferenceCountingGC objA = new ReferenceCountingGC(); 13 ReferenceCountingGC objB = new ReferenceCountingGC(); 14 objA.instance = objB; 15 objB.instance = objA; 16 17 objA = null; 18 objB = null; 19 20 System.gc(); 21 } 22 }
2. 可达性分析算法
主流语言(Java、C#)等都是通过可达性分析算法来判断对象是否存活。
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,
当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
三、垃圾收集算法
1. 标记-清除算法
标记-清除算法分为两个阶段:标记阶段和清除阶段。
- 标记阶段的任务是标记出所有需要被回收的对象。
- 清除阶段就是回收被标记的对象所占用的空间。
缺点:
- 标记和清除两个过程效率不高
- 容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
2. 复制算法(用于新生代)
为了解决效率问题,提出来复制算法。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,内存分配时,就不需要考虑内存碎片等问题,
只要移动堆顶指针,按顺序分配内存。实现简单、运行高效。适用于移动较少的情况。
缺点:
- 能够使用的内存缩减到原来的一半。
- 如果存活对象很多,需要很多复制操作,Copying算法的效率将会大大降低。
3. 标记-整理算法(用于老年代)
在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动(记住是完成标记之后,先不清理,先移动再清理回收对象),然后清理掉端边界以外的内存。
4. 分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。
核心思想是根据对象存活的生命周期将堆区划分为新生代和老年代。
新生代每次垃圾收集时,都有大批对象死去,所以用复制算法。
老年代对象存活率较高,使用“标记-清除”算法或者“标记-整理“算法。
新生代和老年代的区别:
将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(一般为8:1:1),每次使用Eden空间和其中的一块Survivor空间,
数据会首先分配到Eden区 当中,当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC。如果对象经过一次Minor GC还存活,
并且又能被Survivor空间接受,那么将被移动到Survivor空 间当中。并将其年龄设为1。
反复将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
对象每熬过一次Minor GC,年龄就加1,
当年龄达到一定的程度(默认为15)时,就会被晋升到老年代 中了,晋升老年代的年龄是可以设置的。如果老年代满了就执行:Full GC 。