内存溢出和内存泄漏的区别

内存溢出(Out Of Memory,OOM)和内存泄漏的区别

一、概念

内存溢出:
是指程序在申请内存时,没有足够的内存空间供其使用。比如,申请了一个整数的内存,但实际存了一个需要 long 类型来存储的数,这就会导致内存溢出。系统无法满足程序需要的内存大小,导致溢出。

内存泄漏:
是指程序在申请内存后,无法释放已申请的内存空间。虽然单个内存泄漏可能不会带来太大问题,但随着内存泄漏的积累,系统的可用内存空间会逐渐减少。就像一个容器只能装4个水果,但你却放了5个,结果溢出并掉落在地上。内存泄漏最终可能会导致内存耗尽,也就是内存溢出


二、内存溢出和内存泄露区别:

  • 内存溢出指的是当程序需要的内存超过了系统所能提供的内存时,会发生内存溢出。通常情况下,这种情况会导致程序崩溃或者进程被杀死。
  • 内存泄露指的是程序中已经不再需要使用的对象或内存空间却没有被释放,导致这些资源一直被占用,最终导致系统内存耗尽。内存泄露通常会导致程序变慢、崩溃或者死机。

三、内存溢出

3.1、为什么会出现内存溢出?

内存溢出通常是由于程序中存在内存泄露、对象创建过多或者某些操作占用过多内存等问题导致的。
上面说的内存溢出是需要的内存超过了系统能提供的内存,那就说明系统的内存已经没有了,那就可能是内存泄漏(不需要的对象或者内存空间没有释放资源一直被占用着内存,造成内存没有了),对象创建过多(创建的对象太多占用了太多内存)造成的。

好好理解这句话:需要的内存 超过了 系统能提供的内存
那就要从 创建的对象内存 和 系统能提供的内存 理解
系统能提供的内存少了,怎么少的呢?有可能是已经创建的对象占用了内存,怎么占用的呢?过多的数据处理创建对象占用了堆内存大小(可根据缓存解决);还有递归调用深度过大;还有可能是已经创建的对象使用完了一直没有释放占用着内存。
创建的对象内存:有可能是在创建对象的时候本来就是个大的对象,剩余的内存空间根本满足不了这个对象的创建,导致了内存溢出。


3.2、场景具体说明:
  • 1、内存泄漏:应用程序中存在一些无用的对象或者引用没有被及时释放,导致内存占用越来越多,最终超出了堆内存的最大限制。
  • 2、过多的数据:如果一次从数据库取出过多数据,应用程序需要处理大量的数据,而堆内存的大小又无法满足需求,就容易发生内存溢出。
  • 3、递归调用:如果应用程序中存在递归调用的代码,而递归深度过大,也容易导致内存溢出。
  • 4、大对象:如果应用程序需要创建大量的大对象,而堆内存的大小又无法满足需求,也容易导致内存溢出。

内存溢出的示例代码

public class MemoryLeakExample {
    private List<Integer> list = new ArrayList<>();
 
    public void addToList() {
        for (int i = 0; i < 1000000; i++) {
            list.add(i);
        }
    }
 
    public static void main(String[] args) {
        MemoryLeakExample example = new MemoryLeakExample();
        while (true) {
            example.addToList();
        }
    }
}

// 结果:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
/* 在这个示例中,我们创建了一个 ​MemoryLeakExample​类,其中包含一个 ​list​成员变量。
 * 在 ​addToList()​方法中,我们向列表中添加了100万个整数。
 * 在 ​main()​方法中,我们创建了 ​MemoryLeakExample​对象并不断调用 ​addToList()​方法。
 * 由于我们不断向列表中添加元素,并且没有从列表中删除任何元素,因此列表的大小会不断增加,直到最终导致内存溢出。 
*/
// 这里提一点:这就是为什么在程序中为什么尽量不要用 while (true) 去循环
  • 堆内存溢出——频繁创建大对象

    堆内存用于存储Java程序中的对象实例。当程序不断创建对象,但未能及时释放不再使用的对象时,堆内存会逐渐被占满,最终导致内存溢出。

    Java堆内存主要用于存储对象实例。当程序频繁创建大对象未能及时释放时,堆内存可能会被耗尽。以下是一个引起堆内存溢出的典型情况:

    import java.util.ArrayList;
    import java.util.List;
     
    public class HeapMemoryOverflowExample {
        public static void main(String[] args) {
            List<byte[]> byteList = new ArrayList<>();
     
            try {
                while (true) {
                    byte[] byteArray = new byte[1024 * 1024]; // 创建1MB大小的字节数组
                    byteList.add(byteArray);
                }
            } catch (OutOfMemoryError e) {
                System.out.println("Heap Memory Overflow!");
            }
        }
    }
    
    // 在上述代码中,我们通过不断创建1MB大小的字节数组并将其添加到List中,最终导致堆内存溢出。
    
    
  • 栈内存溢出——递归调用

    递归调用时,每次方法调用都会占用一定的栈空间。如果递归深度过大,可能导致栈内存溢出。

    public class StackOverflowExample {
        public static void recursiveFunction() {
            recursiveFunction();
        }
     
        public static void main(String[] args) {
            try {
                recursiveFunction();
            } catch (StackOverflowError e) {
                System.out.println("Stack Overflow!");
            }
        }
    }
    
    // 在这个例子中,递归调用导致栈内存不断增长,最终可能触发栈内存溢出。
    
    

3.3、怎么解决内存溢出?
  • 1、调整JVM虚拟机的堆内存大小(使用 -Xms 和 -Xmx 参数),增加可用内存。
  • 2、优化应用程序代码,避免内存泄漏和过多的数据占用内存。
  • 3、使用分布式缓存或者数据库等外部存储,减少应用程序内存的占用。
  • 4、优化算法,避免递归调用或者大对象的创建。

内存溢出的解决策略

  • 1、优化对象的创建和销毁

    确保不再需要的对象能够及时被销毁,释放占用的内存。使用 try-with-resources、finalize 等机制可以帮助优化资源的管理。

    class Resource implements AutoCloseable {
        // 资源的初始化和操作
     
        @Override
        public void close() throws Exception {
            // 释放资源
        }
    }
     
    public class MemoryOverflowSolution1 {
        public static void main(String[] args) {
            try (Resource resource = new Resource()) {
                // 使用资源
            } catch (Exception e) {
                // 处理异常
            }
        }
    }
    
    // 在上述代码中,Resource类实现了 AutoCloseable 接口,确保在 try 块结束时资源会被自动关闭。
    
    
  • 2、调整堆内存大小

    通过调整JVM的启动参数,可以增大堆内存的大小,提供更多的可用内存

    java -Xmx512m -Xms512m YourProgram
    
    // 这里的 -Xmx 表示最大堆内存,-Xms 表示初始堆内存。根据应用程序的需求和性能要求,可以适当调整这些参数。
    
    
  • 3、使用内存分析工具

    利用内存分析工具如VisualVM、Eclipse Memory Analyzer等,检测内存泄漏和优化内存使用。以下是一个简单的使用VisualVM的示例:

    import java.util.ArrayList;
    import java.util.List;
     
    public class MemoryOverflowSolution3 {
        public static void main(String[] args) {
            List<byte[]> byteList = new ArrayList<>();
     
            try {
                while (true) {
                    byteList.add(new byte[1024 * 1024]); // 模拟频繁创建大对象
                    Thread.sleep(10); // 降低创建速度,方便观察
                }
            } catch (OutOfMemoryError e) {
                System.out.println("Heap Memory Overflow!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 4、避免创建过大的对象

    在设计时避免创建过大的对象,合理设计数据结构和算法,降低内存占用。考虑使用更轻量的数据结构,或者分批处理大数据量。

  • 5、定期清理不再使用的对象

    定期清理不再使用的对象,确保它们能够被垃圾回收。这可以通过手动释放引用或者使用弱引用等机制来实现。

    import java.lang.ref.WeakReference;
     
    public class MemoryOverflowSolution5 {
        public static void main(String[] args) {
            WeakReference<Object> weakReference = new WeakReference<>(new Object());
            // 在适当的时机,可能会被垃圾回收
        }
    }
    
    // 在这个例子中,weakReference 是一个对 Object 对象的弱引用,当没有强引用指向这个对象时,可能会被垃圾回收。
    
    

四、内存泄漏

4.1、为什么会出现内存泄漏?
  • 1、对象未被正确地清理:当一个对象不再被使用时,应该及时将其释放,否则它所占用的内存空间将一直被占用。例如,在Java中,如果一个对象的引用没有被置为空,那么它所占用的内存空间就不会被回收。
  • 2、长生命周期的对象持有短生命周期对象的引用:在某些情况下,一个长生命周期的对象(如单例模式)可能会持有一个短生命周期的对象的引用(如局部变量),导致短生命周期的对象无法被释放。
  • 3、缓存数据过多:如果程序中缓存了大量的数据,而这些数据并不会在后续的执行中被使用到,那么这些数据就会一直占用内存空间,导致内存泄露。
  • 4、循环引用:如果两个对象相互引用,而且没有其他对象引用它们,那么它们所占用的内存空间就不会被释放。

4.2、场景示例:
  • 1、静态集合类

    public class MemoryLeakExample {
        private static List<Object> list = new ArrayList<>();
     
        public void addToList(Object obj) {
            list.add(obj);
        }
     
        public static void main(String[] args) {
            MemoryLeakExample example = new MemoryLeakExample();
            while (true) {
                Object obj = new Object();
                example.addToList(obj);
            }
        }
    }
    // 结果:
    // 	Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
    
    // 解释:
    /* 	在这个示例中,我们创建了一个 ​MemoryLeakExample​类,其中包含一个静态的 ​list​成员变量。
     * 这个类中的 list 是静态的,也就是说只会被实例化一次。如果在 list 中添加元素,但没有及时清空,那么这些对象将一直存在于内存中,导致内存泄漏。
     * 在 ​addToList()​方法中,我们将传入的对象添加到列表中。
     * 在 ​main()​方法中,我们创建了一个 ​MemoryLeakExample​对象,并不断向列表中添加新的对象。
     * 由于我们没有从列表中删除任何对象,而且列表是静态的,因此对象会一直被保存在列表中,导致内存泄露。
     * 如果程序长时间运行,就会导致内存耗尽,并最终导致程序崩溃。*/
    
    
  • 2、没有关闭文件或数据库连接

    public class FileOrDbClass {
      public static void readFile(String fileName) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(fileName));
        String line;
        while ((line = br.readLine()) != null) {
          System.out.println(line);
        }
        br.close();
      }
      
      public static void getConnection() throws SQLException {
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test?" +
                "user=monty&password=greatsqldb");
        // 执行操作
        conn.close();
      }
    }
    
    /* 上述代码中,readFile 方法没有关闭 BufferedReader,而 getConnection 方法也没有关闭 Connection。
     * 如果这些资源没有被适当地关闭,它们将一直存在于内存中。
     * 解决方法:使用 try-with-resources 语句关闭资源。
    */
    
    
  • 3、匿名内部类
    匿名内部类可能导致内存泄漏的原因是,它通常会持有外部类的引用,并且可能会长时间保持这些引用而不释放,这样就会造成内存泄漏。为了证明这一点,如下代码:

    public class AnonymousClassDemo {
        private static Object obj;
     
        public static void main(String[] args) {
            for (int i = 0; i < 10000; i++) {
                obj = new Object() {
                    @Override
                    protected void finalize() throws Throwable {
                        System.out.println("Finalize method called");
                    }
                };
            }
            obj = null;
            System.gc();
            System.out.println("Finish");
        }
    }
    

    在上面的代码中,创建了10000个匿名内部类对象,并在匿名内部类中重写了 finalize() 方法,以便在垃圾回收器对这些对象进行垃圾回收时输出一条消息。然后,将 obj 设置为 null,并请求垃圾回收器进行垃圾回收。

    如果匿名内部类并不导致内存泄漏,那么所有的匿名内部类对象应该都会被垃圾回收器回收,并输出一条消息。但是实际情况并非如此,运行上述代码,会发现程序并没有打印出足够条数的信息。这表明,匿名内部类对象被持续引用而无法被垃圾回收器回收,从而导致内存泄漏。

  • 4、使用ThreadLocal

    ThreadLocal 可能导致线程间的对象引用无法释放,从而引起内存泄漏。以下是一个示例:

    public class MemoryLeakCause4 {
        private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
     
        public static void main(String[] args) {
            threadLocal.set(new Object());
     
            // 在不再使用时未手动调用 threadLocal.remove()
        }
    }
    // 在这个例子中,ThreadLocal 的值在不再使用时未手动清理,可能导致线程间的对象引用无法释放。
    
    
  • 5、使用缓存

    在使用缓存时,如果没有适当的策略来清理过期或不再需要的缓存项,可能导致内存泄漏。以下是一个示例:

    import java.util.HashMap;
    import java.util.Map;
     
    public class MemoryLeakCause5 {
        private static final Map<String, Object> cache = new HashMap<>();
     
        public static void main(String[] args) {
            cache.put("key", new Object());
     
            // 在不再需要时未手动从缓存中移除
        }
    }
    // 在这个例子中,缓存中的对象在不再需要时未手动移除,可能导致内存泄漏。
    
    

    通过深入理解这些导致内存泄漏的原因,并采取相应的解决策略,可以更好地预防和解决内存泄漏问题,提高程序的性能和稳定性。在实际开发中,要谨慎使用和管理对象引用,特别是在容易导致内存泄漏的场景下。


平常程序设计中需要注意什么?

  • 1、及时释放不再需要使用的对象和内存空间。
  • 2、避免长生命周期的对象持有短生命周期对象的引用。
  • 3、合理使用缓存,及时清除不再需要使用的缓存数据。
  • 4、避免循环引用的情况出现。

4.3、内存泄漏(Memory Leak)解决方法

参考连接:https://blog.csdn.net/2201_75809246/article/details/138329458

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值