Java垃圾收集器

GC(垃圾收集器)算是Java语言的一大特色,不同于C/C++要我们手动释放内存,GC能够帮我们回收90%以上的“垃圾”。下面就来介绍一下垃圾收集器。

1. Java中如何定义一个“垃圾” 2. 什么是“垃圾”收集器 3. 如何收集“垃圾” 4. 线程“垃圾”和非线程“垃圾” 5. 弱引用和软引用

1.Java中如何定义一个“垃圾”


所谓垃圾,就是内存中不再有用的对象。

对象成为垃圾的条件:

  • 对于非线程来说,当所有的活动线程(指以及启动但还没有死亡的线程)都不可能访问到该对象时,该对象便成为垃圾。
  • 对于线程对象来说,处理满足上述条件外,还要求此线程本身已经死亡或者还处于新建状态。

光看上面的文字肯定看的模模糊糊,下面我们举例来看看

对于非线程的单个对象,要使其成为垃圾,只需要将指向该对象的所有引用不再指向它即可。下面三种情况都可以实现

  • 将指向该对象的引用设为null
  • 将引用指向别的对象
  • 随着语句块或代码块的退出局部引用消亡
// 创建字符串对象,并将引用s指向该对象
String s = new String();
// 将引用s设为null
s = null;
// 创建值为100的Integr对象,并将引用i指向该对象
Integer i = new Integer(100);
// 将引用i指向另一个对象
i = new Integer(200);

public void someFunction() {
    Double d = new Double(26.77);
    System.out.println(d);
}

上面的三段代码分别对应三种情况。 对于非线程的多个对象,我们来看看一段代码

Rubbish a = new Rubbish("孤岛对象1"); 
Rubbish b = new Rubbish("孤岛对象2"); 
Rubbish c = new Rubbish("孤岛对象3"); 
a.brother = b; 
b.brother = c; 
c.brother = a; 
// 断绝环与外界的关系,形成孤岛 
a = null; 
b = null; 
c = null;

上述代码创建了三个Rubbish对象,有三个引用a,b,c分别指向它们,在对象创建完成后,分别让他们的成员brother指向另一个Rubbish对象,最后将三个对象置为null。这个例子告诉我们,有引用指向的对象不一定不是垃圾,关键要看这些对象能不能被活动线程访问到,显然,上述的brother被完全孤立了,所有即使有引用指向它们,也是垃圾。

关于线程对象成为垃圾的情况,我们在后面用代码说明。

2.什么是“垃圾”收集器


垃圾收集器其实就是一个后台守护进程,在内存充足的情况下,它的优先级很低,一般不出来运行,当内存中有很多垃圾时,它的优先级就会变高,并出来清理垃圾,正因为如此,垃圾收集器的运行时间是没有保障的。

我们都知道Java中垃圾收集器是由系统自动运行的,那如果我们程序员自己想要它运行呢?还是有办法的,人还能让尿憋死啊。不过申请了不一定成功。

一般有两种方法申请运行垃圾收集器

  • 使用Runtime类中的gc()方法。
  • 使用System类中的gc()方法。

Runtime的构造方法是private类型的,所以我们不能直接构造Runtime对象,只能通过特定的对象工厂方法来获取该类的对象,可以通过getRuntime()方法获取。而System就不同了,gc()是它的静态方法,我们可以直接进行调用,所以一般我们都使用System.gc();

/**
 * @author liu
 * @version 创建时间:2018年3月30日 下午8:11:55
 * 申请垃圾收集器运行
 * 1.可使用Runtime的gc()方法
 * 2.可使用System的静态方法gc()
 * 因为使用Runtime还需要调用getRuntime创建Runtime对象,所有一般用第二种
 */
public class Test01 { 
    public static void main(String[] args) { 
        // 获得当前应用程序的Runtime对象 
        Runtime rt = Runtime.getRuntime(); 
        // 输出当前JVM使用的总内存 
        System.out.println("当前JVM使用的总内存为:" + rt.totalMemory()); 
        // 申请垃圾回收器运行 
        // rt.gc();
        System.gc(); 
        try { 
            // 休眠主线程,提高申请垃圾回收器运行的成功率 
            Thread.sleep(100); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
        System.out.println("创建10000000个垃圾对象前JVM可用的内存为:" + rt.freeMemory()); 
        // 创建10000000个rubbish 
        for(int i = 0; i < 10000000; i++) { 
            new String("rubbish"); 
        } 
        System.out.println("创建10000000个rubbish后JVM可用内存:" + rt.freeMemory()); 
        // 申请垃圾回收器运行 
        System.gc(); 
        try { 
            // 休眠主线程,提高申请垃圾回收器运行的成功率 
            Thread.sleep(100); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
        // 申请垃圾回收器后可使用的内存 
        System.out.println("申请垃圾回收器后JVM可用内存为:" + rt.freeMemory()); 
    }
}

输出

当前JVM使用的总内存为:16252928
创建10000000个垃圾对象前JVM可用的内存为:15739232
创建10000000个rubbish后JVM可用内存:11609984
申请垃圾回收器后JVM可用内存为:15739544

3.如何收集“垃圾”


在Object类中有一个finalize()方法,而Object类又是所有类的直接或间接父类,所以每个类都有一个finalize()方法。所以垃圾在被回收前,都会调用该方法。如果我们要在对象成为垃圾被回收前进行一些操作,就可以重写该方法。

既然能重写finalize()方法,那自然就会涉及到一个问题,如果我们在重写的finalize()方法中添加一些代码,在对象成为垃圾被回收之前,阻止对象被回收,比如把成为垃圾的对象重新指向另一个引用,那么就会造成恶意常驻的现象。

为了避免上述情况的发生,Java规定,在一个对象的生命周期内,finalize()方法只会被调用一次。这是什么意思呢?就是当垃圾收集器要回收垃圾对象之前,调用finalize()方法,如果没有回收成功,那么第二次垃圾收集器回收这个垃圾对象,就不再调用它的finalize()方法,而是直接回收。

class Father { 
    // 重写finalize 
    public void finalize() throws Throwable { 
        // 调用object类的finalize方法 
        super.finalize(); 
        System.out.println("This is father."); 
    }
}
class Son extends Father { 
    public void finalize() throws Throwable { 
        // 调用父类的finalize方法 
        super.finalize(); 
        System.out.println("This is son."); 
    }
}
public class Test02 { 
    public static void main(String[] args) { 
        // 使该对象成为垃圾对象 
        new Son(); 
        // 申请垃圾回收器 
        System.gc(); 
        try { 
            // 使主线程休眠,提供申请垃圾回收器的成功率 
            Thread.sleep(200); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
    }
}

输出

This is father.
This is son.

4.线程垃圾和非线程垃圾

前面已经介绍了非线程的对象成为垃圾的情况,但没有用代码说明,现在我们用孤岛现象来描述一下

class Rubbish {
    // 定义一个自身类型的引用 
    Rubbish brother; 
    // 定义字符串常量 
    String name; 
    // 无参构造函数 
    public Rubbish() {} 
    /**
     * 有参构造函数
     * @param name
     */ 
    public Rubbish(String name) { 
        this.name = name; 
    } 
    /**
     * 重写finalize方法
     */ 
    public void finalize() throws Throwable { 
        System.out.println(this.name + "对象成为垃圾"); 
    }
}
public class Island { 
    public static void main(String[] args) { 
        Rubbish a = new Rubbish("孤岛对象1"); 
        Rubbish b = new Rubbish("孤岛对象2"); 
        Rubbish c = new Rubbish("孤岛对象3"); 
        a.brother = b; 
        b.brother = c; 
        c.brother = a; 
        // 断绝环与外界的关系,形成孤岛 
        a = null; 
        b = null; 
        c = null; 
        // 申请垃圾回收器 
        System.gc(); 
        // 使主线程休眠,提供申请的成功率 
        try { 
            Thread.sleep(200); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
    }
}

输出

孤岛对象3对象成为垃圾
孤岛对象2对象成为垃圾
孤岛对象1对象成为垃圾

可以看到,孤岛中的对象都被当做垃圾回收了,这说明,“有引用指向的对象就不是垃圾”这句话是错误的。

对于线程来说,除了要满足一般对象成为垃圾的条件之外,还要求线程没有启动或已经死亡,下面我们也用孤岛来描述。

/**
 * @author liu
 * @version 创建时间:2018年3月31日 上午10:42:27
 * 线程垃圾
 * 执行第一次垃圾收集时,线程a还没有死亡,岛上的对象还可以被a访问,所以没有形成孤岛
 * 执行第二次垃圾收集时,线程a已经死亡,整个孤岛成为垃圾,三个线程都被收集
 */
class RubbishThread extends Thread { 
    RubbishThread brother; 
    String name; 
    /**
     * 无参构造方法
     */ 
    public RubbishThread() {} 
    /**
     * 带参构造方法
     * @param name
     */ 
    public RubbishThread(String name) { 
        this.name = name; 
    } 
    /**
     * 重写run方法,定义线程执行体
     */ 
    public void run() { 
        System.out.println(this.name +"开始执行"); 
        // 对线程进行延迟,使其在足够时间是活着的 
        try { 
            Thread.sleep(100); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
        System.out.println(this.name + "执行结束"); 
    } 
    /**
     * 重写finalize方法
     */ 
    public void finalize() { 
        System.out.println(this.name + "对象成为垃圾"); 
    }
}
public class Test03 { 
    public static void main(String[] args) { 
        RubbishThread a = new RubbishThread("孤岛线程1"); 
        RubbishThread b = new RubbishThread("孤岛线程2"); 
        RubbishThread c = new RubbishThread("孤岛线程3"); 
        a.brother = b; 
        b.brother = c; 
        c.brother = a; 
        // 启动一个线程 
        a.start(); 
        // 形成孤岛 
        a = null; 
        b = null; 
        c = null; 
        System.out.println("---------对无引用但活着的线程进行垃圾收集----------"); 
        System.gc(); 
        try { 
            Thread.sleep(200); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
        // 线程进入死亡状态后申请垃圾收集 
        System.out.println("-------------对无引用并死亡的线程进行垃圾收集----------"); 
        System.gc(); 
        try { 
            Thread.sleep(100); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
    }
}

输出

---------对无引用但活着的线程进行垃圾收集----------
孤岛线程1开始执行
孤岛线程1执行结束
-------------对无引用并死亡的线程进行垃圾收集----------
孤岛线程3对象成为垃圾
孤岛线程2对象成为垃圾
孤岛线程1对象成为垃圾

5.弱引用和软引用


弱引用

试想一下这种情况,一个对象在使用后就不可避免的会成为垃圾,那如果将来某一个时间我们可还要使用,这可怎么办呢?根据前面的描述,一个对象要么是垃圾,要么就不是垃圾,显然前面的知识已经不足以胜任这项工作了。这时候我们使用弱引用—-Java.lang.ref.WeakReference就可以解决问题。

WeakReference类中常用的一个方法是get(),该方法会返回弱引用指向的普通对象。

Java中还有一个WeakHashMap类—–java.util.WeakHashMap,其用法和HashMap相同,只是其中的键都为弱引用。

/**
 * @author liu
 * @version 创建时间:2018年3月31日 上午11:20:10
 * 弱引用
 * 在对象满足垃圾收集的条件后,还可以通过弱引用对其进行访问,但不影响对象成为垃圾
 * 在没有进行垃圾回收之前,弱引用还可以访问,一旦进行垃圾回收,对象就会被清出内存,此时弱引用就不能访问了。
 */
class MyWeakObject { 
    String name; 
    public MyWeakObject(String name) { 
        this.name = name; 
    } 
    public void finalize() { 
        System.out.println(this.name + "满足垃圾回收的条件"); 
    } 
    public void show() { 
        System.out.println(this.name + "对象还可以被调用"); 
    }
}
public class MyWeakObjectTest { 
    public static void main(String[] args) { 
        System.out.println("----------弱引用对象垃圾收集的情况------------"); 
        MyWeakObject a = new MyWeakObject("MyWeakObject1"); 
        // 让弱引用指向创建的MyWeakObject1对象 
        WeakReference<MyWeakObject> wr = new WeakReference<MyWeakObject>(a); 
        // 使MyWeakObject1对象成为垃圾 
        a = null; 
        // 通过弱引用访问MyWeakObject对象 
        ((MyWeakObject)wr.get()).show(); 
        System.out.println("第一次进行垃圾收集"); 
        // 申请垃圾回收器 System.gc(); 
        try { 
            Thread.sleep(200); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
        // 再次通过弱引用访问MyWeakObject对象 
        if(wr.get() != null) { 
            ((MyWeakObject)wr.get()).show(); 
        } 
        System.out.println("--------------弱引用HashMap------------"); 
        // 创建弱引用map对象 
        WeakHashMap<MyWeakObject, String> whm = new WeakHashMap<MyWeakObject, String>(); 
        // 创建MyWeakObject对象 
        MyWeakObject b = new MyWeakObject("MyWeakObject2"); 
        whm.put(b, "xxxxxx"); 
        b = null; 
        // 通过弱引用访问键对象 
        ((MyWeakObject)whm.keySet().iterator().next()).show(); 
        System.out.println("第二次进行垃圾收集"); 
        System.gc(); 
        try { 
            Thread.sleep(200); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
    }
}

输出

----------弱引用对象垃圾收集的情况------------
MyWeakObject1对象还可以被调用
第一次进行垃圾收集
MyWeakObject1满足垃圾回收的条件
--------------弱引用HashMap------------
MyWeakObject2对象还可以被调用
第二次进行垃圾收集
MyWeakObject2满足垃圾回收的条件

软引用

前面说了,软引用指向的对象在成为垃圾还没有被垃圾收集器收集之前,弱引用还可以对其进行调用,但一旦对象被清理出内存,那么就不能调用了。但有时如果我们想在内存没有耗尽之前,就不要把对象给清理,那应该怎么办呢?

这时可以使用软引用—–java.lang.ref.SoftReference,软引用指向的对象在内存没有被耗尽的情况下不会被垃圾收集器回收,具体规则如下:

  • 在内存耗尽之前,垃圾收集器会尝试释放软引用所指向的对象,但在内存耗尽之前不会对软引用所指向的对象进行垃圾收集。
  • 在对软引用指向的对象进行垃圾收集时,垃圾收集器以最近最少使用的顺序释放软引用指向的对象。

软引用中也有一个常用的方法get(),返回软引用指向的普通对象。

/**
 * @author liu
 * @version 创建时间:2018年3月31日 上午11:48:12
 * 软引用
 * 1.无论对象在不计算软引用的情况下是否成为垃圾对象,垃圾收集器在抛出内存耗尽异常之前尝试释放软引用指向的对象
 * 但内存耗尽之前不会对软引用指向的对象进行垃圾收集
 * 2.在对软引用指向的对象进行垃圾收集时,垃圾收集器以最近最少使用的顺序释放引用指向的对象
 */
class MySoftObject { 
    String name; 
    public MySoftObject(String name) { 
        this.name = name; 
    } 
    public void finalize() { 
        System.out.println(this.name + "对象满足垃圾回收的条件"); 
    } 
    public void show() { 
        System.out.println(this.name + "还可以被再次使用"); 
    }
}
public class MySoftObjectTest { 
    public static void main(String[] args) { 
        // 创建一个MySoftObject对象 
        MySoftObject a = new MySoftObject("MySoftObject1"); 
        // 让软引用指向创建的对象 
        SoftReference<MySoftObject> sf = new SoftReference<MySoftObject>(a); 
        // 使对象满足垃圾回收的条件 
        a = null; 
        // 通过软引用调用对象 
        ((MySoftObject)sf.get()).show(); System.gc(); 
        try { 
            Thread.sleep(200); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
        // 再次通过软引用访问对象 
        ((MySoftObject)sf.get()).show(); 
        // 耗尽内存 
        int size = 10000000; 
        String[] sa = new String[size]; 
        for(int i = 0; i < size; i++) { 
            sa[i] = new String("Hello world"); 
        } 
    }
}

输出

MySoftObject1还可以被再次使用
MySoftObject1还可以被再次使用
MySoftObject1对象满足垃圾回收的条件

可以看到申请垃圾收集器后还可以通过软引用调用对象,只有在内存耗尽后,对象才被清理掉。

原文发布于微信公众号 - 秃头哥编程(xp_1311664842)

原文发表时间:2019-05-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券