天天看点

实战java虚拟机03- 垃圾回收算法

实战java虚拟机

垃圾回收的概念

垃圾回收(Garbage Collection,简称GC).垃圾回收是java体系最重要的组成部分。和C/C++的手工内存管理不同,java虚拟机提供了一套全自动的内存管理方案,尽可能的减少开发人员在内存资源管理方面的工作量。

GC中的“垃圾”是指:存在于内存中、不会再被使用的对象,而“回收”,也相当于把垃圾“倒掉。”

早在C/C++时代,垃圾回收基本上都是手工进行的。开发人员用new关键字进行内存申请,并使用delete关键字进行内存释放。

垃圾回收并不是Java虚拟机独创的,早在20世纪60年代,垃圾回收就已经在Lisp语言中使用。现在除了java外,c#,phyton都是用了垃圾回收的思想。

常用垃圾回收算法

引用计数法(Reference Counting)

引用计数法是最经典也是最古老的一种垃圾收集方法。

引用计数法的实现很简单:对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就+1,当引用失效时,引用计数器就-1。它的实现方式很简单:只需要为每个对象分配一个整型计数器即可。

但是引用计数器有两个非常严重的问题:

1. 无法处理循环引用的情况 —-这一点最为致命,导致java垃圾回收器中,没有使用该算法。

2. 引用计数器在每次引用的产生和消除时,需要伴随一个加法或者减法操作,对性能有一定影响。

循环引用的问题描述如下:有对象A和对象B,对象A引用了对象B,而且对象B也引用了对象A,除此之外没有第三个对象引用了对象A和对象B. 也就是说A和B是应该被垃圾回收的对象,但是由于垃圾对象的相互引用,导致垃圾回收无法识别,引起内存泄露。
实战java虚拟机03- 垃圾回收算法

根对象GC ROOTS的对象主要包含下面几种:

  • 虚拟机栈(栈帧中局部变量表(入参和局部变量))中的引用的对象。
  • 方法区中静态属性引用指向的对象。
  • 方法区中常量引用指向的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。

标记清除法(Mark-Sweep)

标记清除算法是现代垃圾回收算法的思想基础。标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是:

- 标记阶段:首先通过根节点,标记所有从根节点开始的可达对象。因此未被标记的对象就是未被引用的垃圾对象。

- 清除阶段:清除所有未被标记的对象。

标记清除法的缺点是:回收后的空间是不连续的。在对象的分配过程中,尤其是大对象的内存分配,不连续内存空间的工作效率要低于连续的内存空间。

复制算法(Copying)

复制算法的核心是:将援用的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将 正在使用的内存块中的所有存活对象复制到未使用的的内存块中,之后清除正在使用内存块中的所有对象,最后交换两个内存块的角色,完成垃圾回收。

实战java虚拟机03- 垃圾回收算法

如果系统中的存活对象很少,垃圾对象很多,复制算法的效率是很高的。且垃圾回收后,存活对象被复制到新的内存空间中,因此可以确保回收后的内存空间是没有碎片的。但是,复制算法的代价是将系统内存折半,这点是难以让人接受的。

因为在因为年轻代中的对象基本都是存活时间很短,所以在java的新生代串行垃圾回收器中,使用了复制算法。新生代分为eden空间(Java新对象的出生地),from空间和to空间3个部分。其中from和to空间可以视为用于复制的两块大小相同、地位相等且可以进行角色互换的空间块。

from和to空间也称为survivor空间(幸存空间),用于存放未被回收的对象。(eden:from = eden:to 默认比例为8:1)

在GC时,eden空间中存活对象会被复制到survivor空间(假设是to),正在使用的 survivor空间(假设是from)中的年轻对象也会复制到to空间(大对象,或者老年对象会直接进入老年代(-XX:MaxTenuringThreshold来设置年龄阈值)),如果to空间已满,则对象也会进入年老代),这次GC后,Eden区和From区被清空。然后,“From”和“To”会交换他们的角色。

实战java虚拟机03- 垃圾回收算法
转自:转载自并发编程网 – ifeve.com本文链接地址: 聊聊JVM的年轻代
实战java虚拟机03- 垃圾回收算法

一个对象的这一辈子

我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

标记压缩算法(Mark-Compact)

复制算法是建立在存货对象少,垃圾对象多的前提下,这种算法多使用在新生代,但是在年老代,通常大部分对象是存活对象,所以复制算法不适用于老年代的回收。

标记压缩算法是一种老年代的回收算法,它是在标记清除算法的基础上增加了一些优化。

和标记清除算法一样,标记压缩算法首先也是需要从根节点开始,对所有可达对象做一次标记,但是之后它并不只是简单的清除未标记对象,而是将所有的存货对象压缩到内存的一端,之后清理边界之外的所有空间。这种算法即避免了碎片的产生, 又不需要两块相同的内存空间。

实战java虚拟机03- 垃圾回收算法

标记压缩算法最终效果等同于标记清除算法之后再进行一次内存碎片整理,因此它也可以成为Mark-Sweep-Compact算法

分代算法(Generational Collecting)

分代算法的思想是:将内存区间根据对象按照生命周期的长短划分为几块,根据每块内存区间的特点,使用不同的 回收算法。

java虚拟机将所有的新建对象都放入新生代内存区域,新生代的特点是对象朝生夕死,大约90%的新建对象会被很快回收,这种新生代比较适合复制算法。当一个对象经历了几次gc后依然存货,对象会被放入成为年老代的内存空间。在年老代中的对象,存活率非常高,极端情况下可以达到100%,因此可以对年老代进行标记压缩或标记清除算法进行垃圾回收。

实战java虚拟机03- 垃圾回收算法
  • 新生代垃圾回收(Minor GC):回收效率很高,且回收耗时短
  • 老年代回收(Major GC):回收效率低,而且耗时长

    为了支持高频率的新生代回收,虚拟机提供了一种叫作卡表(Card Table)的数据结构。卡表为一个比特位集合,每一个比特位可以表示老年代的某一区域中的所有对象是否持有新生代对象的引用。如果存在则标记为1,否则标记为0.这样在新生代GC时,就无需花大量时间去扫描所有老年代对象,来确定每一个对象的引用关系,而是只需扫描卡表标记为1的所对应的老年代区域即可。

    实战java虚拟机03- 垃圾回收算法

分区算法(Region)

分区算法,将整个堆划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这种算法的好处,可以控制一次回收多少个小区间。

实战java虚拟机03- 垃圾回收算法

在相同条件下,堆空间越大,一次GC所需要的时间就越长,从而产生的停顿也就越长,GC产生的停顿(STOP THE WORLD).采用分区算法,可以很好的控制GC产生的停顿时间。

可触及性

垃圾回收的基本思想是考察每一个对象的可触及性,即从根节点开始是否可以访问到这个对象,如果可以说明这个对象正在被使用, 否则则说明这个对象已经不再被使用,一般来说,这个对象需要被回收。但是,一个无法触及的对象可能会在某种情况下“复活”自己,如果这样,那么对它的回收就是不合理的。为此需要给对象的可触及性状态一个定义,并规定在什么状态下,才可以安全的回收对象:

  1. 可触及的:从根节点,可以到达这个对象
  2. 可复活的:对象的所有引用都释放(即从根节点不可达),但是对象有可能在finalize()函数中复活。
  3. 不可触及的:对象的final()函数被调用,并且没有复活,那么就会进入不可触及状态。(因为finalize()函数只会被调用一次,所以不可触及对象不可能被复活)

以上3个状态,只有不可触及的状态的对象才可以被回收。

对象的复活

这里给出一个finalize()复活自己的例子:

public class CanReliveObj {
    public static CanReliveObj obj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("CanReliveObj.finalize() called");
        obj = this;
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new CanReliveObj();
        obj = null;
        System.gc();
        Thread.sleep();
        if(obj == null ) {
            System.out.println("obj 是 null");
        }else{
            System.out.println("obj 可用");
        }
        System.out.println("第二次gc");
        obj = null;
        System.gc();
        Thread.sleep();
        if(obj == null ) {
            System.out.println("obj 是 null");
        }else{
            System.out.println("obj 可用");
        }
    }
}
           

上述代码执行结果:

CanReliveObj.finalize() called
obj 可用
第二次gc
obj 是 null
           

可以看到,讲obj设置为null之后,进行gc,结果发现gc兑现刚被复活了。直到第二次gc时,对象才被真正的回收。这是因为在第一次gc时,finalize()调用之前,虽然系统中对象已经被清除了,但是作为实例方法finalize(),对象的this引用依然会被传入方法内部,对象复活,对象又变为可触及状态。而finalize()方法只能被调用一次,所以在第二次gc时,对象再无机会复活,因此被回收。

finalize()函数是一个非常糟糕的模式,再次不推荐使用finalize()释放资源!

finalize()可能会发生引用外泄,在无意中复活对象。

finalize()是被系统调用的,调用时间不明确,推荐在try-catch-finally语句中释放资源。

引用和可触及性的强度

java提供了四个级别的引用:强引用,软引用,弱引用和虚引用。除了强引用外,其他3中引用都可以在java.lang.ref包中找到:

实战java虚拟机03- 垃圾回收算法
  • FinalReference:意味着“最终”引用,它用以实现finalize()方法
  • SoftReference: 软引用。
  • WeakReference:弱引用。
  • PhantomReference:虚引用。

强引用就是程序中一般使用的引用,强引用是可触及的,不会被回收。相对的软引用、弱引用和虚引用是对象是软可触及、弱可触及和虚可触及的,都是可以被回收的。

StringBuffer sb = new StringBuffer("hello world");
StirngBuffer sb2 = sb;
           
实战java虚拟机03- 垃圾回收算法

上面两个例子都是强引用,强引用具备以下特点:

  • 强引用可以直接访问目标对象
  • 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出oom异常,也不会回收强引用所指的对象。
  • 强引用可能会导致内存泄露

软引用

GC未必会回收软引用的对象,但是当内存资源紧张的时候,软引用对象会被回收,所以软引用对象不会引起内存溢出。

//-Xmx10m
    private static void soft() throws InterruptedException {
        byte[] b = new byte[  *  * ];
        SoftReference<byte[]> soft = new SoftReference<byte[]>( b );
        b  = null;
        System.out.println("soft:"+soft.get());//soft:[[email protected]
        System.gc();
        System.out.println("soft:"+soft.get());//soft:[[email protected]

        byte[] b2 = new byte[  *  * ];
        System.gc();
        System.out.println("soft:"+soft.get());//soft:null
    }
           

将上述代码中的:

输出结果:

soft:[B@1ff9dc36
soft:[B@1ff9dc36
soft:[B@1ff9dc36
           

弱引用

弱引用是一种比较弱的引用类型。在系统GC时,只要发现弱引用,不管系统堆空间使用情况如何,都会对对象进行回收。

private static void weak() throws InterruptedException {
        byte[] b = new byte[ ];
        WeakReference<byte[]> weak = new WeakReference<byte[]>(b);
        b = null;
        System.out.println("weak:"+weak.get());//打印 weak:[[email protected]
        System.gc();
        System.out.println("weak:"+weak.get());//打印 weak:null
    }
           

虚引用

虚引用是所有引用中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能会被垃圾回收期回收。当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚引用必须要和引用队列以前使用,它的作用在于追踪垃圾回收的过程。

private static void phantom() throws InterruptedException {
        byte[] b = new byte[ ];
        PhantomReference<byte[]> phantom = new PhantomReference<byte[]>(b, new ReferenceQueue<byte[]>());
        b = null;
        System.out.println("phantom:"+phantom.get()); //打印 phantom:null
        System.gc();
        System.out.println("phantom:"+phantom.get());//打印 phantom:null
    }
           

垃圾回收停顿—STOP THE WORLD

垃圾回收器的任务是识别和回收垃圾对象进行内存清理。为了让垃圾回收期正常且高效的执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程的执行,只有这样,系统中才不会有新的垃圾对象,同时停顿保证了系统再某一瞬间的一致性,也有助于垃圾回收器更好的标记垃圾对象。停顿产生时,庚哥应用程序会被卡死,没有任何响应,这个停顿也叫做“STOP THE WORD”(STW)

继续阅读