首页 > 系统相关 >【java开发】什么是内存溢出和内存泄漏?如何解决?

【java开发】什么是内存溢出和内存泄漏?如何解决?

时间:2024-11-14 22:29:54浏览次数:1  
标签:泄漏 java 内存 import public 溢出

一、内存溢出
   1.1 导致的原因
   1.2 解决方法

二、内存泄漏
   2.1 导致的原因
   2.2 解决方法
   2.3 示例代码

三、对比

四、总结

内存溢出和内存泄漏是我们经常听到的两种内存管理问题,那么,它们是如何导致的?又该如何解决?这篇文章,我们来聊一聊。

一、内存溢出

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

1.1 导致的原因

导致内存溢出主要有以下几个原因:
1、堆内存溢出:创建大量对象,导致堆内存耗尽。
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 来限制元空间大小,以更快地观察到溢出。

1.2 解决方法

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

增加内存:调整 JVM 参数增加堆内存大小,如 -Xmx。
优化代码:减少不必要的对象创建,优化数据结构。
检查递归:避免过深的递归调用。
监控和分析:使用工具如 JVisualVM、JProfiler 分析内存使用情况。

二、内存泄漏

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

2.1 导致的原因

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

静态集合类:使用 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(); // 应该在适当时机清理缓存
        }
    }
}

2.2 解决方法

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

及时释放引用:确保不再使用的对象引用被清除。
使用弱引用:对缓存或非关键对象使用 WeakReference。比如 ThreadLocal 的弱引用会导致内存泄漏,因此使用完 ThreadLocal 一定要记得使用 remove 方法来进行清除。
正确管理生命周期:特别是监听器和回调,确保在不需要时移除。

2.3 示例代码

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

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,内存,import,public,溢出
From: https://www.cnblogs.com/o-O-oO/p/18546991

相关文章

  • 初识JVM,JVM自动内存管理
    文章目录一、前言1.1计算机==>操作系统==>JVM1.1.1虚拟与实体(对上图的结构层次分析)1.1.2Java程序执行(对上图的箭头流程分析)二、JVM内存空间与参数设置2.1运行时数据区2.2关于StackOverflowError和OutOfMemoryError2.2.1StackOverflowError2.2.2OutOfMemoryErr......
  • 面试题——Java中的锁
    文章目录谈谈你对线程安全的理解?1、synchronized关键字是怎么用的?1.1构造方法可以使用synchronized关键字修饰么?1.2使用String作为锁对象,会有什么问题?1.3synchronized的底层原理有了解吗?1.4synchronized怎么保证可重入性?可见性?抛异常怎么办?1.4还使用过其他锁......
  • 每日OJ题_牛客_计算字符串的编辑距离_DP_C++_Java
    目录牛客_计算字符串的编辑距离_DP题目解析C++代码Java代码牛客_计算字符串的编辑距离_DP计算字符串的编辑距离_牛客题霸_牛客网描述:Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个转换成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换......
  • 基于Java+SpringBoot+Mysql在线课程学习教育系统功能设计与实现九
    一、前言介绍:免费获取:猿来入此1.1项目摘要随着信息技术的飞速发展和互联网的普及,教育领域正经历着深刻的变革。传统的面对面教学模式逐渐受到挑战,而在线课程学习教育系统作为一种新兴的教育形式,正逐渐受到广泛关注和应用。在线课程学习教育系统的出现,不仅为学生提供了更加灵......
  • 基于Java+SpringBoot+Mysql在线课程学习教育系统功能设计与实现十
    一、前言介绍:免费获取:猿来入此1.1项目摘要随着信息技术的飞速发展和互联网的普及,教育领域正经历着深刻的变革。传统的面对面教学模式逐渐受到挑战,而在线课程学习教育系统作为一种新兴的教育形式,正逐渐受到广泛关注和应用。在线课程学习教育系统的出现,不仅为学生提供了更加灵......
  • C++函数的返回值在内存中的传递过程及什么是虚拟内存,为什么要使用虚拟内存,虚拟内存可
    1) C++函数的返回值在内存中的传递过程在C++中,函数返回值在内存中的传递过程如下:基本数据类型返回值传递当函数返回基本数据类型(如`int`、`double`、`char`等)时,函数会将返回值存储在一个临时的寄存器中。然后,调用函数的地方会从这个寄存器中获取返回值,并将其存储到相应......
  • Java 数组操作:反转、扩容与缩容
    在Java中,数组是一种固定长度的数据结构,一旦创建,其大小无法更改。然而,常常在实际编程中,我们需要对数组进行扩容、缩容或其他操作。本文将介绍如何通过Java实现数组反转、扩容和缩容的操作,并在代码中演示这些常见的数组操作。1.数组反转数组反转是一个常见的操作,通常用于......
  • Java常见排序算法详解:快速排序、插入排序与冒泡排序
    在程序设计中,排序是最基本的操作之一。Java提供了多种排序算法,今天我们将介绍三种常见的排序方法:快速排序、插入排序和冒泡排序。我们不仅会分析它们的基本原理,还会提供实际的代码实现,帮助大家更好地理解并应用这些排序算法。一、快速排序(QuickSort)快速排序是一种分治法的排......
  • java 反序列化 cc3 复现
    版本要求:jdk版本<=8u65,common-collections版本<=3.2.1在很多时候,Runtime会被黑名单禁用.在这些情况下,我们需要去构造自定义的类加载器来加载自定义的字节码.类加载机制双亲委派这里直接粘别人的了.实现一个自定义类加载器需要继承ClassLoader,同时覆盖findClass方法......
  • java 反序列化 cc4 复现
    复现环境:jdk<=8u65,commonsCollections=4.0CommonsCollections4.x版本移除了InvokerTransformer类不再继承Serializable,导致无法序列化.但是提供了TransformingComparator为CommonsCollections3.x所没有的,又带来了新的反序列化危险.cc4的执行命令部分依然沿用cc3的TemplatesI......