如何在 Java 中创建内存泄漏?

我刚刚接受了一次采访,要求我使用 Java 创建内存泄漏.

不用说,我觉得很愚蠢,甚至不知道如何开始创建一个.

一个例子是什么?

A simple thing to do is to use a HashSet with an incorrect (or non-existent) hashCode() or equals(), and then keep adding "duplicates". Instead of ignoring duplicates as it should, the set will only ever grow and you won't be able to remove them.

If you want these bad keys/elements to hang around you can use a static field like

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.

</div>

Below there will be a non-obvious case where Java leaks, besides the standard case of forgotten listeners, static references, bogus/modifiable keys in hashmaps, or just threads stuck without any chance to end their life-cycle.

  • File.deleteOnExit() - always leaks the string, if the string is a substring, the leak is even worse (the underlying char[] is also leaked) - in Java 7 substring also copies the char[], so the later doesn't apply; @Daniel, no needs for votes, though.

I'll concentrate on threads to show the danger of unmanaged threads mostly, don't wish to even touch swing.

  • Runtime.addShutdownHook and not remove... and then even with removeShutdownHook due to a bug in ThreadGroup class regarding unstarted threads it may not get collected, effectively leak the ThreadGroup. JGroup has the leak in GossipRouter.

  • Creating, but not starting, a Thread goes into the same category as above.

  • Creating a thread inherits the ContextClassLoader and AccessControlContext, plus the ThreadGroup and any InheritedThreadLocal, all those references are potential leaks, along with the entire classes loaded by the classloader and all static references, and ja-ja. The effect is especially visible with the entire j.u.c.Executor framework that features a super simple ThreadFactory interface, yet most developers have no clue of the lurking danger. Also a lot of libraries do start threads upon request (way too many industry popular libraries).

  • ThreadLocal caches; those are evil in many cases. I am sure everyone has seen quite a bit of simple caches based on ThreadLocal, well the bad news: if the thread keeps going more than expected the life the context ClassLoader, it is a pure nice little leak. Do not use ThreadLocal caches unless really needed.

  • Calling ThreadGroup.destroy() when the ThreadGroup has no threads itself, but it still keeps child ThreadGroups. A bad leak that will prevent the ThreadGroup to remove from its parent, but all the children become un-enumerateable.

  • Using WeakHashMap and the value (in)directly references the key. This is a hard one to find without a heap dump. That applies to all extended Weak/SoftReference that might keep a hard reference back to the guarded object.

  • Using java.net.URL with the HTTP(S) protocol and loading the resource from(!). This one is special, the KeepAliveCache creates a new thread in the system ThreadGroup which leaks the current thread's context classloader. The thread is created upon the first request when no alive thread exists, so either you may get lucky or just leak. The leak is already fixed in Java 7 and the code that creates thread properly removes the context classloader. There are few more cases (like ImageFetcher, also fixed) of creating similar threads.

  • Using InflaterInputStream passing new java.util.zip.Inflater() in the constructor (PNGImageDecoder for instance) and not calling end() of the inflater. Well, if you pass in the constructor with just new, no chance... And yes, calling close() on the stream does not close the inflater if it's manually passed as constructor parameter. This is not a true leak since it'd be released by the finalizer... when it deems it necessary. Till that moment it eats native memory so badly it can cause Linux oom_killer to kill the process with impunity. The main issue is that finalization in Java is very unreliable and G1 made it worse till 7.0.2. Moral of the story: release native resources as soon as you can; the finalizer is just too poor.

  • The same case with java.util.zip.Deflater. This one is far worse since Deflater is memory hungry in Java, i.e. always uses 15 bits (max) and 8 memory levels (9 is max) allocating several hundreds KB of native memory. Fortunately, Deflater is not widely used and to my knowledge JDK contains no misuses. Always call end() if you manually create a Deflater or Inflater. The best part of the last two: you can't find them via normal profiling tools available.

(I can add some more time wasters I have encountered upon request.)

Good luck and stay safe; leaks are evil!

Any time you keep references around to objects that you no longer need you have a memory leak. See Handling memory leaks in Java programs for examples of how memory leaks manifest themselves in Java and what you can do about it.

Here's a simple/sinister one via 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);
}

}

Because the substring refers to the internal representation of the original, much longer string, the original stays in memory. Thus, as long as you have a StringLeaker in play, you have the whole original string in memory, too, even though you might think you're just holding on to a single-character string.

The way to avoid storing an unwanted reference to the original string is to do something like this:

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

For added badness, you might also .intern() the substring:

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

Doing so will keep both the original long string and the derived substring in memory even after the StringLeaker instance has been discarded.

A common example of this in GUI code is when creating a widget/component and adding a listener to some static/application scoped object and then not removing the listener when the widget is destroyed. Not only do you get a memory leak, but also a performance hit as when whatever you are listening to fires events, all your old listeners are called too.

The interviewer was probably looking for a circular reference like the code below (which incidentally only leak memory in very old JVMs that used reference counting, which isn't the case any more). But it's a pretty vague question, so it's a prime opportunity to show off your understanding of JVM memory management.

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 */
}

Then you can explain that with reference counting, the above code would leak memory. But most modern JVMs don't use reference counting any longer. Most use a sweep garbage collector, which will in fact collect this memory.

Next you might explain creating an Object that has an underlying native resource, like this:

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. */
    }
}

Then you can explain this is technically a memory leak, but really the leak is caused by native code in the JVM allocating underlying native resources, which weren't freed by your Java code.

At the end of the day, with a modern JVM, you need to write some Java code that allocates a native resource outside the normal scope of the JVM's awareness.

I thought it was interesting that no one used the internal class examples. If you have an internal class; it inherently maintains a reference to the containing class. Of course it is not technically a memory leak because Java WILL eventually clean it up; but this can cause classes to hang around longer than anticipated.

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

Now if you call Example1 and get an Example2 discarding Example1, you will inherently still have a link to an Example1 object.

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.
}
}

I've also heard a rumor that if you have a variable that exists for longer than a specific amount of time; Java assumes that it will always exist and will actually never try to clean it up if cannot be reached in code anymore. But that is completely unverified.