百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

10个java常见内存泄露场景的模拟和解决方案

yund56 2025-07-28 23:31 4 浏览

模拟内存泄漏的场景是理解和排查内存泄漏问题的一个重要手段,今天就给大家贴一些常见内存泄漏的实例模拟吧!


了解了以下这些示例,在自己写代码和做review时,轻松拿捏内存泄露问题。


实例1:静态集合类导致的内存泄漏


【java】

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

public class StaticCollectionLeak {

    // 使用静态集合类持有对象引用
    private static List<Object> staticList = new ArrayList<>();

    public static void main(String[] args) {
        // 不断向静态集合中添加对象
        while (true) {
            staticList.add(new Object());

            // 模拟任务执行
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            // 打印当前静态集合的大小(可选)
            System.out.println("Static list size: " + staticList.size());
        }
    }
}


说明:静态集合类(如ArrayList)在应用程序的整个生命周期内存在,如果它们持有大量对象的引用,这些对象即使不再需要也无法被垃圾回收,从而导致内存泄漏。


如何避免:

1.及时清理集合:

当确定集合中的对象不再需要时呢,要及时从集合中移除它们哦,这样垃圾回收器才能顺利回收那些不再使用的对象。

2.使用弱引用:

可以考虑使用像 WeakHashMap 这样的弱引用集合呀,这样当对象没有其他强引用时,垃圾回收器就能自动回收它们

3.避免长时间持有对象引用:

尽量不要让静态集合长时间持有大量对象的引用呀,要合理地管理对象的生命周期~


实例2:未关闭的资源导致的内存泄漏

【java】

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;

public class UnclosedResourceLeak {
    public static void main(String[] args) {
        // 不断打开网络连接但不关闭
        while (true) {
            try {
                URL url = new URL("http://example.com");
                URLConnection conn = url.openConnection();
                BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
                String line;
                while ((line = br.readLine()) != null) {
                    // 处理读取的数据(这里只是简单读取并未关闭流)
                }
                // 注意:这里没有关闭BufferedReader和URLConnection,导致资源泄漏
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 模拟任务执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}



说明:未关闭的资源(如文件流、网络连接等)会占用系统资源,如果这些资源没有及时释放,就可能导致内存泄漏。在这个例子中,我们不断打开网络连接但不关闭它们,从而模拟内存泄漏。


如何避免:


1.使用 try-with-resources 语句:

在 Java 里呀,可以用这个语句来自动管理资源呢,它会在代码块执行完毕后自动关闭资源。

2.显式关闭资源:

如果没用那个语句的话,也要记得在用完资源后,显式地调用它们的关闭方法呀,就像关上门一样,要把资源也关好哟。

3.使用资源池:

对于一些经常需要使用的资源呀,可以考虑使用资源池来管理呢,这样资源的复用率会更高,也能减少内存泄漏的风险。



实例3:长生命周期对象持有短生命周期对象导致的内存泄漏

【java】

import java.util.LinkedHashMap;
import java.util.Map;

public class CacheLeak {
    // 使用LinkedHashMap作为缓存
    private Map<Integer, Object> cache = new LinkedHashMap<>(16, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Object> eldest) {
            // 这里只是简单示例,并未实现真正的缓存淘汰策略
            return false;
        }
    };

    public static void main(String[] args) {
        CacheLeak demo = new CacheLeak();
        demo.start();
    }

    private void start() {
        // 不断向缓存中添加对象
        while (true) {
            cache.put((int) (Math.random() * 1000), new Object());
            // 模拟任务执行
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            // 打印当前缓存的大小(可选)
            System.out.println("Cache size: " + cache.size());
        }
    }
}


说明:长生命周期对象(如缓存)如果持有短生命周期对象的引用,并且没有实现合适的缓存淘汰策略,就可能导致内存泄漏。


如何避免:


1.实现合适的缓存淘汰策略:

要给缓存设置一个合适的淘汰策略呀,比如 LRU(最近最少使用)或者 TTL(生存时间)之类的呢,这样那些不再需要的短生命周期对象就能被及时淘汰啦,就不会一直占用内存


也可以考虑使用弱引用来持有那些短生命周期对象的引用呀,这样当它们没有其他强引用时,垃圾回收器就能把它们回收掉啦,就不会造成内存泄漏。


要定期地对缓存进行清理呀,把那些不再需要的对象从缓存中移除掉呢,这样也能避免内存泄漏~


实例4:单例模式中的内存泄漏


【java】

public class Singleton {
    private static Singleton instance;
    private Context context; // Context 可能是 Activity 或者其他长生命周期的对象

    private Singleton(Context context) {
        this.context = context;
    }

    public static Singleton getInstance(Context context) {
        if (instance == null) {
            instance = new Singleton(context);
        }
        return instance;
    }

    // 其他方法...
}


说明:在单例模式中,如果单例对象持有一个长生命周期对象的引用(如 Activity),并且这个长生命周期对象在不需要时没有被正确清理,就可能导致内存泄漏。因为单例对象的生命周期和应用程序一样长,所以它会一直持有那个长生命周期对象的引用,不让它被垃圾回收。


如何避免:


1.不要直接持有长生命周期对象的强引用:

可以让单例对象不直接持有那个长生命周期对象的强引用,比如可以使用弱引用或者软引用之类。

2.在适当的时机清理引用:

如果单例对象确实需要持有那个长生命周期对象的引用,那也要在适当的时机,比如那个长生命周期对象不再需要时,及时清理掉这个引用。

3.使用生命周期管理工具:

比如Spring框架中,可以作为bean,利用容器来管理长生命周期对象的生命周期,这样就能更好地避免内存泄漏啦。


实例5:非静态内部类中的内存泄漏


【java】

public class OuterClass {
    private class InnerClass {
        // 内部类代码...
    }

    public void start() {
        InnerClass inner = new InnerClass();
        // 内部类对象被创建并持有外部类对象的隐式引用
    }
}


说明:非静态内部类会隐式持有外部类对象的引用。如果外部类对象不再需要,但由于内部类对象仍然存在(比如被其他对象持有),那么外部类对象也无法被垃圾回收,从而导致内存泄漏。


如何避免:


1.及时清理内部类对象的引用:

当确定内部类对象不再需要时,要及时清理掉它的引用,可以设置为null,这样垃圾回收器就能顺利回收外部类对象。

2.使用静态内部类:

可以考虑将非静态内部类改为静态内部类哦。静态内部类不会隐式持有外部类对象的引用,这样就能避免这个问题。

实例6:ThreadLocal 中的内存泄漏

【java】

public class ThreadLocalLeak {
    private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

    public static void set(byte[] data) {
        threadLocal.set(data);
    }

    // 通常情况下,应该有一个 clear 方法来清理 ThreadLocal 中的数据
    // 但如果忘记调用 clear 方法,就可能导致内存泄漏
    public static void clear() {
        threadLocal.remove();
    }
}


说明:ThreadLocal用于为每个线程提供变量的独立副本。但如果ThreadLocal被设置为一个长生命周期的对象(此处的字节数组),并且在线程结束后没有调用ThreadLocal的remove方法来清理这些对象,就可能导致内存泄漏。


因为ThreadLocalMap的entry的键是弱引用,但值是强引用,所以即使线程结束了,只要ThreadLocalMap中的值还被强引用着,就无法被垃圾回收。


如何避免:


1. 在使用完ThreadLocal后,一定要调用其remove()方法:

这样可以清除当前线程对该变量的引用,防止内存泄漏。你可以把remove()方法放在finally块中,确保即使在发生异常时也能正确清理。

2. 避免将ThreadLocal变量声明为static:

因为static变量的生命周期会与类加载器一样长,如果不及时清理,很容易导致内存泄漏。

3. 使用InheritableThreadLocal时要谨慎:

虽然InheritableThreadLocal可以在父子线程之间传递ThreadLocal变量,但也可能导致内存泄漏,所以在使用时需要特别小心。

4. 设置合适的初始值和清理逻辑:

为ThreadLocal变量设置合适的初始值和清理逻辑,确保在不需要时能够正确释放资源。


5. 避免过度使用ThreadLocal:

虽然ThreadLocal提供了线程隔离数据的便利,但过度使用也可能导致内存泄漏和其他问题。在设计系统时,要权衡使用ThreadLocal的利弊,并考虑其他可能的解决方案。


实例7:重写equals和hashCode方法时的内存泄漏

【java】

public class CustomObject {
    private String id;
    private byte[] data;

    public CustomObject(String id, byte[] data) {
        this.id = id;
        this.data = data;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CustomObject that = (CustomObject) o;
        return id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }

    // 其他方法...
}



说明:在使用HashSet或HashMap等集合时,如果自定义对象的equals和hashCode方法实现不当,就可能导致内存泄漏。因为当从集合中移除对象时,如果hashCode方法返回的值不正确,就可能导致对象无法被正确找到和移除,从而一直留在集合中占用内存。


如何避免:


1. 正确实现equals和hashCode方法:

equals方法要满足自反性、对称性、传递性、一致性和对于任何非null值的x,x.equals(null)必须返回false这些特性哦。hashCode方法呢,只要对象的equals方法所比较的信息没有被修改,那么对这同一个对象调用多次hashCode方法必须始终如一地返回同一个整数呢。如果两个对象是相等的,那么它们的hashCode值一定要相同。

2. 不要使用可变字段来计算hashCode:

如果用来计算hashCode的字段在对象生命周期中可能会被修改,那就不要用这些字段来计算,不然可能会导致hashCode值发生变化,从而使得对象在集合中的位置也发生变化,这样就很难正确找到和移除对象。

3. 避免使用复杂的对象作为键:

如果可能的话,尽量避免使用复杂的自定义对象作为HashMap或HashSet的键哦,可以使用一些简单的、不可变的类型,比如String、Integer这些。

4. 使用合适的集合类型:

选择合适的集合类型也能避免一些问题呢。比如,如果你只需要判断对象是否存在而不需要关心对象的顺序,那可以使用LinkedHashSet呀,它既能保证元素的唯一性,又能保持插入的顺序。

5. 定期清理集合:

虽然这不是直接解决equals和hashCode问题的方法,但定期清理集合中的无用元素也是一种避免内存泄漏的好办法哦。可以通过遍历集合,然后移除那些不再需要的元素。

实例8:动态代理导致的内存泄漏

【java】

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.ConcurrentHashMap;

public class DynamicProxyLeak {
    private static ConcurrentHashMap<String, Object> proxyMap = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        while (true) {
            createProxy();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Proxy count: " + proxyMap.size());
        }
    }

    private static void createProxy() {
        Object proxyInstance = Proxy.newProxyInstance(
            Thread.currentThread().getContextClassLoader(),
            new Class<?>[]{MyInterface.class},
            new MyInvocationHandler()
        );
        proxyMap.put(System.nanoTime() + "", proxyInstance);
    }

    interface MyInterface {
        void doSomething();
    }

    static class MyInvocationHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 处理方法调用
            return null;
        }
    }
}


说明:

在这个例子中,我们使用

Proxy.newProxyInstance创建了大量的动态代理对象,并将它们存储在一个ConcurrentHashMap中。由于动态代理对象会持有其InvocationHandler的引用,如果InvocationHandler持有外部对象的引用(比如在这个例子中,MyInvocationHandler可能持有其他长生命周期对象的引用),并且这些动态代理对象没有被及时清理,就可能导致内存泄漏。


如何避免:


1.及时清理持有的动态代理对象:

要定期检查和清理不再需要的动态代理对象。可以使用一些策略,比如基于时间的清理策略,或者当某个条件满足时就移除对应的动态代理对象。这样可以减少无用的动态代理对象对内存的占用。

2.避免InvocationHandler持有长生命周期对象的引用:

如果可能的话,尽量不要让InvocationHandler持有长生命周期对象的引用。如果确实需要持有,那要考虑在使用完后及时清理这些引用,比如将引用设置为null,或者采用其他方式来管理这些引用的生命周期。


实例9:ThreadLocal 与线程池结合使用时的内存泄漏

【java】

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;

public class ThreadLocalWithThreadPoolLeak {
    private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            executor.submit(() -> {
                byte[] data = new byte[ThreadLocalRandom.current().nextInt(1024, 1024 * 1024)]; // 分配大内存
                threadLocal.set(data);
                try {
                    // 模拟任务执行
                    Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // 忘记清理 ThreadLocal 中的数据
                    // threadLocal.remove();
                }
            });
        }
        // 关闭线程池(在实际应用中应该等待所有任务执行完毕再关闭)
        executor.shutdown();
    }
}


说明:在这个例子中,我们使用ThreadLocal来为每个线程存储数据,并且与线程池结合使用。由于线程池中的线程是复用的,如果ThreadLocal中的数据在任务执行完毕后没有被及时清理(即没有调用threadLocal.remove()方法),就可能导致内存泄漏。因为ThreadLocal中的数据会一直占用内存,直到线程池中的线程被销毁。


如何避免:

1.使用try-finally块确保清理:

在每个使用ThreadLocal的任务中,都可以用try-finally块来确保在任务执行完毕后调用threadLocal.remove()方法清理数据。

2.自定义线程池,覆盖线程的run方法:

可以自定义一个线程池,然后覆盖线程的run方法,在这个方法中添加对ThreadLocal数据的清理逻辑。不过这种方法需要对线程池的实现有一定了解,而且可能会影响到线程池的其他行为。3.使用线程池的钩子函数:

有些线程池实现提供了钩子函数,可以在线程执行前后执行一些自定义的逻辑。可以利用这些钩子函数来清理ThreadLocal数据。

4.使用弱引用的ThreadLocal:

虽然Java原生的ThreadLocal不支持弱引用,但可以通过一些第三方库或者自己实现一个弱引用的ThreadLocal。这样,即使数据没有被及时清理,也不会导致严重的内存泄漏问题,因为弱引用的对象在垃圾回收时更容易被回收。

5.定期清理ThreadLocal数据:

虽然这种方法不是很推荐,因为它可能会引入额外的复杂性和开销,但在某些情况下,可以通过定期遍历线程池中的所有线程,并清理它们持有的ThreadLocal数据来避免内存泄漏。


实例10:缓存中的内存泄漏

【java】

import java.util.concurrent.ConcurrentHashMap;

public class CacheLeak {
    private static ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        while (true) {
            String key = String.valueOf(System.nanoTime());
            Object value = new Object();
            cache.put(key, value);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Cache size: " + cache.size());
        }
    }
}

说明:在这个例子中,我们使用ConcurrentHashMap来实现一个简单的缓存。然而,由于我们没有实现任何缓存淘汰策略(比如 LRU、LFU 等),并且不断向缓存中添加新的键值对,就可能导致内存泄漏。因为缓存中的对象会一直占用内存,直到程序结束。


在实际应用中,我们应该根据业务需求实现合适的缓存淘汰策略来避免内存泄漏。



以上就是今天整理的各种泄露场景了,觉得有用的话,记得点赞关注哦!



相关推荐

在这款15年老端游的手机版中,正在上演着“萌新拯救计划”

以往我们判断一款刚公测的新手游到底火不火,不是瞅苹果的免费榜畅销榜,就是看各家数据网站的预估流水。不过如今这个法子放在《剑网3无界》身上似乎就不那么适用了。作为一款与原端游完全数据互通的手游,点卡制收...

708090后集体回忆!88款经典街机游戏,你通关过几部?

街机厅的霓虹灯在夜色中闪烁,投币口“叮当”的声响此起彼伏,摇杆与按键的碰撞声混合着玩家的欢呼与叹息,构成了那个年代独有的电子交响乐。对于70后、80后、90后来说,街机不仅是游戏,更是一段无法复制的...

爷青回!这10款童年小游戏,玩过5个以上的都当爸妈了吧?

当手机游戏被3A画质与开放世界统治的今天,那些藏在像素点阵里的童年记忆,才是真正刻进DNA的快乐密码!我们翻遍全网玩家回忆录,结合抖音、Steam等平台数据,为你揭开这代人的集体记忆封印一、经典益智三...

怀旧时刻:PS2十大经典动作游戏盘点,老玩家不可错过的青春回忆

说起PS2,那可是游戏史上最火的主机之一,上面好游戏多得数不清,给咱们带来过不少欢乐时光。今天,小核桃就带大家回忆一下PS2上那些超经典的动作游戏,一起重温那些热血沸腾的日子吧!当年在电玩店看到《战神...

又是一年仲夏,三十年前的暑假,你还记得在玩什么游戏吗?

今年山东的夏天似乎比往年都热,夜晚繁星点点,本该轻柔的晚风却没有丝毫凉意,伴随着远处草丛里此起彼伏的虫鸣声,听的让人心里愈加烦躁,翻来覆去睡不着的笔者,无聊且乏困地坐在院子里的老槐树下,思绪却不由自主...

十六年前的首款安卓1.0手机,内置物理全键盘,如今二手45元

周末聊点轻松的话题,说起智能手机系统之争,安卓和iOS绝对是两大“宿敌”。2007年苹果在乔布斯带领下发布了初代iPhone,也凭借iOS系统掀起了智能手机的新时代。短短一年后,谷歌联合HTC推出了...

HTC巅峰时期的安卓手机,自带全键盘,居然很多人用过

上次写三部最经典的侧滑盖全键盘手机,居然很多人报出了DesireZ的大名,这让我很吃惊。因为这部手机没有行货只有水货,你们咋都用过?那今天好好聊聊它。十多年前,HTC是安卓手机领域绝对的霸主,当年只...

十年前的 iPhone 6s 还在 “服役”:一部手机的 “超长待机” 启示录

当iPhone16系列已经开始预热,有人却还握着2015年发布的iPhone6s刷着微信、看视频——这部诞生已近十年的手机,至今仍在不少人的生活里扮演着重要角色。它的“超长寿命”...

SFC黄金时代10款动作RPG神作,每一款都是回忆满满的经典游戏!

任天堂的16位主机SFC可是游戏史上的一个高峰,它用像素画面打造出无数经典作品,其中结合动作和冒险的ARPG特别受欢迎。最近几年复古风又火起来,大厂们忙着移植或重制老游戏,模拟器也让玩家轻松重温旧梦。...

揭秘十年前真正的游戏手机:索尼爱立信R800魔改系统超乎想象!

对于游戏手机的起源,众说纷纭,有人认为是黑鲨问世,有人说是红魔领路,还有人坚称自从iPhone问世起,游戏手机就已然存在。然而,如果从更宏观的角度审视,这些所谓的游戏手机,其本质上仍旧是多功能的智能手...

专属于八零,九零后的插卡游戏,你还记得吗

1.魂斗罗这是我玩过的第一款插卡游戏,永远记得上上下下左左右右,BABA开始,这个可以有三十条命的“魔法。”也是第一次体验双打游戏的那行配合的责任感使命感。2.忍者神龟四只神龟的名字都是意大利著名的画...

Java编程的那些屎山代码分析之二(java编程神器)

以下是个人总结的一些代码习惯问题和优化,单独一个也许不起眼,但堆积起来,就让一个项目代码变成一座屎山。1.滥用`public`修饰符o重要性:滥用`public`修饰符可能导致类的成员变量或方法被不...

六种java的多线程设计模式详解和代码举例

java的多线程处理,有哪些模式可以使用呢,如何使用呢。本文列举了六种多线程设计模式供大家参考。1.生产者-消费者模式设计理念:生产者-消费者模式通过协调两个线程(生产者和消费者)来处理数据,生产者生...

java的四种引用(java 中都有哪些引用类型)

java中的引用分为4种1.强引用引用存在就不会被GC*2.软引用heapmemory(堆内存)满了就会被GC掉*3.弱引用每次GC就会回收掉(应用有:ThreadLocal)*4....

@程序员 2020了看不懂这些动图,你可能是个假的程序员

点击上方Java编程技术乐园,轻松关注!及时获取有趣有料的技术文章文章很有趣,开心一下,如果有收获,记得点赞和关注哦~「1」外包产品交付,给客户演示时「2」与领导斗智斗勇,躲猫猫「3」领导总是能识破程...