乐趣区

关于后端:容易发生内存泄漏的八个场景你都知道吗

内存透露与内存溢出

JVM 在运行时会存在大量的对象,一部分对象是短暂应用的,一部分对象只会短暂应用

JVM 会通过可达性剖析算法和一些条件判断对象是否再应用,当对象不再应用时,通过 GC 将这些对象进行回收,防止资源被用尽

内存透露:当不再须要应用的对象,因为不正确应用时,可能导致 GC 无奈回收这些对象

当不正确的应用导致对象生命周期变成也是宽泛意义上的内存透露

内存溢出:当大量内存透露时,可能没有资源为新对象调配

举例内存透露

接下来将从对象生命周期变长、不敞开资源、扭转对象哈希值、缓存等多个场景举例内存透露

对象生命周期变长引发内存透露

动态汇合类
public class StaticClass {private static final List<Object> list = new ArrayList<>();

    /**
     * 只管这个局部变量 Object 生命周期十分短
     * 然而它被生命周期十分长的动态列表援用
     * 所以不会被 GC 回收 产生内存溢出
     */
    public void addObject(){Object o = new Object();
        list.add(o);
    }
}

类卸载的条件十分刻薄,这个动态列表生命周期根本与 JVM 一样长

动态汇合援用部分对象,使得部分对象生命周期变长,产生内存透露

饿汉式单例模式
public class Singleton {private static final Singleton INSTANCE = new Singleton();

    private Singleton(){if (INSTANCE!=null){throw new RuntimeException("not create instance");
        }
    }

    public static Singleton getInstance(){return INSTANCE;}
}

饿汉式的单例模式也是被动态变量援用,即时不须要应用这个单例对象,GC 也不会回收

非动态外部类

非动态外部类会有一个指针指向外部类

public class InnerClassTest {class InnerClass {}

    public InnerClass getInnerInstance() {return this.new InnerClass();
    }

    public static void main(String[] args) {
        InnerClass innerInstance = null;

        {InnerClassTest innerClassTest = new InnerClassTest();
            innerInstance = innerClassTest.getInnerInstance();
            System.out.println("=================== 内部实例对象内存布局 ==========================");
            System.out.println(ClassLayout.parseInstance(innerClassTest).toPrintable());

            System.out.println("=================== 外部实例对象内存布局 ===========================");
            System.out.println(ClassLayout.parseInstance(innerInstance).toPrintable());
        }

        // 省略很多代码.....
    }
}

当调用外部类实例办法通过内部实例对象返回一个外部实例对象时(调用代码中的 getInnerInstance 办法)

内部实例对象不须要应用了,但外部实例对象被长期应用,会导致这个内部实例对象生命周期变长

因为外部实例对象暗藏了一个指针指向(援用)创立它的内部实例对象

实例变量作用域不合理

如果只须要一个变量作为局部变量,在办法完结就不应用它了,然而把他设置为实例变量,此时如果该类的实例对象生命周期很长也会导致该变量无奈回收产生内存透露(因为实例对象援用了它)

变量作用域设置的不合理会导致内存透露

隐式内存透露

动静数组 ArrayList 中 remove 操作会扭转 size 的同时将删除地位置空,从而不再援用元素,防止内存透露

不置空要删除的元素对数组的增加删除查问等操作毫无影响(看起来是失常的),只是会带来隐式内存透露

不敞开资源引发内存透露

各种连贯: 数据库连贯、网络连接、IO 连贯在应用后遗记敞开,GC 无奈回收它们,会产生内存透露

所以应用连贯时要 应用 try-with-resource 主动敞开连贯

扭转对象哈希值引发内存透露

个别认为对象逻辑相等,只有对象要害域相等即可

一个对象退出到散列表是通过计算该对象的哈希值,通过哈希算法失去放入到散列表哪个索引中

如果将对象存入散列表后,批改了该对象的要害域,就会扭转对象哈希值,导致后续要在散列表中删除该对象,会找错索引从而找不到该对象导致删除失败(极小概率找失去)

public class HashCodeTest {
    /**
     * 假如该对象实例变量 a,d 是要害域
     * a,d 别离相等的对象逻辑相等
     */
    private int a;
    private double d;

    @Override
    public boolean equals(Object o) {if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        HashCodeTest that = (HashCodeTest) o;
        return a == that.a &&
                Double.compare(that.d, d) == 0;
    }

    @Override
    public int hashCode() {return Objects.hash(a, d);
    }

    public HashCodeTest(int a, double d) {
        this.a = a;
        this.d = d;
    }

    public HashCodeTest() {}

    @Override
    public String toString() {
        return "HashCodeTest{" +
                "a=" + a +
                ", d=" + d +
                '}';
    }

    public static void main(String[] args) {HashMap<HashCodeTest, Integer> map = new HashMap<>();
        HashCodeTest h1 = new HashCodeTest(1, 1.5);
        map.put(h1, 100);
        map.put(new HashCodeTest(2, 2.5), 200);

        // 批改要害域 导致扭转哈希值
        h1.a=100;

        System.out.println(map.remove(h1));//null

        Set<Map.Entry<HashCodeTest, Integer>> entrySet = map.entrySet();
        for (Map.Entry<HashCodeTest, Integer> entry : entrySet) {System.out.println(entry);
        }
        //HashCodeTest{a=100, d=1.5}=100
        //HashCodeTest{a=2, d=2.5}=200
    }
}

所以说对象当作 Key 存入散列表时,该对象最好是逻辑不可变对象,不能在外界扭转它的要害域,从而无奈扭转哈希值

将要害域设置为 final,只能在实例代码块中初始化或结构器中

如果要害域是援用类型,能够用 final 润饰后,对外不提供扭转该援用要害域的办法,从而让外界无奈批改援用要害域中的值(如同 String 类型,所以 String 经常用来当作散列表的 Key)

缓存引发内存透露

当缓存充当散列表的 Key 时,如果不再应用该缓存,就要手动在散列表中删除,否则会产生内存透露

如果应用的是 WeakHashMap,它外部的 Entry 是弱援用,当它的 Key 不再应用时,下次垃圾回收就会回收掉,不会产生内存透露

public class CacheTest {private static Map<String, String> weakHashMap = new WeakHashMap<>();
    private static  Map<String, String> map = new HashMap<>();
    public static void main(String[] args) {
        // 模仿要缓存的对象
        String s1 = new String("O1");
        String s2 = new String("O2");
        weakHashMap.put(s1,"S1");
        map.put(s2,"S2");

        // 模仿不再应用缓存
        s1=null;
        s2=null;

        // 垃圾回收 WeakHashMap 中存的弱援用
        System.gc();
        try {TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {e.printStackTrace();
        }

        // 遍历各个散列表
        System.out.println("============HashMap===========");
        traverseMaps(map);
        System.out.println();
        System.out.println("============WeakHashMap===========");
        traverseMaps(weakHashMap);
    }

    private static void traverseMaps(Map<String, String> map){for (Map.Entry<String, String> entry : map.entrySet()) {System.out.println(entry);
        }
    }
}

后果

留神: 监听器和回调 也应该像这样成为弱援用

总结

这篇文章介绍内存透露与内存溢出的区别,并从生命周期变长、不敞开资源、扭转哈希值、缓存等多方面举例内存透露的场景

内存透露是指当对象不再应用,然而 GC 无奈回收该对象

内存溢出是指当大量对象内存透露,没有资源再给新对象调配

动态汇合、饿汉单例、不合理的设置变量作用域都会使对象生命周期变长,从而导致内存透露

非动态外部对象有隐式指向内部对象的指针、应用汇合不删除元素等都会隐式导致内存透露

遗记敞开资源导致内存透露(try-with-resource 主动敞开解决)

应用散列表时,充当 Key 对象的哈希值被扭转导致内存透露(key 应用逻辑不可变对象,要害域不能被批改)

缓存引发内存透露(应用弱援用解决)

最初(一键三连求求拉~)

本篇文章将被支出 JVM 专栏,感觉不错感兴趣的同学能够珍藏专栏哟~

本篇文章笔记以及案例被支出 gitee-StudyJava、github-StudyJava 感兴趣的同学能够 stat 下继续关注喔 \~

有什么问题能够在评论区交换,如果感觉菜菜写的不错,能够点赞、关注、珍藏反对一下 \~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

本文由博客一文多发平台 OpenWrite 公布!

退出移动版