您的位置:首页 > 编程语言 > Java开发

Java虚拟机学习随笔(三)

2012-09-06 09:23 169 查看

垃圾收集器

说起垃圾收集器(Garbage Collection,GC),我想大家都并不陌生,GC需要完成的事情有哪些呢?1、哪些内存需要回收?2、什么时候回收?如何回收?其实时日至今,内存的动态分配与内存回收技术已经相当成熟,一切看起来都进入了“自动化”时代,那为什么我们还要去了解GC和内存分配呢?当需要排查各种内存溢出、内存泄露问题时,当垃圾收集器成为系统达到更高并发量的瓶颈时,我们就需要对这些技术实施必要的监控和调节。

上篇内容已经介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法

栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊执行出栈和入栈操作。因此这几个区域的内存分配和回收都具备确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。堆中几乎存放着Java世界中所有的实例对象,GC在对堆进行回收前,第一件事就是要确定哪些对象还“存活”着,哪些已经“死去”。GC是通过什么方法来判断这些对象的状态呢?

Java是使用根搜索算法(GC Roots Tracing)判断对象是否存活的。这个算法是通过一些列的名为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如图所示,对象object5、object6、object7虽然互相关联,但是它们到GC Roots是不可达到的,所以它们将会被判断为是可回收对象。



在Java里,可作为GC Roots的对象包括下面几种:

虚拟机栈中的引用对象;
方法区中的类静态属性引用的对象;
方法区中的常量引用的对象;
本地方法栈中JNI的引用的对象。

由此可看出判断对象是否存活都与“引用”有关,Java将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这四种引用强度依次逐渐减弱。

强引用就是指在程序代码中普遍存在的,类似“Object obj = new Object()”这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用用来表述一些还有用,但并非必要的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存可用,才会抛出内存溢出异常。Java提供了SoftReference类来实现软引用。
弱引用也是用来描述非必要对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无能当前内存是否足够,弱引用关联的对象都会被回收掉。Java提供了WeakReference类来实现弱引用。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。Java提供了PhantomReference类来实现虚引用。
在根搜索算法中不可到达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真在宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖该方法,或该方法已经被虚拟机调用过,虚拟机将这两中情况都视为没有必要执行。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列之中,并稍后由虚拟机自动建立的、低优先级的Finalizer线程去执行。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()中重新与引用链上的任何一个对象建立了关联,那么在第二次标记时它将被移除“即将回收”的集合。

public class FinalizeEscapeGC {

public static FinalizeEscapeGC SAVE_HOOK = null;

public void isAlive() {
System.out.println("yes, i am still alive :)");
}

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}

public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();

// 对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();

// 因为Finalizer方法优先级很低,暂停0.5秒,让它先执行
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}

// 对象第二次拯救自己失败
SAVE_HOOK = null;
System.gc();

// 因为Finalizer方法优先级很低,暂停0.5秒,让它先执行
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
从代码的运行结果可以看到,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集之前成功逃脱。代码中有两段完全一样的代码片段,执行结果确实一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次调用,因此第二段代码的自救行动失败了。

垃圾收集器常用的算法有一下几种:

标记-清除算法

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



复制算法

它将可用的内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。复制算法的执行过程如图所示。



标记-整理算法

“标记-整理”算法的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,该算法适用于老年代区域内存的回收。“标记-整理”算法的执行过程如图所示。



如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。下面看看基于Sun HotSpot虚拟机所包含的收集器有哪些。

 


图中展示了7种作用于不同分代的收集器,如果两个收集器之前存在连线,就说明他们可以搭配使用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: