I was surprised to witness the Java Executor behavior – which caused “java.lang.OutOfMemoryError: unable to create new native thread” in our application. I would like to share my surprise (i.e. problem) and resolution to it.
In order to explain the problem better, I created the following example:
Above program creates 5 workers threads in the runJobs() method and executes 10 DummyJobs in parallel. In order to do so, it uses ThreadPoolExecutor.import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadLeaker { /** * Dummy Job that just prints a statement. */ private class DummyJob implements Runnable { private String jobName; public DummyJob(String jobName) { this.jobName = jobName; } @Override public void run() { System.out.println(this.jobName + " executed!"); } } public void runJobs() { // Build an executor with core pool size 5, max pool size 5 and Queue size 5 ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5)); // Execute 10 jobs; for (int counter = 0; counter < 10; ++counter) { executor.execute(new DummyJob("Dummy Job - " + counter)); } } public static void main(String args[]) { new ThreadLeaker().runJobs(); } }
In the “runJobs()” method, one would expect ThreadPoolExecutor instance & the worker threads created by that executor would be ready for garbage collection when:
1. “runJobs()” method is completed. As ‘executor’ is a local variable, it should be made available for garbage collection after the execution of the method. As variable has become out of scope.
2. All jobs that were dropped to the Executor were executed.
However surprisingly – even though both of the conditions are meet, still worker threads in the ThreadPoolExecutor instance aren’t getting garbage collected. Whenever “runJobs()” method is called, 5 new worker threads gets created and remains in the memory. If “runJobs()” is called few hundred times, several hundreds of worker threads are created. This causes memory leak in the application.
How to fix this problem?
On line number 37, one would need to invoke “shutdown()” API. So that all the worker threads would be explicitly destroyed after execution of the jobs. So revised runJobs() method would look like:
public void runJobs() { // Build an executor with core pool size 5, max pool size 5 and Queue size 5 ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5)); // Execute 10 jobs; for (int counter = 0; counter < 10; ++counter) { executor.execute(new DummyJob("Dummy Job - " + counter)); } // Explicitly calling shutdown API to destroy threads executor.shutdown(); }
What triggers this Memory Leak?
Apparently worker threads were put on to wait state by the Executor. Following is the excerpt from the thread dump of the program. Excerpt shows the stack trace of one of the worker thread in the Executor.
You can see the line “at java.util.concurrent.locks.LockSupport.park(LockSu pport.java:186)". This will park the threads thus making them not to die down. Only when shutdown() API is invoked it would unpark the threads and they would die down.
pool-1-thread-5" prio=6 tid=0x0000000007444800 nid=0x33fec waiting on condition [0x0000000009eef000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000007ac3aff48> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043) at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:374) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1043) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1103) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603) at java.lang.Thread.run(Thread.java:722) Locked ownable synchronizers: - None