leak - java内存溢出




使用Java创建内存泄漏 (20)

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

一个例子是什么?


静态字段保持对象引用[esp final field]

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

在冗长的String上调用String.intern()

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();

(未封闭的)开放流(文件,网络等......)

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

未封闭的连接

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

JVM的垃圾收集器无法访问的区域 ,例如通过本机方法分配的内存

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

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

不正确或不适当的JVM选项 ,例如IBM JDK上的noclassgc选项,用于防止未使用的类垃圾回收

请参阅IBM jdk设置


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

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

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.

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

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

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


什么是内存泄漏:

  • 这是由错误糟糕的设计引起的
  • 这是浪费记忆。
  • 随着时间的推移会变得更糟
  • 垃圾收集器无法清理它。

典型例子:

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

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构造。
  • 添加更复杂的参考树
  • 第三方图书馆造成的泄密。

经常发生的事情:

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


如果您不了解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 ,也可能在最终确定期间抛出异常。 由此产生的行为是,与现在“休眠”对象关联的任何内存都不会被回收,因为finalize确保只调用一次。

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

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

  • 管理一个List实例,您只添加到列表中而不是从列表中删除(尽管您应该删除不再需要的元素),或者
  • 打开SocketFile ,但不再需要它们时关闭它们(类似于涉及Connection类的上述示例)。
  • 在关闭Java EE应用程序时不卸载单例。 显然,加载单例类的类加载器将保留对类的引用,因此永远不会收集单例实例。 当部署应用程序的新实例时,通常会创建一个新的类加载器,并且由于单例,前一个类加载器将继续存在。

您可以使用sun.misc.Unsafe类进行内存泄漏。 实际上,此服务类用于不同的标准类(例如,在java.nio类中)。 您无法直接创建此类的实例 ,但您可以使用反射来执行此操作

代码无法在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();
        }
    }

}

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

Log4j具有这种称为嵌套诊断上下文(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的引用,从而导致内存泄漏。


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

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

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


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

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

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

  • 理论上“完美”的Java实现是否容易受到泄漏?
  • 候选人是否理解理论与现实之间的区别?
  • 候选人是否了解垃圾收集的工作原理?
  • 或者垃圾收集如何在理想情况下起作用?
  • 他们知道他们可以通过本地接口调用其他语言吗?
  • 他们知道用其他语言泄漏记忆吗?
  • 候选人是否知道内存管理是什么,以及Java中幕后发生了什么?

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

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


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

  1. 应用程序创建一个长时间运行的线程(或使用线程池来更快地泄漏)。
  2. 该线程通过(可选的自定义)ClassLoader加载一个类。
  3. 该类分配一大块内存(例如new byte[1000000] ),在静态字段中存储对它的强引用,然后在ThreadLocal中存储对它自己的引用。 分配额外的内存是可选的(泄漏Class实例就足够了),但它会使泄漏工作更快。
  4. 该线程清除对自定义类或从中加载的ClassLoader的所有引用。
  5. 重复。

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

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

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

更新 :由于很多人不断要求它, 这里有一些示例代码显示了这种行为


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

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

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

-Wes Tarle


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


您可以通过在该类的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());
        }
    }
}

我曾经有过一次与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)需要大约两天才能到达......


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


最近我遇到了一种更为微妙的资源泄漏。我们通过类加载器的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,与迭代次数无关。

非常简单和令人惊讶。


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

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

过去工作的一种方式- 我不知道它是否仍然存在 - 是有一个三深的圆形链。如在对象A中引用了对象B,对象B引用了对象C,对象C引用了对象A.GC非常聪明地知道两条深链 - 如在A < - > B中 - 如果A和B无法通过其他任何方式访问,可以安全地收集,但无法处理三向链......


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

  1. 声明本机方法。
  2. 在本机方法中,请致电malloc。不要打电话free
  3. 调用本机方法。

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


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

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

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

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

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


这是一个简单/险恶的通过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实例之后,这样做也会将原始长字符串和派生子字符串保留在内存中。





memory-leaks