GC机制(Garbage Collection):当一个对象不再被引用的时候,回收它占领的内存空间,以便空间被后来的新对象使用。
怎么判断占领这片内存区域的对象已经不再使用,应该被回收呢?
一、识别垃圾对象
1.1、引用计数法
在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;计数器为0的对象就是不再被使用的对象。
这种方式实现简单,效率较高,但是它无法解决循环引用的问题。那循环引用的对象将永远存在内存中,不会被回收。
1.2、可达性分析法
通过一系列的被称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
二、哪些是GC Roots对象?
-
1、JVM虚拟机栈中的引用的对象(当前栈帧的引用对象,每个java方法都是一个栈帧)。
-
2、方法区中静态引用指向的对象(static 修饰)。
-
3、方法区中常量引用指向的对象(final 修饰)。
-
4、本地方法栈中JNI引用的对象。
三、GC算法
3.1、标记清除算法(mark-sweep)
标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
3.2、复制算法(Coping)
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
复制(Copying)算法实现简单,运行高效且不容易产生内存碎片,但是能够使用的内存缩减到原来的一半。Copying算法的效率跟存活对象数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将大大降低。
3.3、标记整理算法(Mark-Compact)
该算法标记阶段和标记-清除(Mark-Sweep)一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
3.4、分代回收算法
借助前面三种算法实现:分代收集算法是目前大部分JVM的垃圾收集器采用的算法。
核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下,将堆区划分为老年(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数比较少,但是实际中并不是按照1:1的比例来划分新生代的空间,一般来说是将新生代划分为一块较大的Eden区和两块较小的Survivor区。绝大多数新创建的对象分配在Eden区,在Eden区发生一次GC后,存活的对象移到其中一个Survivor区,一旦这个Survivor区已满,存活的对象移动到另外一个Survivor区。然后将之前那个空间已满的Survivor区置为空。经过重复多次这样的步骤后依旧存活的对象将被移到老年代。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。