为什么要回收

每个Java程序中对象都会占用计算机的资源。最常见的,**每个对象都会在堆空间上申请一定的内存空间。**但除了内存之外,对象还会占用其他资源,如文件句柄,端口,socket等等。当你创建一个对象的时候,必须保证它在销毁的时候会释放掉,否则程序将会在OOM中结束。

Java虚拟机中存在自动回收不再使用的对象的机制——GC回收机制

GC简介

复盘笔记

当程序创建对象,数组引用类型实体时,系统都会在堆内存中为之分配一块内存区,对象就保存在这块内存区,当这块内存不再被任何变量引用时,这块内存就变成垃圾,系统就要回收。

  • 只回收堆内存中对象,不会回收物理资源
  • 程序无法精确控制回收时机。
  • 在垃圾回收机制回收任何对象之前,总会先调用它的 finlize() 方法,可能导致垃圾回收机制取消

堆是Java虚拟机进行垃圾回收的主要场所,其次要场所是方法区。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s2R9mnnY-1644547627626)(../img/image-20210812133551959.png)]

为什么需要引用类型

引用类型是与JVM密切相关的。在运行的程序中,GC回收器一直在工作,但你无法在代码中对其进行控制。无法要求垃圾回收器在精确的时间点对某些对象进行回收。

有了引用类型之后,可以在一定程度上对GC回收的粒度进行把控,可以让垃圾回收器在适合的时机回收那些可以被回收的对象,而不仅仅是回收不再使用的对象。

如果有这样一种对象:当内存足够的时候,可以将它们保存在内存中,不进行回收;当内存空间变得紧张时,允许JVM回收这些对象。大部分缓存都符合这样的场景。

引用类型也分为四种:强引用、软引用、弱引用虚引用

如果把GC比作城管,强引用相当于本地人,软引用相当于移民,弱引用相当于黑户口,虚引用相当于得了病的黑户口(不知道什么时候就会die)

四种引用类型与GC之间的关系

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器宁愿抛出OOM(OutOfMemoryError)也不会回收它。

User s = new User();

强可达:如果一个对象与GC Roots之间存在强引用,则称这个对象为强可达对象

当你声明一个变量并指向一个实例的时候,其实就是在创造一个强引用。

强在哪里:这主要体现在JVM进行GC的时候,只要对象有强引用与其关联,就绝对不会对它进行回收,即使已经内存不足了也不会收回有强引用指向的对象。

如果你不需要使用某个对象了,可以将相应的引用设置为null,消除强引用来帮助垃圾回收器进行回收。因为过多的强引用也是导致OOM的罪魁祸首。

s = null;

显式地设置消除引用,或已超出对象的生命周期范围,则JVM会认为该对象不存在引用,这时就可能会回收这个对象。但是具体什么时候收集这要取决于具体的GC算法。

如果在一个方法的内部有一个变量s持有一个对象(Object)的强引用,那么这个变量s保存在栈中,而真正的引用内容(object)保存在堆中。当这个方法运行完成后就会退出方法栈,则引用s也会被销毁,这个object就会被回收。但是当这个s是全局变量时,就需要在不再使用这个对象时赋值为null,因为有强引用关联的对象是不会被垃圾回收的。

内存泄漏

A a = new A();
B b = new B(a);
a = null;

这里a和b是强引用,当把 a = null 时,这时 a 不再指向 A 的地址。讲道理:当某个对象不再与其他引用关联时,就会被 垃圾回收器判定为可回收,在GC中就会被回收掉。但是这里a = null 时,A 对象不能被回收,因为还有一个B对象持有其强引用,这时候就造成了内存泄漏

public static ArrayList<Object> list = new ArrayList<Object>();
public void stackOverflowTest(Object object){
    list.add(object);
    object = null;
}

GC回收的是不可达、弱可达或者虚可达对象,但是,在这个静态集合类对象中,持有了对象的强引用,但是却有可能对象已经不再使用了,所以当非静态对象被静态变量持有强引用的时候,最容易发生内存泄露,在方法中从list获取到对象后赋值给一个变量,使用完之后将这个变量设置为null并不会释放object引用的对象,因为list中还是持有对象的强引用。这时就造成了[内存泄漏]。严重内存泄漏最终会导致OOM

软引用

软引用是使用SoftReference创建的引用,强度弱于强引用,被其引用的对象在内存不足的时候会被回收,不会产生内存溢出。

当一个对象与GC Roots之间存在强引用时,无论何时都不会被GC回收掉。如果一个对象与GC Roots之间没有强引用与其关联而存在软引用关联时,那么垃圾回收器对它的态度就取决于内存的紧张程度了。如果内存空间足够,垃圾回收器就不会回收这个对象,但如果内存空间不足了,它就难逃被回收的厄运。

如果一个对象与GC Roots之间不存在强引用,但是存在软引用,则称这个对象为**软可达(soft reachable)对象**。

在垃圾回收器没有回收它的时候,软可达对象就像强可达对象一样,可以被程序正常访问和使用,但是需要通过软引用对象间接访问,需要的话也能重新使用强引用将其关联。所以软引用适合用来做内存敏感的高速缓存。

String s = new String("AABB");    // 创建强引用与String对象关联,现在该String对象为强可达状态
SoftReference<String> softRef = new SoftReference<String>(s);     // 再创建一个软引用关联该对象
s = null;        // 消除强引用,现在只剩下软引用与其关联,该String对象为软可达状态

s = softRef.get();  // 重新关联上强引用

这里变量s持有对字符串对象的强引用,而softRef持有对该对象的软引用,所以当执行s = null后,字符串对象就只剩下软引用了,这时如果因为内存不足发生Full GC,就会把这个字符串对象回收掉。

注意,在垃圾回收器回收一个对象前,SoftReference类所提供的get方法会返回Java对象的强引用,**一旦垃圾线程回收该对象之后,get方法将返回null。**所以在获取软引用对象的代码中,一定要先判断返回是否为null,以免出现NullPointerException异常而导致应用崩溃。

下面的代码会让s再次持有对象的强引用:

s = softRef.get();

如果在softRef指向的对象被回收前,用强引用指向该对象,那这个对象又会变成强可达。

演示:

先把jvm参数调整一下,并且在控制台打印GC日志

-Xms2m -Xmx2m -Xmn1m -XX:+PrintGCDetails -XX:+PrintGCDateStamps

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rHLltw8g-1644547627628)(../img/image-20210812172155074.png)]

public class Main {
	static class OOMClass{
        private int[] oom = new int[1024 * 100];// 100KB
    }

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<OOMClass> queue = new ReferenceQueue<>();
        List<SoftReference> list = new ArrayList<>();
        while(true){
            for (int i = 0; i < 100; i++) {
                list.add(new SoftReference<OOMClass>(new OOMClass(), queue));
            }
            Thread.sleep(500);
        }
    }
}

​ 使用死循环不断的往list中添加新对象,如果是强引用,会很快因为内存不足而抛出OOM,因为这里的堆内存大小设置为了2M,而一个对象就有100KB,一个循环添加100个对象,也就是差不多10M,显然一个循环都跑不完就会内存不足,而这里,因为使用的是软引用,所以JVM会在内存不足的时候将软引用回收掉。


省略n次Full GC

如果没有使用软引用会怎么样?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y8wdc8zL-1644547627632)(../img/image-20210813093259483.png)]

如果没有使用软引用,在达到内存上线之后,Java head space,也就是堆内存不足,就会报OOM

软引用可以作为缓存

弱引用

弱引用是使用WeakReference创建的引用,弱引用也是用来描述非必需对象的,它是比软引用更弱的引用类型。在发生GC时,只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收。

弱引用,从名字来看就很弱嘛,这种引用指向的对象,一旦在GC时被扫描到,就逃脱不了被回收的命运。

但是,弱引用指向的对象也并不一定就马上会被回收,如果弱引用对象较大,直接进到了老年代,那么就可以苟且偷生到Full GC触发前,所以弱引用对象也可能存在较长的一段时间。一旦一个弱引用对象被垃圾回收器回收,便会加入到一个引用队列中(如果有的话)。

String s = new String("Frank");    
WeakReference<String> weakRef = new WeakReference<String>(s);
s = null;

这里我们把s设置为null后,字符串对象便只有弱引用指向它。

弱可达:如果一个对象与GC Roots之间仅存在弱引用,则称这个对象为弱可达(weakly reachable)对象。

在垃圾回收器回收一个对象前,WeakReference类所提供的get方法会返回其引用对象的强引用,一旦垃圾回收器回收掉该对象之后,get方法将返回null。所以在获取弱引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃。

下面的代码会让s再次持有对象的强引用:

s = weakRef.get();

如果在weakRef包裹的对象被回收前,用强引用关联该对象,那这个对象又会变成强可达状态。

public class WeakReferenceTest {
    private static final List<Object> TEST_DATA = new LinkedList<>();
    private static final ReferenceQueue<TestClass> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        TestClass obj = new TestClass("Test");
        WeakReference<TestClass> weakRef = new WeakReference<>(obj, QUEUE);
        //可以重新获得OOMClass对象,并用一个强引用指向它
        //oomObj = weakRef.get();

        // 该线程不断读取这个弱引用,并不断往列表里插入数据,以促使系统早点进行GC
        new Thread(() -> {
            while (true) {
                TEST_DATA.add(new byte[1024 * 100]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                System.out.println(weakRef.get());
            }
        }).start();

        // 这个线程不断读取引用队列,当弱引用指向的对象呗回收时,该引用就会被加入到引用队列中
        new Thread(() -> {
            while (true) {
                Reference<? extends TestClass> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 弱引用对象被jvm回收了 ---- " + poll);
                    System.out.println("--- 回收对象 ---- " + poll.get());
                }
            }
        }).start();

        //将强引用指向空指针 那么此时只有一个弱引用指向TestClass对象
        obj = null;

        try {
            Thread.currentThread().join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    static class TestClass {
        private String name;

        public TestClass(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "TestClass - " + name;
        }
    }
}

设置虚拟机参数:-Xms4m -Xmx4m -Xmn2m

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mHTh35xk-1644547627634)(../img/image-20210813113533625.png)]

可以看到,其实弱引用也并不是一发生GC就被回收掉了。

WeakHashMap有用到WeakReference

弱引用案例

虚引用

虚引用是使用PhantomReference创建的引用,虚引用也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个。一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用获得一个对象实例。

虚引用,正如其名,对一个对象而言,这个引用形同虚设,有和没有一样。

如果一个对象与GC Roots之间仅存在虚引用,则称这个对象为虚可达(phantom reachable)对象。

当试图通过虚引用的get()方法取得强引用时,总是会返回null,并且,虚引用必须和引用队列一起使用。既然这么虚,那么它出现的意义何在??

别慌别慌,自然有它的用处。它的作用在于跟踪垃圾回收过程,在对象被收集器回收时收到一个系统通知。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。 所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。

如果一个对象没有强引用和软引用,对于垃圾回收器而言便是可以被清除的,在清除之前,会调用其finalize方法,如果一个对象已经被调用过finalize方法但是还没有被释放,它就变成了一个虚可达对象。

与软引用和弱引用不同,显式使用虚引用可以阻止对象被清除,只有在程序中显式或者隐式移除这个虚引用时,这个已经执行过finalize方法的对象才会被清除。想要显式的移除虚引用的话,只需要将其从引用队列中取出然后扔掉(置为null)即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B9dNR7bg-1644547627635)(../img/image-20210813140242085.png)]

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐