Java虚拟机进行垃圾回收,有两个阶段,一是对内存进行标记,二是将未标记的内存进行清除,各个阶段都有不同的算法。
标记阶段
JVM进行内存回收,首先需要区分出内存中哪些是存活对象,哪些是死亡对象。只有被标记死亡的对象,GC才会在执行垃圾回收时进,释放掉其占用的内存空间,这个过程称为垃圾标记阶段
。
判断对象是否存活,一般有两种方式:引用计数算法
和可达性分析算法
- 引用计数算法
引用技术算法,就是对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况。如一个对象A,另一个对象B引用了A,那么A的计数器就+1,当这个引用失效时就-1。对象的引用为0,表示对象不再使用。
该算法的优点:
实现简单,垃圾对象便于标识,判定效率高,回收几乎没有延时。
缺点也很明显:
需要单独的字段存储计数器,增加了存储空间的开销;计数器的加减操作,增加了时间开销;而且,最关键的是无法处理循环引用的情况
。
循环引用:
class A{
B b;//A对象中引用了B对象
}
class B{
A a;//B对象又引用了A对象
}
此时,如果还存在对象C引用了A
class C{
A a;
}
void main(){
A a = new A();
B b = new B();
C c = new C();
c.a = a;//a的引用计数器为1
a.b = b;//b的引用计数器为1
b.a = a;//a的引用计数器为2
//把对象c中的a引用置为null
c.a = null;//a的引用计数器为1
}
在c中的a置为null后,对象a和对象b互相引用,导致没有办法被JVM回收,导致内存泄漏。所以JVM没有使用
。Python中倒是使用了引用计数算法,需要手动解除循环依赖,或者使用弱引用weakref。
- 可达性分析算法(GC Roots)
可达性分析算法也叫根搜索算法,跟踪性垃圾收集。相对于引用计数算法而言,可达性分析算法不仅同样具备简单和执行高效的特点,而且能够有效的解决循环依赖的问题,防止内存泄漏。
可达性分析算法是以跟对象集合(GC Roots)为起始点,从上到下的方式搜索被跟对象集合所连接的目标对象是否可达
。存活的对象都会被根对象集合直接或间接连接着,搜索所走过的路径成为引用链(Reference Chain)。
如果目标对象没有任何引用链相连,则是不可达的,就以为着该对象已经死亡,可以标记为垃圾对象。
GC Roots包括以下几类:
虚拟机栈中引用的对象,如各个线程栈帧中使用到的参数、局部变量等
本地方法栈JNI引用的对象
常量、静态属性引用的对象
被同步锁synchronized持有的对象
Java虚拟机的内部引用,如基本数据类型对应的Class,一些常驻对象(NPE,OOM等),类加载器
反应JVM内部情况的JMXBean,JVMTI中注册回调、本地代码缓存等。
除上述外,还有可能临时性的加入其他的对象,共同构成GC Roots,如分代收集
和局部回收
。
一般来说,Root采用栈的方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但自己又不存放在堆内存中,那么他就是一个Root。
如果只针对堆中的某一块区域进行垃圾回收,必须考虑到这个区域的对象完全有可能被其他区域的对象所引用,这是就需要将关联区域的对象也加入到GC Roots中,才能保证可达性分析的准确性。
使用可达性分析算法, 必须在一个能保证一致性的快照中进行,这也是导致Stop The World的一个重要原因,因为枚举根节点时必须要停顿。
清除阶段
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间。
在这个阶段,有三种常见垃圾收集算法,分别是标记清除算法、标记复制算法和标记压缩算法。
- 标记清除算法(Mark-Sweep)
该算法是一种非常基础和常见的垃圾手机算法,于1960年应用于Lisp语言。
当堆内存的有效空间(availabe memory)被耗尽的时候,就会停止整个程序(Stop The World),然后进行两项工作,一是进行标记、二是进行清除。
标记:收集器(Collector)从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的header中记录为可达对象
。
清除:收集器对堆内存从头到尾进行串行遍历,如果发现某个对象在其header中没有标记为可达对象,则将其清除。 清除并不是真的置空,而是把需要清除的对象的地址保存在空闲的地址列表中,下次需要时,直接覆盖 。
优点:
容易理解、容易实现。
缺点:
需要两次遍历,效率不高;进行GC时整个程序STW,体验较差;产生较多的内存碎片,碎片空间难利用。
- 复制算法(-Coping)
该算法与1963年出现,被引入到了Lisp的另一个实现版本中。
该算法的核心思想是:将内存空间分为两块,每次只使用其中的一块,在垃圾回收时,将正在使用的内存中的存活对象,复制到未使用的内存块中。之后清除正在使用的内存块中的所有对象。交换两个内存块的角色,完成垃圾回收。堆内存中的Survivor区就是基于该算法。
优点:
简单高效、不会造成内存碎片;
缺点:
有一半的内存空间无法被使用;对于G1这种分拆成大量region的GC,复制而不是移动,意味着GC需要维护region之间对象的引用关系,不管是内存占用和时间开销都很大。极端情况下,整个区域所有的对象都存活,那么每个对象都要回收一遍。
应用场景:
内存区域中的垃圾对象很多,复制算法需要复制的存活对象很少(适合新生代使用)。
- 标记压缩算法(Mark-Compact)
在堆的老年代,经GC后存活的对象很多,不适合使用复制算法。而标记清除算法容易产生较多的内存碎片,为此在标记清除算法的基础上,标记压缩算法应运而生。该算法与1970年发布,现在已经广泛应用到各种垃圾收集器中。
该算法的标记阶段和标记清除算法一样。第二阶段则是将所有存活的对象移动到内存的一端,按顺序排放。之后再清除边界外的所有空间。
标记:开销与存活对象成正比
清除:开销与内存区域大小成正比
压缩:开销与存活对象的大小成正比
该算法其实就是在标记清除算法的中间,加入一次内存整理 。
优点:
不会产生内存碎片;能够完整的利用整个内存空间;
缺点:
需要维护对象之间的引用关系;加入了整理阶段,性能较差;移动对象的过程中,需要STW。
- 三种算法的对比
标记清除算法 | 复制算法 | 标记压缩算法 | |
---|---|---|---|
速度 | 中等 | 最快 | 最慢 |
空间开销 | 少,但会产生内存碎片 | 高,需要两倍的内存,不会产生内存碎片 | 少,不会产生内存碎片 |
移动对象 | 否 | 是 | 是 |