一. 内存泄漏(memroy leak)

        严格来说,只有对象不会再被程序用到了,但是GC又不能回收它们的情况,才叫内存泄漏

        宽泛的讲,实际情况中很多时候一些不太好的实践会导致对象的生命周期变得很长甚至导致OOM,也叫“内存泄漏”

        申请了内存用完了不释放,如申请了1024M内存,分配了512M内存一直不回收,那么可用内存就只有512M,仿佛泄漏掉一部分。

二. 内存溢出(out of memory)

        申请内存时,没用足够的内存可以使用。 

三. 两者的关系

        内存泄漏的增多,最终会导致内存溢出。

四. 内存泄漏的分类 

        经常发生: 发生内存泄露的代码会被多次执行,每次执行,泄露一块内存
        偶然发生: 在某些特定情况下才会发生
        一次性: 发生内存泄露的方法只会执行一次
        隐式泄漏:  一直占着内存不释放,直到执行结束; 严格的说这个不算内存泄漏,因为最终释放掉了, 但是如果执行时间特别长,也可能会导致内存耗尽

五. 内存泄漏的8种情况

1. 静态集合类

        如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

public class test {
    static List list = new ArrayList<>();

    public void oomTest() {
        Object object = new Object();
        list.add(object);
    }
}

2. 单例模式

        和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

class Singleton {
	private static volatile Singleton instance;
	
	private Singleton() {}
	
	//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
	//同时保证了效率, 推荐使用
	
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			synchronized (Singleton.class) {
				if(instance == null) {
					instance = new Singleton();
				}
			}
			
		}
		return instance;
	}
}

3. 内部类持有外部类

        如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

4. 各种连接,如数据库连接、网络连接和IO连接等

        在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、 Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

public class test {
    public static void main(String[] args) {
        try {
            Connection conn = null;
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("url","","");
            Statement statement = conn.createStatement();
            ResultSet resultSet = statement.executeQuery();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }finally {
            //1. 关闭结果集 statement
            //2. 关闭声明的对象 resultSet
            //3. 关闭连接 Connection
        }
    }
}

5. 变量不合理的作用域
        一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

public class test {
   private  String msg;
   public void receiveMsg(){
       readFromNet();//从网络中接受数据保存到msg中
       saveDB();//把msg保存到数据库中
   }
}

        如上述伪码,msg保存到数据库后已经没用了,但是其生命周期与对象生命相同,则msg还不回收,则造成了内存泄漏。

        修正,将msg变量置于方法内部

  public void receiveMsg(){
       String msg;
       readFromNet();//从网络中接受数据保存到msg中
       saveDB();//把msg保存到数据库中
   }

                或者在使用完msg后,将其置空 

public class test {
   private  String msg;
   public void receiveMsg(){
       readFromNet();//从网络中接受数据保存到msg中
       saveDB();//把msg保存到数据库中
       msg = null;
   }
}

6. 改变哈希值

        当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。

        这也是String 为什么被设置成了不可变类型,我们可以放心地把String 存入HashSet,或者把String当做HashMap的key值。当我们想把自己定义的类保存到散列表的时候,需要保证对象hashCode 不可变。

public class ChangeHashCode1 {
    public static void main(String[] args) {
        HashSet<Point> hs = new HashSet<Point>();
        Point cc = new Point();
        cc.setX(10);//hashCode = 41
        hs.add(cc);

        cc.setX(20);//hashCode = 51  修改了参与计算哈希值的字段,此行为导致了内存的泄漏

        System.out.println("hs.remove = " + hs.remove(cc));//cc的哈希值改变,找不到对象删除false
        hs.add(cc);
        System.out.println("hs.size = " + hs.size());// size = 2 

        System.out.println(hs); // [Point{x=20}, Point{x=20}]
    }

}

class Point {
    int x;

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Point other = (Point) obj;
        if (x != other.x) return false;
        return true;
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                '}';
    }
}

7. 缓存泄漏

        内存泄漏的一个常见来源是缓存,一旦你把对象引用放入到缓存中,就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。|
        对于这个问题,可以使用WeakHashMap(弱引用)代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。

public class MapTest {
    static Map wMap = new WeakHashMap();
    static Map map = new HashMap();

    public static void main(String[] args) {
        init();
        testWeakHashMap();
        testHashMap();
    }

    public static void init() {
        String ref1 = new String("obejct1");
        String ref2 = new String("obejct2");
        String ref3 = new String("obejct3");
        String ref4 = new String("obejct4");
        wMap.put(ref1, "cacheObject1");
        wMap.put(ref2, "cacheObject2");
        map.put(ref3, "cacheObject3");
        map.put(ref4, "cacheObject4");
        System.out.println("String引用ref1,ref2,ref3,ref4 消失");

    }

    public static void testWeakHashMap() {

        System.out.println("WeakHashMap GC之前");
        for (Object o : wMap.entrySet()) {
            System.out.println(o);
        }
        try {
            System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("WeakHashMap GC之后");
        for (Object o : wMap.entrySet()) {
            System.out.println(o);
        }
        System.out.println("wMap中无数据");
    }

    public static void testHashMap() {
        System.out.println("HashMap GC之前");
        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
        try {
            System.gc();
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("HashMap GC之后");
        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
        System.out.println("Map中有数据");
    }

}



8. 监听器和回调

        内存泄漏的常见来源还有监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。

六. 示例

        自定义一个栈,模拟入栈出栈。但是出栈时,只设置指针下移,并没有将出栈数据设为空,即使程序不再使用它们,它们也不会被回收,因为栈中仍然存储着它们的引用,俗称过期引用

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) { //入栈
        ensureCapacity();
        elements[size++] = e;
    }
    //存在内存泄漏
    public Object pop() { //出栈
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

   

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

        修正:出栈时,设置指针下移的同时,将出栈数据设为空

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) { //入栈
        ensureCapacity();
        elements[size++] = e;
    }


    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐