内存泄漏的定义:不会再被使用的对象,其占用的内存却不能被回收,这就是内存泄漏。

内存泄漏的本质是:长生命周期的对象持有短生命周期对象的引用

在学习GC的过程中我们知道,JVM在垃圾回收时判断一个对象是否应该被回收,采用的是可达性分析算法。

因此,从这个角度来理解,内存泄漏现象就是应该被回收的无用对象却由于某些原因在可达性分析算法中被判断为可达,因此无法被回收。

内存泄漏的常见情况有以下几种:

1.使用容器造成内存泄漏

对象被加入容器内之后,就由容器对象持有该对象的引用。因此在该对象被使用完成之后,即使将其置为null,该容器依然持有该对象的引用,导致该对象对于GC来说是不可回收的,从而造成内存泄漏。

例如:

class LeakByContainer{
    public void aFunction(){
        Vector v = new Vector();
        for(int i=0;i<10;i++){
            Object o = new Object();
            v.add(o);
            o=null;//此时v仍然持有对该Object 的引用
        }
        /*
        *some steps with o
        */
        
        //此时对象o已经无用,但是由于被v引用,并不能被GC回收,从而造成了内存泄漏。
        
        /*
        *some steps without o
        */
    }
}

解决方法就是,在some steps with o即与对象o有关的操作都结束之后,将Vector对象v赋值为nullv=null,那么他所持有的一系列的Object对象也会一同被回收。

2.变量作用域不合理导致的内存泄漏

如果一个对象定义的作用范围远大于其使用范围,就很可能会造成内存泄漏。举例如下:

class leakByBiggerScope(){
    private Object o;
    public void methodWithString(){
        o = new Object();
        //some other codes;
    }
    public void methodWithOutString(){
        //some codes
    }
}

事实上,在这个例子中,对象o的作用域仅仅限于第一个方法,另外一个方法并不会用到它。但是,第一个方法结束之后o所分配的内存并不会被释放,z只有在整个类的对象被释放时对象o才会被释放,因此造成了内存泄漏。

如果要解决这个问题,可以把该对象设置为第一个方法中的局部变量。也可以在第一个方法结束时将其赋值为null,这样的话在第一个方法结束时该对象就可以被回收。

3.内部类持有外部类的引用

我们看看如下类:

public class Outer{
    public String s = "zzzzz";
    public class Inner{
        public String s = "hahahahaha";
    }
}

将其编译之后,再用IntelliJ打开class文件,可以看到内部类Inner如下:

在这里插入图片描述

可以看出,其实内部类Inner隐含了一个变量this$0,这个变量就是对外部类Outer的引用。因此,当内部类对象的生命周期大于创建其的外部类对象的生命周期时,由于其持有对该外部类对象的引用,导致外部类对象无法释放,就会造成内存泄漏的问题。

一般来说,解决的方法就是将内部类定义为静态。

public class Outer{
    public String s = "zzzzz";
    public static class Inner{
        public String s = "hahahahaha";
    }
}

编译后的class文件用IntelliJ打开如下:

在这里插入图片描述

显然,此时该静态内部类不再持有对外部类的引用。

4.各种提供了close()方法、用于建立连接的对象

如数据库连接、socket连接等,连接对象建立之后,除非显式调用其close()方法将连接关闭,否则连接对象不会被GC回收。

很显然,代码设计者并不清除我们建立连接之后会使用多久,因此只能提供一个方法让我们不使用时主动关闭连接。所以,我们在使用这类对象时,一定要记得在使用完成之后调用close()方法。一个经典写法如下:

public static void main(String[] args) {
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection con = DriverManager.getConnection(url,user,passwd);
        //do something with the connection to sql
    }catch(Exception e){
        e.printStackTrace();
    }finally{
		con.close();
    }
}

将close()语句写在finally块中,就可以保证该连接使用完之后一定被关闭。

5.使用集合时,改变对象的Hash值

如果将一个对象存入一个HashSet,就不能够再去修改该对象的Hash值。否则,将无法用contains()函数去该集合中查找这个对象,也无法用remove()函数从集合中删除这个对象,从而造成内存泄漏。

我们先创建一个类Person,并重写它的hashCode方法和equals方法:

class Person{
    private String name;
    private int gender;
    private int age;
    private String tel;
    public Person(String name,int gender,int age,String tel){
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.tel = tel;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return gender == person.gender && age == person.age && name.equals(person.name) && tel.equals(person.tel);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, gender, age, tel);
    }
}

测试方法如下:

public static void main(String[] args) {
    Person p = new Person("l",1,10,"123456");
    Set<Person> personSet = new HashSet<>();

    personSet.add(p);
    System.out.println(personSet.contains(p));

    p.setName("w");
    System.out.println(personSet.contains(p));
    System.out.println(personSet.remove(p));

    System.out.println(p);
    p=null;
    System.out.println(personSet);
}

结果如下:

在这里插入图片描述

可以看出,在修改了p的参与哈希值计算的字段之后,用contains()方法在personSet中找不到对象p了,用remove()方法也无法移除p。但是,即使将该对象p赋值为null,personSet中仍然持有该对象的引用,因此该对象无法被回收,造成内存泄漏。

6.单例模式造成的内存泄漏

单例模式里的单例对象,生命周期从单例加载开始一直到整个应用结束为止。如果长生命周期的单例对象持有其他短生命周期对象的应用,就会造成内存泄漏。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐