Java 杂谈JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统

JVM学习笔记(4)---垃圾收集器

2018-08-29  本文已影响14人  18587a1108f1

了解垃圾收集器,我们需要搞清三个问题:

哪些内存需要回收?

垃圾收集(Garbage Collection,GC),即常说的GC。
尽管目前“自动化”时代,内存由JVM自动回收,但需要了解其回收机制。当出现内存溢出、泄漏问题时方便排查,当垃圾收集成为系统达到更高并发量的瓶颈时,方便监控调节
由于程序计数器、虚拟机栈、本地方法栈随线程生灭,且每个栈帧分配多少内存都是在类结构确定下来就已知,所以这几个区域的内存分配和回收都有确定性
而堆和方法区的内存分配是 动态 的,这里所讨论的GC即指 堆和方法区 的内存。

什么时候回收?

内存的回收时机主要在于确定 对象是否还通过任何途径被使用

引用计数算法(已经淘汰的算法)

引用计数算法是给每个对象添加一个引用计数器,每当一个地方引用它,计数器就加一;引用失效时,计数器就减一。
其特点是:算法很简单,判定效率高。但它不能解决相互引用的问题。
看如下示例代码:

public class ReferenceCountingGC {

    public Object instance = null;

    //这个成员属性单纯用于内存占用,方便查看是否回收
    private byte[] bigSize = new byte[2*1024*1024];

    public static void testGC(){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();

        objA.instance = objB;
        objB.instance = objA;

        //清除引用
        objA = null;
        objB = null;

        //GC 观察objA与objB能否被回收
        System.gc();
    }

    public static void main(String[] args) {

      testGC();

    }
}

对象ObjA与ObjB互相引用,但再无其他引用。如果采用引用计数算法,GC就无法回收他们。
主流的JVM中,没有采用引用计数法,就是因为它无法解决对象间循环引用的问题。

运行上段代码,我们可以看到内存 4328k->644k 的变化,说明对ObjA和ObjB的内存进行了回收,也说明Java虚拟机中没有采用引用计数算法。

运行结果
可达性分析算法

主流商用程序语言(Java,C#等)中,都是通过 可达性分析(Rechability Analysis) 判断对象存活。

可达性分析
可达性分析是通过一系列"GC Roots"作为起始点,向下开始搜索,所经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何链相连,则对象不可用。

Java中,可作为GC Roots的对象包括以下几种:

虚拟机栈中引用的对象 (栈帧中的局部变量表)
本地方法栈中JNI引用的对象 (JNI即Native方法)
方法区中静态属性引用的对象 (static)
方法区中常量引用的对象 (final)

对象引用

判断对象的存活,无论是引用计数法计算数量,还是可达性分析法判断引用的可达性,都与 引用 有关。

引用可分为 4 类:

  • 强引用(Strong Reference):形如 Object obj = new Object(),只要引用在,GC不回收对象。
  • 软引用(SoftReference):内存溢出前,会进行二次回收,适合做缓存
  • 弱引用(WeakReference):下一次GC前,会直接进行回收
  • 虚引用(PhantomRefence):仅用于对象回收时发送系统通知

强引用
强引用的对象,不会被GC回收;删除强引用后,GC才会回收。
示例代码如下:

/**
 * 强引用:GC不会回收对象
 */
public class StrongRef {

    public static void main(String[] args) throws InterruptedException {
        Referred strong = new Referred();
        System.gc();
        Thread.sleep(2000);
        //删去引用
        strong = null;
        System.out.println("删除强引用后");
        System.gc();
        Thread.sleep(2000);
    }

    static class Referred {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("GC时引用对象被收集");
        }
    }
}

finalize()函数:如果一个对象覆盖了finalize()函数,则在对象被回收时,finalize()方法会被GC收集器触发。每个对象的finalize()函数只会被系统调用一次。

运行结果如下:

删除强引用后
GC时引用对象被收集

当强引用存在时,GC时对象没被回收;当强引用被删除,GC时对象被回收。


软引用
软引用的对象,在内存溢出前,会进行二次回收,这种特性非常适合做缓存。

示例代码如下:

/**
 * VM args: -Xmx100m -Xms100m
 *
 */
public class SoftRef {

    public static void main(String[] args) throws InterruptedException {

        SoftReference<Referred> soft = new SoftReference<Referred>(new Referred());

        System.gc();
        Thread.sleep(2000);

        System.out.println("开始堆占用");

        try {
            List<SoftRef> heap = new ArrayList<>();
            while (true) {
                heap.add(new SoftRef());
            }
        } catch (OutOfMemoryError e) {
            // 软引用对象应该在这个之前被收集
            System.out.println("内存溢出");
        }
    }
    static class Referred {

        @Override
        protected void finalize() throws Throwable {
            System.out.println("GC时引用对象被收集");
        }
    }
}

运行结果如下:

开始堆占用
GC时引用对象被收集
内存溢出

当内存足够,GC时不回收软引用对象;当内存快溢出时,自动二次回收软引用对象。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。


弱引用
被弱引用的对象,如果只有弱引用与之关联,则GC时,对象会被回收掉;
如果存在强引用同时与之关联,则GC时,不会回收该对象。(对一条对软引用也适用)

示例代码如下:
代码1:对象只有弱引用,gc直接被回收

/**
 * 弱引用,对象只有弱引用,gc直接被回收
 * 
 */
public class WeakRef1 {
    public static void main(String[] args) {
        WeakReference<String> weak = new WeakReference<>(new String("weakRef"));
        System.out.println(weak.get());
        System.gc();
        System.out.println(weak.get());
    }
}

运行结果为:

weakRef
null

代码2:对象有弱引用,也有强引用,gc不被回收;删除强引用后,gc回收

/**
 * 弱引用,下一次gc即回收对象
 * 
 */
public class WeakRef2 {

    public static void main(String[] args) throws InterruptedException {

        String s = new String("weakRef");
        WeakReference<String> weak = new WeakReference<>(s);
        System.out.println(weak.get());

        System.gc();
        Thread.sleep(2000);
        System.out.println(weak.get());

        s = null;
        System.gc();
        Thread.sleep(2000);
        System.out.println(weak.get());
    }
}

运行结果为:

weakRef
weakRef
null

只有弱引用的对象,GC时直接回收
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。


虚引用
虚引用不影响对象的生命周期,如果一个对象仅持有虚引用,有它和没有任何引用一样。
它需要和引用队列联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

示例代码如下:

/**
 * 虚引用 与引用队列联合使用
 */
public class PhantomRef {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }

}

运行结果为:

null


方法区回收

之前所谈的对象回收主要在堆内存,方法区尽管回收效率低,也是有垃圾需要回收的。主要分为两部分:废弃常量无用的类
回收常量池和回收堆中的对象类似,比如“abc”加入常量池,没有任何String对象引用常量池中的“abc”,那么就要回收。常量池中的其他类(接口)、方法、字段符号引用也与此类似。
无用类的判断需要满足3个条件:

  • 堆中不存在该类的任何实例,所有该类实例都被回收。
  • 加载该类的ClassLoader已经被回收。
  • 类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

在大量使用反射、动态代理、Cglib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

如何回收?

标记-清除算法

“标记-清除”(Mark-Sweep)算法:先通过可达性分析算法 标记 需要回收的对象,再统一 清除 标记对象。
缺点是:过程效率低;会产生大量不连续的空间碎片

标记-清除
复制算法

“复制”(Copying)算法:将内存分为两块,每次只使用一块,当一块用完了,将存活对象复制到另一块上面。
优点是:简单高效;没有碎片
缺点是:内存缩小一半,硬件代价高
由于复制效率考虑,适用于对象存活率低的新生代。

复制
标记-整理算法

“标记-整理”(Mark-Compact)算法:先标记回收对象,之后让存活对象向一端移动,直接清理掉端边界以外的内存。适合于对象存活率高的老年代。

标记-整理
分代算法

根据对象存活周期,把Java堆分为新生代和老年代,根据各个年代的特点采用最合适的收集算法。在新生代中,每次垃圾收集时有大批对象死去,只有少量存活,可以选用复制算法。而老年代对象存活率高,使用标记清除或者标记整理算法。

HotSpot的算法实现

由于本篇已经写了很长了,所以把目前主流使用的HotSpot JVM如何实现垃圾收集的,放到下一篇文章进行单独分析。

上一篇下一篇

猜你喜欢

热点阅读