内存溢出和内存泄漏是我们经常听到的两种内存管理问题,那么,它们是如何导致的?又该如何解决?这篇文章,我们来聊一聊。
一、内存溢出
内存溢出(OutOfMemoryError)是指程序在运行时尝试分配内存,但由于没有足够的内存可用,Java 虚拟机(JVM)抛出了OutOfMemoryError错误。常见的内存溢出区域包括堆内存和永久代(在 Java 8 之后被元空间取代)。
1.导致的原因
导致内存溢出主要有以下几个原因:1. 堆内存溢出:创建大量对象,导致堆内存耗尽。2.栈内存溢出:递归调用过深,导致栈内存耗尽。3.永久代/元空间溢出:类加载过多,导致永久代/元空间耗尽。
下面我们用三个示例,分别展示了堆内存溢出、栈内存溢出和永久代/元空间溢出的情况:
(1) 堆内存溢出
如下示例代码,通过不断向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。
(2) 栈内存溢出
如下示例代码,通过递归调用一个没有终止条件的方法,导致栈内存溢出。
public class StackMemoryOverflow {public static void main(String[] args) {recursiveMethod();}public static void recursiveMethod() {// 没有终止条件的递归调用recursiveMethod();}}
运行StackOverflowError代码,通常会很快发生栈内存溢出,因为默认的栈大小不大。
(3) 永久代/元空间溢出
在 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() {@Overridepublic 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来限制元空间大小,以更快地观察到溢出。
2.解决方法
在这里,我们只是给了一个大的思路,关于内存溢出的排查工作也是一个很重要的知识点,我们会在后面的文章中去详细介绍。
二、内存泄漏
内存泄漏(Memory Leak)是指程序中存在一些对象,它们不再被使用,但由于仍然被引用,垃圾回收器无法回收这些对象。因此,随着时间的推移,内存泄漏会导致可用内存逐渐减少,最终可能导致内存溢出。
1.导致的原因
导致内存泄漏主要有以下几个原因:
下面我们用三个示例,分别展示了内存泄漏可能发生的场景:
(1) 静态集合类导致的内存泄漏
静态集合类持有对象引用,导致这些对象无法被垃圾回收。
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();// 这些对象仍然无法被回收,因为它们被静态集合引用}}
(2) 监听器和回调未被移除
注册的监听器或回调未被移除,导致内存泄漏。
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() {@Overridepublic void onEvent() {System.out.println("Event triggered");}});// 假设在某个时候不再需要监听器,但未移除// listeners.remove(listener); // 应该移除不需要的监听器}}interface EventListener {void onEvent();}
(3) 长生命周期对象持有短生命周期对象
长生命周期对象不当持有短生命周期对象的引用,导致短生命周期对象无法被回收。
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[]>
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的内存溢出和内存泄漏并且应示例展示了它们导致的原因,应该说它们是比较常见的内存管理问题,如果在生产环境出现也是比较头疼的问题。所以在日常开发中,我们一定要注意自己的代码风格和代码质量,尽量避免这些问题的发生。
本网站的文章部分内容可能来源于网络和网友发布,仅供大家学习与参考,如有侵权,请联系站长进行删除处理,不代表本网站立场,转载者并注明出处:https://jmbhsh.com/qitabaihuo/33733.html