最近为大三实习面试也是头疼得很,在复习的时候刚好复习到JVM的垃圾回收机制,得写一篇博客记录一下,不然会忘
简介
垃圾回收机制(Garbage Collection,GC)是java语言的一大特性之一,有了它,不用再像c/c++那样麻烦且频繁地free()和delete()。其实,垃圾回收机制的历史远远要比java的历史久远,因为在1960年诞生的Lisp是第一门应用垃圾回收机制的语言。虽然有了垃圾回收机制帮我们完成垃圾回收,但是我们还是得了解它的运行机制,以便于在开发中排查内存泄露和内存溢出。
垃圾回收机制的基本思路
了解过jvm内存区域的同学都知道,垃圾回收机制主要作用于java堆(Heap),也就是jvm用于存放对象实例的地方,所以很多时候我们也把java堆成为GC堆。在jvm运行时,由于所需要存放的对象越来越多,java堆就得想办法回收已不再用的对象再节省空间,那么垃圾回收机制在进行垃圾回收前就得判断哪些实例已不再使用,哪些实例还要使用,以便于回收不再使用的实例(怎样判断实例是否已死,将在后面介绍),然后回收不再使用的实例,回收过后,新的问题又产生了,那就是回收后的空间不可能是一个连续的空间,要是程序需要分配一块足够大的内存,那就是一个麻烦事了,所以此处根据使用不同的垃圾回收算法,可以分为很多不同的收集器,比如:serial收集器,Parnew收集器,parallel scavenge收集器,CMS收集器和很前沿的G1收集器等。不同收集器有不同收集器的优劣。在垃圾收集过程中,也会不断地产生垃圾,导致一般垃圾回收不干净,而且有时候在回收过程中,实例的引用也会发生变化,所以jvm在回收的时候,会使工作线程暂停下来,然后再回收,然后线程什么时候暂停,暂停后就会产生效率问题,又该怎么处理优化?这个问题我看到博客上分享的大神很少,我也是在《深入理解JVM虚拟机》上面看到过。详情请查看《深入理解JVM虚拟机》。
对象是否已死?
jvm在回收垃圾之前,得先判断哪些实例是不再需要的,不能乱回收。
引用计数算法(Reference Counting)
给每个对象添加一引用计数器,每当有一个地方引用它的时候,计数器+1;引用失效的时候,计数器-1。引用计数算法的实现很简单也很高效。但是,现在主流的虚拟机没有选用引用计数算法来管理内存,主要的原因是它很难解决对象之间相互引用的问题。
public class ReferenceCountingGC{
public Object instance = null;
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC ();
ReferenceCountingGC objB = new ReferenceCountingGC ();
objB.instance = objA;
objA.instance = objB;
objA = null;
objB = null;
System.gc();
}
}
试想一下,要是虚拟机用了引用计数算法,那么上面的objA和objB会不会回收呢?
可达性分析算法(Rearchability Analysis)
在主流的商用的程序语言中,都是称通过可达性分析算法来判断对象是否还存活着。这个算法是通过一系列的“GC roots”的对象作为起点,从这些节点向下搜索,搜索所超过的路径称为引用链,当一个对象到GC roots没有任何引用链相连的时候。则此对象是不可用的。不可用的对象则是JVM判定为可回收的对象。
在java中,可作为gc roots的对象包括下面几种:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
就算是在可达性分析算法是不可达的对象,也不是非死不可,现在它们只是被“怀疑”,要真正宣告一个对象死亡,至少要经过两个阶段:在可达性分析的时候,要是不可达,那么它会被第一次标记并且进行一次筛选,筛选的条件是此对象有没有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过。那么,此对象就没有必要执行finalize()方法。
当一个对象被判定为有必要执行finalize()方法的时候,它就会被放到一个F-Queue的队列中,并且稍后会有一个由虚拟机自动创建的线程来执行它,而且在执行它的时候,虚拟机不保证会等待它运行结束,这是因为要是finalize()里面有死循环或者什么的就麻烦了,就会堵塞队列,有时会导致垃圾回收系统崩溃。稍后GC还会再次对F-Queue进行小规模地第二次标记,如果这次不啻 成功被标记,那么它基本上真的就会被回收了。上面说了,finalize()不保证被完全执行,所以我们在开发中,最好不要用它来关闭资源什么的,因为try finally能更好地完成这项工作,所以建议大家忘记这个方法。
垃圾收集算法
标记--清除算法
顾名思义,先标记再清除,当对象被统一标记后,再统一回收,这是最基本的收集算法,这个算法有几个不足:一是效率不高,标记和清除的效率不高;二是清除后会产生大量碎片,要是程序需要分配一块足够大的内存,那就是一个麻烦事了,这时GC就不得不进行另一个垃圾回收以满足要求。
复制算法
复制算法把内存按容量分为两个相同的块。每次只使用其中的一块,当一块用完后,就将存活的对象复制到另一块,然后再清理已使用的空间。这种算法的缺点就是每次能用的内存只有一半,代价有点高。现在商用的虚拟机都采用这种算法来回收新生代。因为研究发现,新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。
标记--整理算法
复制算法在对象存活率较高的时候效率就很低了,一般存活率很高的老年代就不能用复制算法了。
标记--整理算法的思想和标记--清除算法类似。但后续步骤不是清除可回收的对象,而是把存活的对象都向一端移动,然后直接清除掉边界以外的内存。
分代收集算法
当代商用的虚拟机都是使用的分代收集算法,这种算法没有什么新的思想,就是根据对象存活的情况不同,把内存分为新生代和年老代,新生代对象存活率低,就用复制算法,年老代存活率高,就用标记--清除算法或者标记--整理算法。
参考资料
《深入理解java虚拟机》