JVM——垃圾回收

本文围绕下面四个问题来介绍垃圾回收:

(1)哪些内存需要进行垃圾回收?

(2)什么叫对象已死?怎么判断对象已死?

(3)什么时候进行垃圾回收?

(4)垃圾收集算法有哪些?

到目前为止,内存的动态分配和内存回收技术已经相当成熟,很多语言都能够实现内存的自动分配和回收。我们需要了解GC和内存分配的原因就是为了能更好的对系统进行调优:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统到达更高并发量的瓶颈时,我们就需要对这些自动化的技术实施必要的监控和调节。

在程序计数器、虚拟机栈、本地方法栈这三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而油条不许地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上在类结构确定下来是就已知的,因此这三个区域的内存分配和回收都具备确定性,在这几个区域内不需要过多的考虑回收的问题,因为方法结束或线程结束时,内存自然就跟着回收了。

Java堆和方法区中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建那些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。

判断对象是否存活有两种算法:引用计数算法和根搜索算法

引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器都为0的对象就是不可能再被使用的。引用计数算法实现简单,判定效率也高,但是Java语言中没有选用引用计数算法来管理内存,其主要原因是它很难解决对象之间的相互循环引用问题。看看如下的例子:

 

public class DoSomeTest {
private Object instance = null;
private static final int _oneMB = 1024 * 1024;
/** 这个成员变量的唯一意义就是占点内存 以便查看内存是否被回收 */
   private byte[] bigSize = new byte[2 * _oneMB];

public static void test() {
DoSomeTest objA = new DoSomeTest();
DoSomeTest objB = new DoSomeTest();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}

上面的两个对象objA、objB都相互引用着,导致他们的引用计数都不为0,所以引用计数算法无法通知GC收集器回收它们。

根搜索算法:通过一系列的名为GC Roots的对象作为起始点,当一个对象到GC Roots没有任何引用链接时,则证明此对象是不可用的。


在Java语言中可作为GC Roots的对象包括下面几种:

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

(2)方法区中的类静态属性引用的对象;

(3)方法区中的常量引用的对象;

(4)本地方法栈中JNI(即一般说的Native方法)的引用的对象;

上文中说到了垃圾回收主要是在方法区和Java堆中进行的,在方法区中进行垃圾收集的“性价比”一般比较低(why?)。在Java堆中,尤其在新生代中,常规引用进行一次垃圾收集一般可以回收70%-95%的空间,而永久代的垃圾收集效率远低于此;永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

废弃常量:简单来说,就是没有任何对象引用了该常量,进行垃圾回收时,该常量会被请出常量池。

无用的类:满足下面三个条件,即可称为无用的类:

(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例

(2)加载该类的ClassLoader已经被回收

(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

下面简单介绍几种垃圾收集算法:标记-清除(Mark-Sweep)算法、复制(Coping)算法【针对新生代】、标记-整理(Mark-Compact)算法【针对老年代】、分代收集(Generational Collection)算法。

标记-清除(Mark-Sweep)算法:该算法是最基础的收集算法,分为“标记”和“清除”两个阶段,首先标记处所有要回收的对象,在标记完成后统一回收掉所有被标记的对象,标记过程就是判断对象存活的过程;它有两个缺点:一是标记和清除的效率都不高;二是标记清除之后会产生大量的不连续的内存碎片,碎片太多可能会导致:当程序在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。

复制(Coping)算法:该算法是为了解决标记-清除(Mark-Sweep)算法的效率问题;它将可用内存按容量话费为大小相等的两块,每次只是用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉;这样使得每次都是对其中一块内存进行内存回收,没存分配时也就不用考虑内存碎片等复杂情况,秩序移动堆顶指针,按顺序分配内存即可,实现简单,运行高效;其缺点也显而易见,可使用内存降为原来的一半,另一半内存就浪费了。

标记-整理(Mark-Compact)算法:其标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉那端边界意外的内存。

分代收集(Generational Collection)算法将Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法;在新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;在老年代中因为对象存活率高、没有额外空间堆它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

猜你喜欢

转载自youcp999.iteye.com/blog/2344444