内存泄漏的8种情况(附代码示例)

一. 内存泄漏(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);
    }
}

### 回答1: 在 C/C++ 程序中,内存泄漏通常是由于程序员在使用动态内存分配函数(如 malloc、calloc、realloc)时疏忽或错误导致的。 一常见的情况是程序员在使用动态内存分配函数申请内存后,忘记调用相应的内存释放函数(如 free)释放内存。如果这情况发生了多次,那么会导致系统内存的浪费,并可能导致程序崩溃。 另一情况是,程序员调用了内存释放函数,但是在调用之后仍然继续使用了已经释放的内存区域。这情况可能会导致程序崩溃或数据错误。 此外,程序员可能会在释放指针时忘记将指针赋值为 NULL,导致内存泄漏。 在 C++ 中,还有另一情况,即程序员使用了 new 关键字动态分配内存,但忘记使用 delete 关键字释放内存。 总的来说,内存泄漏的原因很多,但大多数情况都是由于程序员在使用动态内存分配函数时疏忽或错误导致的。程序员应该牢记内存管理的原则,尽量减少内存泄漏的发生。 ### 回答2: C/C++中可能导致内存泄漏情况有几: 1. 未释放堆内存:在使用`malloc()`、`new`等函数动态分配内存时,必须使用`free()`、`delete`等函数释放内存。如果忘记释放内存或者释放的次数不正确,就会出现内存泄漏的问题。 2. 循环引用:在使用引用计数的方式管理内存时,如果出现循环引用情况即两个对象相互引用,而没有外部引用指向它们,引用计数就无法减到0,导致内存泄漏。 3. 未关闭文件句柄:在使用文件操作函数`fopen()`、`open()`等打开文件时,需要通过`fclose()`、`close()`等函数来关闭文件句柄。如果忘记关闭文件句柄,系统资源将无法释放,造成内存泄漏。 4. 未释放系统资源:除了内存和文件句柄之外,还有其他系统资源也需要手动释放,如数据库连接、网络连接等。如果在使用完这些资源后没有正确释放它们,就会导致内存泄漏。 5. 堆栈不匹配:在使用C/C++的堆栈内存时,需要确保每次`malloc()`或`new`的内存分配与`free()`或`delete`的内存释放是匹配的,否则会发生内存泄漏。 6. 重复分配内存:如果在已有指针变量上多次调用`malloc()`或`new`,而没有对之前分配的内存进行释放,就会导致内存泄漏。 以上是一些常见的C/C++中导致内存泄漏情况,正确管理内存和资源是保证程序运行稳定性和性能的重要一环,开发者需要注意避免这些问题的发生。 ### 回答3: 在C/C++程序开发中,存在几常见的情况会导致内存泄漏: 1. 动态内存分配没有被正确释放:如果在程序中使用malloc、new等方法分配内存,但是忘记释放对应的内存,则会造成内存泄漏。例如,如果在一个循环中重复分配内存但没有释放,最终会耗尽系统内存。 2. 对象生命周期没有被正确管理:在C++中,如果对象的析构函数中没有正确释放申请的资源(如内存、文件、数据库连接等),则会导致内存泄漏。这通常发生在没有及时调用对象的析构函数或者程序逻辑错误导致无法调用析构函数的情况。 3. 全局变量未释放:全局变量会在程序运行期间一直存在,如果在全局变量中分配了动态内存但未释放,那么这部分内存会一直被占用而无法回收,导致内存泄漏。因此,在使用全局变量时,需要注意释放对应的资源。 4. 异常情况未被处理:如果程序存在异常情况,但没有正确处理,导致跳过了内存释放的代码段,就会导致内存泄漏。例如,try-catch块内没有对内存进行释放操作。 5. 循环引用导致内存泄漏:在使用动态内存分配时,如果存在循环引用(两个或多个对象相互引用且没有其他对象引用它们),并且没有采用有效的内存释放策略,就会导致内存泄漏。这情况下需要特别注意对象的析构函数中释放相关的资源。 总之,当程序分配了内存资源但没有及时释放,或者释放不完全时,就会造成内存泄漏。为了避免内存泄漏,需要在程序中正确管理内存的申请和释放,及时释放不再使用的内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值