Java中内存溢出和内存泄漏如何解决

 更新时间:2024年12月04日 10:24:23   作者:这孩子叫逆  
‌内存溢出‌和‌内存泄漏‌是两种常见的内存管理问题,它们都会对程序的性能产生负面影响,本文主要介绍了Java中的内存溢出和内存泄漏问题解决,感兴趣的可以了解一下

内存溢出

内存溢出(OutOfMemoryError)是指程序在运行时尝试分配内存,但由于没有足够的内存可用,Java 虚拟机(JVM)抛出了 OutOfMemoryError 错误。常见的内存溢出区域包括堆内存和永久代(在 Java 8 之后被元空间取代)。

导致的原因

导致内存溢出主要有以下几个原因:1. 2. 堆内存溢出:创建大量对象,导致堆内存耗尽。2. 栈内存溢出:递归调用过深,导致栈内存耗尽。3. 永久代/元空间溢出:类加载过多,导致永久代/元空间耗尽。

下面我们用三个示例,分别展示了堆内存溢出、栈内存溢出和永久代/元空间溢出的情况:

堆内存溢出

如下示例代码,通过不断向 ArrayList 添加对象来耗尽堆内存。

import java.util.ArrayList;
import java.util.List;

public class HeapMemoryOverflow {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true) {
            list.add(new Object());
        }
    }
}

在运行上述 HeapMemoryOverflow 示例时,可能需要调整 JVM 参数以较小的堆大小运行,例如 -Xmx10m,以更快地观察到 OutOfMemoryError

栈内存溢出

如下示例代码,通过递归调用一个没有终止条件的方法,导致栈内存溢出。

public class StackMemoryOverflow {
    public static void main(String[] args) {
        recursiveMethod();
    }

    public static void recursiveMethod() {
        // 没有终止条件的递归调用
        recursiveMethod();
    }
}

运行StackOverflowError代码,通常会很快发生栈内存溢出,因为默认的栈大小不大。

永久代/元空间溢出

在 Java 8 之前,永久代溢出可以通过动态生成大量类来模拟,Java 8 之后,永久代被元空间取代,以下是一个使用 CGLIB 动态生成类的示例,可能导致元空间溢出,需要添加 CGLIB 库依赖。

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MetaspaceOverflow {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(DummyClass.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }

    static class DummyClass {
    }
}

运行 MetaspaceOverflow 示例时,可以使用 JVM 参数 -XX:MaxMetaspaceSize=10m 来限制元空间大小,以更快地观察到溢出。

解决方法

在这里,我们只是给了一个大的思路,关于内存溢出的排查工作也是一个很重要的知识点,我们会在后面的文章中去详细介绍。

  • 增加内存:调整 JVM 参数增加堆内存大小,如 -Xmx

  • 优化代码:减少不必要的对象创建,优化数据结构。

  • 检查递归:避免过深的递归调用。

  • 监控和分析:使用工具如 JVisualVM、JProfiler 分析内存使用情况。

内存泄漏

内存泄漏(Memory Leak)是指程序中存在一些对象,它们不再被使用,但由于仍然被引用,垃圾回收器无法回收这些对象。因此,随着时间的推移,内存泄漏会导致可用内存逐渐减少,最终可能导致内存溢出。

导致的原因

导致内存泄漏主要有以下几个原因:

  • 静态集合类:使用 static 修饰的集合类持有对象引用,因为静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。

  • 监听器和回调:注册的监听器或回调未被移除。

  • 长生命周期对象持有短生命周期对象:长生命周期对象不当持有短生命周期对象的引用。

下面我们用三个示例,分别展示了内存泄漏可能发生的场景:

静态集合类导致的内存泄漏

静态集合类持有对象引用,导致这些对象无法被垃圾回收。

import java.util.ArrayList;
import java.util.List;

public class StaticCollectionLeak {
    // 静态集合持有对象引用
    private static List<Object> objectList = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            // 每次创建一个新对象并添加到静态集合中
            objectList.add(new Object());
        }
        // 即使在这里试图清理掉一些其他的引用
        System.gc();  // 这些对象仍然无法被回收,因为它们被静态集合引用
    }
}

监听器和回调未被移除

注册的监听器或回调未被移除,导致内存泄漏。

import java.util.ArrayList;
import java.util.List;

public class ListenerLeak {
    private List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }

    public void triggerEvent() {
        for (EventListener listener : listeners) {
            listener.onEvent();
        }
    }

    public static void main(String[] args) {
        ListenerLeak leakExample = new ListenerLeak();
        
        // 匿名类创建的监听器对象
        leakExample.addListener(new EventListener() {
            @Override
            public void onEvent() {
                System.out.println("Event triggered");
            }
        });

        // 假设在某个时候不再需要监听器,但未移除
        // listeners.remove(listener); // 应该移除不需要的监听器
    }
}

interface EventListener {
    void onEvent();
}

长生命周期对象持有短生命周期对象

长生命周期对象不当持有短生命周期对象的引用,导致短生命周期对象无法被回收。

import java.util.HashMap;
import java.util.Map;

public class LongLifeCycleLeak {
    private static Map<String, byte[]> cache = new HashMap<>();

    public static void main(String[] args) {
        while (true) {
            // 短生命周期对象
            byte[] data = new byte[1024 * 1024]; // 1MB

            // 长生命周期对象持有短生命周期对象的引用
            cache.put(String.valueOf(System.nanoTime()), data);

            // 需要定期移除不再需要的数据,否则会导致内存泄漏
            // cache.clear(); // 应该在适当时机清理缓存
        }
    }
}

解决方法

在这里,我们只是给了一个大的思路,关于内存泄漏的排查工作也是一个很重要的知识点,我们会在后面的文章中去详细介绍。

  • 及时释放引用:确保不再使用的对象引用被清除。

  • 使用弱引用:对缓存或非关键对象使用 WeakReference。比如 ThreadLocal 的弱引用会导致内存泄漏,因此使用完 ThreadLocal 一定要记得使用 remove 方法来进行清除。

  • 正确管理生命周期:特别是监听器和回调,确保在不需要时移除。

示例代码

下面示例代码,用于测试内存泄漏。

import java.util.HashMap;
import java.util.Map;

public class MemoryLeakExample {
    private static Map<Integer, String> map = new HashMap<>();

    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            map.put(i, "value" + i);
        }
    }
}

在上面的代码中,如果 map 是一个长期存在的静态变量,并且没有及时清理,则可能导致内存泄漏。

对比

关于内存溢出和内存泄漏的比较如下:

  • 触发时机:内存溢出通常在内存耗尽时立即触发,而内存泄漏可能在一段时间后逐渐显现。

  • 影响范围:内存溢出会立即影响程序的可用性,而内存泄漏通常是一个逐步积累的问题。

  • 检测难度:内存溢出较容易检测,而内存泄漏往往需要深入分析和调试。

  • 解决复杂度:内存溢出的解决相对简单,通常通过优化内存使用或增加内存即可。而内存泄漏的解决需要识别并清理不必要的引用,可能涉及更复杂的代码重构。

到此这篇关于Java中内存溢出和内存泄漏如何解决的文章就介绍到这了,更多相关Java 内存溢出和内存泄漏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Java经典算法汇总之冒泡排序

    Java经典算法汇总之冒泡排序

    冒泡排序基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
    2016-04-04
  • Spring控制反转和依赖注入超详细讲解

    Spring控制反转和依赖注入超详细讲解

    控制反转(IoC)与依赖注入(DI)是密切相关的概念,它们通常一起出现在讨论Spring框架时,这篇文章主要介绍了Spring控制反转和依赖注入的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-10-10
  • 如何解决Project SDK is not defined问题

    如何解决Project SDK is not defined问题

    这篇文章主要介绍了如何解决Project SDK is not defined问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • Java mysql详细讲解双数据源配置使用

    Java mysql详细讲解双数据源配置使用

    在开发过程中我们常常会用到两个数据库,一个数据用来实现一些常规的增删改查,另外一个数据库用来实时存数据。进行数据的统计分析。可以读写分离。可以更好的优化和提高效率;或者两个数据存在业务分离的时候也需要多个数据源来实现
    2022-06-06
  • SpringBoot实现优雅停机的正确方法

    SpringBoot实现优雅停机的正确方法

    什么叫优雅停机?就是向应用进程发出停止指令之后,能保证正在执行的业务操作不受影响,直到操作运行完毕之后再停止服务。本文就来和大家聊聊SpringBoot实现优雅停机的正确姿势,希望对大家有所帮助
    2023-01-01
  • 使用webmagic实现爬虫程序示例分享

    使用webmagic实现爬虫程序示例分享

    这篇文章主要介绍了使用webmagic实现爬虫程序示例,需要的朋友可以参考下
    2014-04-04
  • Java多线程正确使用倒计时协调器CountDownLatch方法详解

    Java多线程正确使用倒计时协调器CountDownLatch方法详解

    这篇文章主要为大家介绍了Java多线程倒计时协调器CountDownLatch的正确使用方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • MyBatis-Plus通用枚举自动关联注入的实现

    MyBatis-Plus通用枚举自动关联注入的实现

    本文主要介绍了MyBatis-Plus通用枚举自动关联注入的实现,解决了繁琐的配置,让 mybatis 优雅的使用枚举属性,感兴趣的可以一起来了解一下
    2021-06-06
  • AJAX Servlet实现数据异步交互的方法

    AJAX Servlet实现数据异步交互的方法

    本篇文章主要介绍了AJAX Servlet实现数据异步交互的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • JAVA_基本LDAP操作实例

    JAVA_基本LDAP操作实例

    这篇文章介绍了JAVA_基本LDAP操作实例,有需要的朋友可以参考一下
    2013-09-09

最新评论