首页 文章

使用Java创建内存泄漏

提问于
浏览
2786

我刚接受采访,并被要求用Java创建内存泄漏 . 毋庸置疑,我觉得自己很傻,甚至不知道如何开始创建一个 .

一个例子是什么?

30 回答

  • 15

    答案完全取决于面试官的想法 .

    在实践中是否有可能使Java泄漏?当然是,并且在其他答案中有很多例子 .

    但是有多个元问题可能会被问到?

    • 理论上"perfect" Java实现是否易受泄漏?

    • 候选人是否了解理论与现实之间的区别?

    • 候选人是否了解垃圾收集的工作原理?

    • 或者垃圾收集在理想情况下应该如何工作?

    • 他们知道他们可以通过本地接口调用其他语言吗?

    • 他们知道用其他语言泄漏内存吗?

    • 候选人是否知道内存管理是什么,以及Java中幕后发生了什么?

    我正在读你的元问题:“在这次面试中我能用到什么答案” . 因此,我将专注于面试技巧而不是Java . 我相信你更有可能在面试中重复不知道问题答案的情况,而不是在需要知道如何使Java泄漏的地方 . 所以,希望这会有所帮助 .

    您可以为面试开发的最重要的技能之一是学习积极倾听问题并与面试官合作以提取他们的意图 . 这不仅可以让你以他们想要的方式回答他们的问题,而且还表明你有一些重要的沟通技巧 . 当谈到许多同样有才华的开发人员之间的选择时,我会聘请那些在每次回复之前倾听,思考和理解的人 .

  • 11

    面试官可能正在寻找一个循环引用,如下面的代码(顺便说一下,只有在使用引用计数的非常旧的JVM中泄漏内存,情况不再如此) . 但这是一个非常模糊的问题,因此这是展示您对JVM内存管理的理解的绝佳机会 .

    class A {
        B bRef;
    }
    
    class B {
        A aRef;
    }
    
    public class Main {
        public static void main(String args[]) {
            A myA = new A();
            B myB = new B();
            myA.bRef = myB;
            myB.aRef = myA;
            myA=null;
            myB=null;
            /* at this point, there is no access to the myA and myB objects, */
            /* even though both objects still have active references. */
        } /* main */
    }
    

    然后您可以通过引用计数来解释,上面的代码会泄漏内存 . 但是大多数现代JVM不再使用引用计数,大多数使用扫描垃圾收集器,实际上会收集这个内存 .

    接下来,您可以解释创建具有基础本机资源的Object,如下所示:

    public class Main {
        public static void main(String args[]) {
            Socket s = new Socket(InetAddress.getByName("google.com"),80);
            s=null;
            /* at this point, because you didn't close the socket properly, */
            /* you have a leak of a native descriptor, which uses memory. */
        }
    }
    

    然后你可以解释这在技术上是一个内存泄漏,但实际上泄漏是由JVM中的本机代码分配底层本机资源引起的,这些资源没有被Java代码释放 .

    在一天结束时,使用现代JVM,您需要编写一些Java代码,以便在JVM意识的正常范围之外分配本机资源 .

  • 21

    GUI代码中的一个常见示例是创建窗口小部件/组件并向某个静态/应用程序范围对象添加侦听器,然后在窗口小部件被销毁时不删除侦听器 . 您不仅会遇到内存泄漏,而且还会受到性能影响,因为无论您何时正在收听火灾事件,您的所有旧听众都会被调用 .

  • 16

    获取在任何servlet容器中运行的任何Web应用程序(Tomcat,Jetty,Glassfish,等等......) . 连续重新部署应用程序10次或者20次(可能只需触摸服务器的autodeploy目录中的WAR即可 .

    除非有人实际测试过这个,否则很有可能在经过几次重新部署之后你会得到一个OutOfMemoryError,因为应用程序没有注意自己清理 . 您甚至可以通过此测试在服务器中找到错误 .

    问题是,容器的生命周期比应用程序的生命周期长 . 您必须确保容器可能对应用程序的对象或类的所有引用都可以进行垃圾回收 .

    如果只有一个引用在您的Web应用程序取消部署后仍然存在,则相应的类加载器将导致您的Web应用程序的所有类都无法进行垃圾回收 .

    您的应用程序启动的线程,ThreadLocal变量,日志记录附加程序是导致类加载器泄漏的一些常见嫌疑 .

  • 30

    创建一个静态Map并继续添加对它的硬引用 . 那些永远不会是GC .

    public class Leaker {
        private static final Map<String, Object> CACHE = new HashMap<String, Object>();
    
        // Keep adding until failure.
        public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
    }
    
  • 412

    您可以使用 sun.misc.Unsafe 类进行内存泄漏 . 实际上,此服务类用于不同的标准类(例如,在 java.nio 类中) . You can't create instance of this class directly ,但你可以 use reflection to do that .

    代码无法在Eclipse IDE中编译 - 使用命令 javac 编译它(在编译期间,您将收到警告)

    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import sun.misc.Unsafe;
    
    
    public class TestUnsafe {
    
        public static void main(String[] args) throws Exception{
            Class unsafeClass = Class.forName("sun.misc.Unsafe");
            Field f = unsafeClass.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            System.out.print("4..3..2..1...");
            try
            {
                for(;;)
                    unsafe.allocateMemory(1024*1024);
            } catch(Error e) {
                System.out.println("Boom :)");
                e.printStackTrace();
            }
        }
    
    }
    
  • 17

    我曾经有过一次与PermGen和XML解析有关的“内存泄漏” . 我们使用的XML解析器(我不记得它是哪一个)在标记名称上做了一个String.intern(),以便更快地进行比较 . 我们的一位客户拥有最好不要将数据值存储在XML属性或文本中,而是存储为标记名,因此我们有一个文档,如:

    <data>
       <1>bla</1>
       <2>foo</>
       ...
    </data>
    

    实际上,他们并没有使用数字,而是使用更长的文本ID(大约20个字符),这些ID是独一无二的,每天的速度为10-15百万 . 这每天产生200 MB的垃圾,这是永远不需要的,而且从来没有GCed(因为它是在PermGen) . 我们将permgen设置为512 MB,因此内存异常(OOME)需要大约两天才能到达......

  • 64

    Static field holding object reference [esp final field]

    class MemorableClass {
        static final ArrayList list = new ArrayList(100);
    }
    

    Calling String.intern() on lengthy String

    String str=readString(); // read lengthy string any source db,textbox/jsp etc..
    // This will place the string in memory pool from which you can't remove
    str.intern();
    

    (Unclosed) open streams ( file , network etc... )

    try {
        BufferedReader br = new BufferedReader(new FileReader(inputFile));
        ...
        ...
    } catch (Exception e) {
        e.printStacktrace();
    }
    

    Unclosed connections

    try {
        Connection conn = ConnectionFactory.getConnection();
        ...
        ...
    } catch (Exception e) {
        e.printStacktrace();
    }
    

    Areas that are unreachable from JVM's garbage collector ,例如通过本机方法分配的内存

    在Web应用程序中,某些对象存储在应用程序范围中,直到明确停止或删除应用程序 .

    getServletContext().setAttribute("SOME_MAP", map);
    

    Incorrect or inappropriate JVM options ,例如IBM JDK上的 noclassgc 选项,用于防止未使用的类垃圾回收

    IBM jdk settings .

  • 119

    每个人总是忘记本机代码路由 . 这是泄漏的简单公式:

    • 声明本机方法 .

    • 在本机方法中,调用 malloc . 不要拨打 free .

    • 调用本机方法 .

    请记住,本机代码中的内存分配来自JVM堆 .

  • 2040

    您可以通过在该类的finalize方法中创建类的新实例来创建移动内存泄漏 . 如果终结器创建多个实例,则奖励积分 . 这是一个简单的程序,它会在几秒到几分钟之间的某个时间内泄漏整个堆,具体取决于您的堆大小:

    class Leakee {
        public void check() {
            if (depth > 2) {
                Leaker.done();
            }
        }
        private int depth;
        public Leakee(int d) {
            depth = d;
        }
        protected void finalize() {
            new Leakee(depth + 1).check();
            new Leakee(depth + 1).check();
        }
    }
    
    public class Leaker {
        private static boolean makeMore = true;
        public static void done() {
            makeMore = false;
        }
        public static void main(String[] args) throws InterruptedException {
            // make a bunch of them until the garbage collector gets active
            while (makeMore) {
                new Leakee(0).check();
            }
            // sit back and watch the finalizers chew through memory
            while (true) {
                Thread.sleep(1000);
                System.out.println("memory=" +
                        Runtime.getRuntime().freeMemory() + " / " +
                        Runtime.getRuntime().totalMemory());
            }
        }
    }
    
  • 107

    也许通过JNI使用外部本机代码?

    使用纯Java,几乎是不可能的 .

    但这是关于“标准”类型的内存泄漏,当您无法再访问内存时,它仍然由应用程序拥有 . 您可以保留对未使用对象的引用,或者在不关闭它们的情况下打开流 .

  • 33

    我觉得有趣的是没有人使用内部类的例子 . 如果你有内部课程;它本身就维护了对包含类的引用 . 当然,从技术上讲,它不是内存泄漏,因为Java最终会将其清理干净;但这可能会导致课程比预期更长时间 .

    public class Example1 {
      public Example2 getNewExample2() {
        return this.new Example2();
      }
      public class Example2 {
        public Example2() {}
      }
    }
    

    现在,如果您调用Example1并获取Example2丢弃Example1,您将固有地仍然拥有指向Example1对象的链接 .

    public class Referencer {
      public static Example2 GetAnExample2() {
        Example1 ex = new Example1();
        return ex.getNewExample2();
      }
    
      public static void main(String[] args) {
        Example2 ex = Referencer.GetAnExample2();
        // As long as ex is reachable; Example1 will always remain in memory.
      }
    }
    

    我也听说过一个谣言,如果你的变量存在的时间超过特定的时间; Java假设它将永远存在,并且如果再也无法在代码中访问它,它实际上永远不会尝试清理它 . 但这完全未经证实 .

  • 28

    有许多不同的情况,内存会泄漏 . 我遇到的一个,它暴露了一个不应该在其他地方暴露和使用的 Map .

    public class ServiceFactory {
    
    private Map<String, Service> services;
    
    private static ServiceFactory singleton;
    
    private ServiceFactory() {
        services = new HashMap<String, Service>();
    }
    
    public static synchronized ServiceFactory getDefault() {
    
        if (singleton == null) {
            singleton = new ServiceFactory();
        }
        return singleton;
    }
    
    public void addService(String name, Service serv) {
        services.put(name, serv);
    }
    
    public void removeService(String name) {
        services.remove(name);
    }
    
    public Service getService(String name, Service serv) {
        return services.get(name);
    }
    
    // the problematic api, which expose the map.
    //and user can do quite a lot of thing from this api.
    //for example, create service reference and forget to dispose or set it null
    //in all this is a dangerous api, and should not expose 
    public Map<String, Service> getAllServices() {
        return services;
    }
    
    }
    
    // resource class is a heavy class
    class Service {
    
    }
    
  • 245

    下面将有一个非显而易见的案例,其中Java泄漏,除了被遗忘的侦听器的标准情况,静态引用,哈希映射中的虚假/可修改键,或者只是没有任何机会结束其生命周期的线程 .

    • File.deleteOnExit() - 总是泄漏字符串,如果字符串是子字符串,则泄漏更严重(底层char []也泄露) - 在Java 7子字符串中也复制 char[] ,所以后者不适用; @Daniel,不需要投票 .

    我将专注于线程,以显示大多数非托管线程的危险,不希望甚至触摸挥杆 .

    • Runtime.addShutdownHook 并且不删除...然后甚至使用removeShutdownHook由于ThreadGroup类中有关未启动线程的错误,它可能无法收集,有效地泄漏了ThreadGroup . JGroup在GossipRouter中有漏洞 .

    • 创建但未启动 Thread 与上面的类别相同 .

    • 创建一个线程继承 ContextClassLoaderAccessControlContext ,加上 ThreadGroup 和任何 InheritedThreadLocal ,所有这些引用都是潜在的泄漏,以及类加载器和所有静态引用加载的整个类,以及ja-ja . 整个j.u.c.Executor框架具有超级简单的 ThreadFactory 界面,但效果特别明显,但大多数开发人员都不知道潜伏的危险 . 此外,许多库都会根据请求启动线程(太多行业流行的库) .

    • ThreadLocal 缓存;那些在许多情况下是邪恶的 . 我确信每个人都已经看到了很多基于ThreadLocal的简单缓存,好消息是坏消息:如果线程持续超过预期生命的上下文ClassLoader,它是一个纯粹的小泄漏 . 除非确实需要,否则不要使用ThreadLocal缓存 .

    • 当ThreadGroup本身没有线程时调用 ThreadGroup.destroy() ,但它仍保留子ThreadGroups . 一个错误的泄漏将阻止ThreadGroup从其父级中删除,但所有子级都变为不可枚举 .

    • 使用WeakHashMap并且值(in)直接引用该键 . 没有堆转储,这是一个很难找到的 . 这适用于所有扩展的 Weak/SoftReference ,它可能会将硬引用保留回受保护对象 .

    • 使用 java.net.URL 与HTTP(S)协议并从(!)加载资源 . 这个是特殊的, KeepAliveCache 在系统ThreadGroup中创建一个新线程,它泄漏当前线程的上下文类加载器 . 当没有活动线程存在时,线程是在第一个请求时创建的,所以你可能会幸运或只是泄漏 . 泄漏已在Java 7中修复,并且创建线程的代码正确删除了上下文类加载器 . 创建类似线程的情况很少(如ImageFetcher,也已修复) .

    • 在构造函数中使用 InflaterInputStream 传递 new java.util.zip.Inflater() (例如 PNGImageDecoder )而不调用inflater的 end() . 好吧,如果你只使用 new 传递构造函数,那就没有机会......是的,如果终结器发布了's manually passed as constructor parameter. This is not a true leak since it',那么在流上调用 close() 并不会关闭它...当它认为有必要时 . 直到那一刻它吃掉本机内存如此糟糕,它可能导致Linux oom_killer肆无忌惮地杀死进程 . 主要问题是Java的最终确定非常不可靠,G1在7.0.2之前变得更糟 . 故事的道德:尽快释放本地资源;终结者太穷了 .

    • java.util.zip.Deflater 相同的情况 . 这个更糟糕,因为Deflater在Java中需要内存,即总是使用15位(最大值)和8个内存级别(最多9个)来分配几百KB的本机内存 . 幸运的是, Deflater 并没有被广泛使用,据我所知,JDK没有滥用 . 如果手动创建 DeflaterInflater ,请始终调用 end() . 最后两个中最好的部分:您无法通过常规的分析工具找到它们 .

    (我可以根据要求添加更多时间浪费 . )

    祝你好运,保持安全;泄漏是邪恶的!

  • 172

    这是在纯Java中创建真正的内存泄漏(通过运行代码但仍然存储在内存中无法访问的对象)的好方法:

    • 应用程序创建一个长时间运行的线程(或使用线程池来更快地泄漏) .

    • 该线程通过(可选的自定义)ClassLoader加载一个类 .

    • 该类分配一大块内存(例如 new byte[1000000] ),在静态字段中存储对它的强引用,然后在ThreadLocal中存储对自身的引用 . 分配额外的内存是可选的(泄漏Class实例就足够了),但它会使泄漏工作更快 .

    • 该线程清除对自定义类或从中加载的ClassLoader的所有引用 .

    • 重复 .

    这是有效的,因为ThreadLocal保留对该对象的引用,该对象保持对其Class的引用,而Class又保持对其ClassLoader的引用 . 反过来,ClassLoader保持对它已加载的所有类的引用 .

    (在许多JVM实现中,尤其是在Java 7之前,情况更糟,因为Classes和ClassLoader直接分配到permgen并且根本就没有GC . 但是,无论JVM如何处理类卸载,ThreadLocal仍然会阻止被回收的类对象 . )

    此模式的一个变体是,如果您经常重新部署碰巧以任何方式使用ThreadLocals的应用程序,那么应用程序容器(如Tomcat)可能会像筛子那样泄漏内存 . (由于应用程序容器使用了所描述的线程,并且每次重新部署应用程序时都会使用新的ClassLoader . )

    Update :由于很多人不断要求它,here's some example code that shows this behavior in action .

  • 1105

    我认为一个有效的例子可能是在线程被池化的环境中使用ThreadLocal变量 .

    例如,使用Servlet中的ThreadLocal变量与其他Web组件进行通信,使容器创建线程并在池中维护空闲的线程 . ThreadLocal变量如果没有被正确清理,将会存在,直到可能相同的Web组件覆盖它们的值 .

    当然,一旦确定,问题就可以轻松解决 .

  • 141

    线程在终止之前不会被收集 . 它们用作roots垃圾收集 . 它们是为数不多的仅通过忘记它们或清除对它们的引用而无法回收的对象之一 .

    考虑:终止工作线程的基本模式是设置线程看到的一些条件变量 . 线程可以定期检查变量并将其用作终止信号 . 如果变量未声明为 volatile ,则线程可能看不到对变量的更改,因此它不会知道终止 . 或者想象一下,如果某些线程想要更新共享对象,但在尝试锁定它时会出现死锁 .

    如果您只有少数线程,这些错误可能很明显,因为您的程序将无法正常工作 . 如果您有一个根据需要创建更多线程的线程池,那么可能不会注意到过时/卡住的线程,并且将无限累积,从而导致内存泄漏 . 线程可能会在您的应用程序中使用其他数据,因此也会阻止他们直接引用的任何数据被收集 .

    作为玩具例:

    static void leakMe(final Object object) {
        new Thread() {
            public void run() {
                Object o = object;
                for (;;) {
                    try {
                        sleep(Long.MAX_VALUE);
                    } catch (InterruptedException e) {}
                }
            }
        }.start();
    }
    

    调用 System.gc() 你喜欢什么,但传递给 leakMe 的对象永远不会死 .

    编辑

  • 41

    每当你保持对不再需要的对象的引用时,就会发生内存泄漏 . 有关内存泄漏如何在Java中表现出来以及您可以采取哪些措施的示例,请参阅Handling memory leaks in Java programs .

  • 9

    我最近遇到了log4j造成的内存泄漏情况 .

    Log4j具有这种称为Nested Diagnostic Context(NDC)的机制,它是一种区分不同来源的交错日志输出的工具 . NDC工作的粒度是线程,因此它分别区分不同线程的日志输出 .

    为了存储特定于线程的标记,log4j的NDC类使用一个由Thread对象本身键入的Hashtable(而不是线程id),因此直到NDC标记在内存中保留所有挂起线程的对象对象也留在记忆中 . 在我们的Web应用程序中,我们使用NDC标记带有请求ID的logoutput,以区分日志和单个请求 . 将NDC标记与线程相关联的容器也会在从请求返回响应时将其删除 . 在处理请求的过程中,生成了一个子线程,类似于以下代码:

    pubclic class RequestProcessor {
        private static final Logger logger = Logger.getLogger(RequestProcessor.class);
        public void doSomething()  {
            ....
            final List<String> hugeList = new ArrayList<String>(10000);
            new Thread() {
               public void run() {
                   logger.info("Child thread spawned")
                   for(String s:hugeList) {
                       ....
                   }
               }
            }.start();
        }
    }
    

    因此,NDC上下文与生成的内联线程相关联 . 作为此NDC上下文的键的线程对象是内联线程,其中有hugeList对象 . 因此,即使在线程完成其正在执行的操作之后,对NDC上下文Hastable仍然保持对hugeList的引用,从而导致内存泄漏 .

  • 14

    我认为还没有人说过这个:你可以通过覆盖finalize()方法来复活一个对象,以便finalize()在某处存储一个引用 . 垃圾收集器只会在对象上调用一次,因此对象永远不会被销毁 .

  • 33

    这里的大多数例子都“过于复杂” . 他们是边缘案件 . 使用这些示例,程序员犯了一个错误(比如不重新定义equals / hashcode),或者被JVM / JAVA的一个角落案例(带有静态的类加载......)所困扰 . 我认为这不是面试官想要的例子,甚至是最常见的案例 .

    但是内存泄漏的情况确实比较简单 . 垃圾收集器只释放不再引用的内容 . 我们作为Java开发人员并不关心内存 . 我们在需要时分配它并让它自动释放 . 精细 .

    但任何长期存在的应用程序都倾向于共享状态 . 它可以是任何东西,静力学,单身......通常非平凡的应用程序往往会制作复杂的对象图 . 只是忘记设置null或更多的引用经常忘记从集合中删除一个对象足以导致内存泄漏 .

    当然,如果处理不当,所有类型的侦听器(如UI侦听器),缓存或任何长期共享状态都会产生内存泄漏 . 应该理解的是,这不是Java角落案例,也不是垃圾收集器的问题 . 这是一个设计问题 . 我们设计我们为一个长期存在的对象添加一个监听器,但是当不再需要时我们不会删除监听器 . 我们缓存对象,但我们没有策略将它们从缓存中删除 .

    我们可能有一个复杂的图形来存储计算所需的先前状态 . 但是之前的状态本身与之前的状态有关,依此类推 .

    就像我们必须关闭SQL连接或文件一样 . 我们需要设置对null的正确引用并从集合中删除元素 . 我们将有适当的缓存策略(最大内存大小,元素数量或计时器) . 允许侦听器得到通知的所有对象都必须同时提供addListener和removeListener方法 . 当这些通知符不再使用时,他们必须清除他们的听众列表 .

    内存泄漏确实是可能的,并且是完全可预测的 . 无需特殊语言功能或角落案例 . 内存泄漏可能表明某些内容可能缺失甚至是设计问题 .

  • 9

    如果你不理解JDBC,以下是一个非常毫无意义的例子 . 或者至少JDBC希望开发人员在丢弃它们或丢失对它们的引用之前关闭 ConnectionStatementResultSet 实例,而不是依赖于 finalize 的实现 .

    void doWork()
    {
       try
       {
           Connection conn = ConnectionFactory.getConnection();
           PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
           ResultSet rs = stmt.executeQuery();
           while(rs.hasNext())
           {
              ... process the result set
           }
       }
       catch(SQLException sqlEx)
       {
           log(sqlEx);
       }
    }
    

    上面的问题是 Connection 对象没有关闭,因此物理连接将保持打开状态,直到垃圾收集器出现并看到它无法访问 . GC将调用 finalize 方法,但是有些JDBC驱动程序没有实现 finalize ,至少与实现 Connection.close 的方式不同 . 由此产生的行为是,由于收集了无法访问的对象而将回收内存,因此可能无法回收与 Connection 对象关联的资源(包括内存) .

    在这样的情况下 Connectionfinalize 方法没有清理所有内容,实际上可能会发现与数据库服务器的物理连接将持续几个垃圾收集周期,直到数据库服务器最终发现连接不存在(如果存在),并且应该关闭 .

    即使JDBC驱动程序要实现 finalize ,也有可能在最终确定期间抛出异常 . 由此产生的行为是,不会回收与now "dormant"对象关联的任何内存,因为保证只能调用 finalize 一次 .

    在对象最终化期间遇到异常的上述情况与另一个可能导致内存泄漏的情况有关 - 对象复活 . 对象复活通常是通过从另一个对象创建对象的强引用来有意识地完成的 . 当对象复活被滥用时,它将导致内存泄漏以及其他内存泄漏源 .

    你可以想出更多的例子 - 比如

    • 管理 List 实例,其中只添加到列表而不是从中删除(尽管您应该删除不再需要的元素),或者

    • 打开 SocketFile ,但不再需要它们时关闭它们(类似于上面涉及 Connection 类的示例) .

    • 在关闭Java EE应用程序时不卸载单例 . 显然,加载单例类的类加载器将保留对类的引用,因此永远不会收集单例实例 . 当部署应用程序的新实例时,通常会创建一个新的类加载器,并且由于单例,前一个类加载器将继续存在 .

  • 21

    正如许多人所建议的那样,资源泄漏很容易引起 - 就像JDBC示例一样 . 实际内存泄漏有点困难 - 特别是如果你不依赖于JVM的破碎位来为你做这件事......

    创建具有非常大的占用空间然后无法访问它们的对象的想法也不是真正的内存泄漏 . 如果没有什么可以访问它那么它将被垃圾收集,如果有东西可以访问它那么它不是泄漏...

    以前工作的一种方式 - 我不能通过任何其他方式访问,但无法处理三向链......

  • 11

    一个简单的事情是使用不正确(或不存在) hashCode()equals() 的HashSet,然后继续添加"duplicates" . 而不是忽略重复,它只会增长,你将无法删除它们 .

    如果你想让这些坏键/元素闲逛,你可以使用静态字段

    class BadKey {
       // no hashCode or equals();
       public final String key;
       public BadKey(String key) { this.key = key; }
    }
    
    Map map = System.getProperties();
    map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.
    
  • 46

    面试官可能正在寻找循环参考解决方案:

    public static void main(String[] args) {
            while (true) {
                Element first = new Element();
                first.next = new Element();
                first.next.next = first;
            }
        }
    

    这是引用计数垃圾收集器的典型问题 . 然后,您可以礼貌地解释JVM使用更复杂的算法,但没有此限制 .

    -Wes Tarle

  • 34

    什么是内存泄漏:

    • 这是由 bugbad design. 引起的

    • 浪费记忆力 .

    • It gets worse over time.

    • The garbage collector cannot clean it.

    典型例子:

    对象缓存是弄乱事物的好起点 .

    private static final Map<String, Info> myCache = new HashMap<>();
    
    public void getInfo(String key)
    {
        // uses cache
        Info info = myCache.get(key);
        if (info != null) return info;
    
        // if it's not in cache, then fetch it from the database
        info = Database.fetch(key);
        if (info == null) return null;
    
        // and store it in the cache
        myCache.put(key, info);
        return info;
    }
    

    您的缓存增长和增长 . 很快整个数据库就被吸进了内存 . 更好的设计使用LRUMap(仅在缓存中保留最近使用的对象) .

    当然,你可以让事情变得更复杂:

    • 使用 ThreadLocal 构造 .

    • 添加更多 complex reference trees .

    • 或由 3rd party libraries 引起的泄漏 .

    经常发生的事情:

    如果此Info对象具有对其他对象的引用,则其他对象也会引用其他对象 . 在某种程度上,你也可以认为这是某种内存泄漏(由不良设计引起) .

  • 14

    可能是潜在内存泄漏的最简单示例之一,以及如何避免它,是ArrayList.remove(int)的实现:

    public E remove(int index) {
        RangeCheck(index);
    
        modCount++;
        E oldValue = (E) elementData[index];
    
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        elementData[--size] = null; // (!) Let gc do its work
    
        return oldValue;
    }
    

    如果你自己实现它,你会想到清除不再使用的数组元素( elementData[--size] = null )吗?这个参考可能会让一个巨大的物体活着......

  • 22

    我可以从这里复制我的答案:Easiest way to cause memory leak in Java?

    “当计算机程序消耗内存但无法将其释放回操作系统时,会发生计算机科学(或泄漏,在此上下文中)的内存泄漏 . ” (维基百科)

    简单的答案是:你不能 . Java执行自动内存管理,并将释放您不需要的资源 . 你不能阻止这种情况发生 . 它总是能够释放资源 . 在具有手动内存管理的程序中,这是不同的 . 你可以使用malloc()在C中获得一些内存 . 要释放内存,需要malloc返回的指针并在其上调用free() . 但是如果你不再使用指针(覆盖或超过生命周期),那么你很遗憾无法释放这个内存,因此你有内存泄漏 .

    一切到目前为止,其他答案在我的定义中并不是真正的内存泄漏 . 它们都旨在快速地用无意义的东西填充内存 . 但是在任何时候你仍然可以取消引用你创建的对象,从而释放内存 - >没有泄漏 . acconrad's answer非常接近,但我不得不承认,因为他的解决方案实际上只是通过强制它在无限循环中垃圾收集器"crash" .

    长的答案是:您可以通过使用JNI编写Java库来获取内存泄漏,JNI可能具有手动内存管理,因此存在内存泄漏 . 如果你调用这个库,你的java进程将泄漏内存 . 或者,您可以在JVM中出现错误,以便JVM丢失内存 . JVM中可能存在错误,甚至可能存在一些已知错误,因为垃圾收集不是那么简单,但它仍然是一个错误 . 按设计这是不可能的 . 你可能会要求一些受这种bug影响的java代码 . 对不起我不知道一个,无论如何它在下一个Java版本中可能不再是一个bug .

  • 16

    最近我遇到了一种更为微妙的资源泄漏 . 我们通过类加载器的getResourceAsStream打开资源,并且输入流句柄没有关闭 .

    嗯,你可能会说,多么愚蠢 .

    那么,有趣的是:这样,你可以泄漏底层进程的堆内存,而不是来自JVM的堆 .

    您只需要一个jar文件,其中包含一个文件,该文件将从Java代码中引用 . jar文件越大,分配的内存越快 .

    您可以使用以下类轻松创建此类jar:

    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipOutputStream;
    
    public class BigJarCreator {
        public static void main(String[] args) throws IOException {
            ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
            zos.putNextEntry(new ZipEntry("resource.txt"));
            zos.write("not too much in here".getBytes());
            zos.closeEntry();
            zos.putNextEntry(new ZipEntry("largeFile.out"));
            for (int i=0 ; i<10000000 ; i++) {
                zos.write((int) (Math.round(Math.random()*100)+20));
            }
            zos.closeEntry();
            zos.close();
        }
    }
    

    只需粘贴到名为BigJarCreator.java的文件中,从命令行编译并运行它:

    javac BigJarCreator.java
    java -cp . BigJarCreator
    

    Etvoilà:你在当前的工作目录中找到一个jar存档,里面有两个文件 .

    让我们创建第二个类:

    public class MemLeak {
        public static void main(String[] args) throws InterruptedException {
            int ITERATIONS=100000;
            for (int i=0 ; i<ITERATIONS ; i++) {
                MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
            }
            System.out.println("finished creation of streams, now waiting to be killed");
    
            Thread.sleep(Long.MAX_VALUE);
        }
    
    }
    

    这个类基本上什么都不做,但创建了未引用的InputStream对象 . 这些对象将立即被垃圾收集,因此不会对堆大小产生影响 . 对于我们的示例来说,从jar文件加载现有资源非常重要,大小在这里很重要!

    如果您有疑问,请尝试编译并启动上面的类,但请确保选择合适的堆大小(2 MB):

    javac MemLeak.java
    java -Xmx2m -classpath .:big.jar MemLeak
    

    这里不会遇到OOM错误,因为没有保留引用,无论您在上面的示例中选择了多大的ITERATIONS,应用程序都将继续运行 . 除非应用程序进入wait命令,否则进程的内存消耗(在顶层(RES / RSS)或进程资源管理器中可见)会增长 . 在上面的设置中,它将在内存中分配大约150 MB .

    如果您希望应用程序安全播放,请在创建它的位置关闭输入流:

    MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
    

    并且您的过程不会超过35 MB,与迭代次数无关 .

    非常简单和令人惊讶 .

  • 16

    这是一个简单/险恶的通过http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

    public class StringLeaker
    {
        private final String muchSmallerString;
    
        public StringLeaker()
        {
            // Imagine the whole Declaration of Independence here
            String veryLongString = "We hold these truths to be self-evident...";
    
            // The substring here maintains a reference to the internal char[]
            // representation of the original string.
            this.muchSmallerString = veryLongString.substring(0, 1);
        }
    }
    

    因为子串指的是原始字符串的内部表示,所以原始字符串保留在内存中 . 因此,只要你有一个StringLeaker,你就可以在内存中拥有整个原始字符串,即使你可能认为你只是坚持使用单字符字符串 .

    避免将不需要的引用存储到原始字符串的方法是执行以下操作:

    ...
    this.muchSmallerString = new String(veryLongString.substring(0, 1));
    ...
    

    对于增加的不良,您可能还会 .intern() 子字符串:

    ...
    this.muchSmallerString = veryLongString.substring(0, 1).intern();
    ...
    

    即使在丢弃StringLeaker实例之后,这样做也会将原始长字符串和派生子字符串保留在内存中 .

相关问题