ThreadLocal. Proceed with caution!

by perfstories

…Especially, when you have large-size objects and a lot of class loaders.

I’ve got very high memory footprint several times because ThreadLocal contained very large-size objects. And moreover, there were several ThreadLocals ([2]) as well, as each Glassfish application has it’s own Class Loader ([1]).

What is memory footprint? Briefly speaking, it’s how much memory your application uses. We will talk about java heap memory further.

In cases mentioned above I saw approximately the following picture:

So, on the left part you can see the usual (normal) picture, on the right part you can see the situation on the new build flavoured with ThreadLocals.

(Also, there is written: “application start”. However, this depends on situation. It could be start of load, and then starting point is above zero, of course.)

The main point is very simple. Say, ThreadLocal contains an object about N Mb. So, we could get (in our application) N*C*T Mb of memory occupied with such objects, where C is the number of Class Loaders and T is the number of working threads

So the typical situation for us is: ~100 threads, ~6 Class Loaders. So, if the object is about 4Mb, there will be 100*4*6 > 2 Gb, i.e. a lot. So the application has no chances to survive (of course it depends on heap size, but anyway).

If the object is about 4 Kb, we will get 2 Mb. And it’s, perhaps, much better.

What is the medicine? There should be an individual approach.

For example, it depends on whether your large-size object is thread-safe or not. I had a situation where JAXBContext played a role of large-size object. Fortunately, it turned out to be thread-safe (http://jaxb.java.net/guide/Performance_and_thread_safety.html). So I just made it static.

If you don’t believe that N*C*T Mb of memory will be occupied (see above), I wrote a simple reproducer, you don’t need Glassfish for it 🙂

So:

The main class is regular.Test:


package regular; import java.util.HashSet; import java.util.concurrent.atomic.AtomicInteger; public class Test { //a number of "applications" (different Class Loaders) public static int APPLICATIONS_NUMBER = 13; //a number of threads (like thread pool in Glassfish) public static int THREADS_NUM = 16; //loops iterations for each thread public static final int THREAD_LOOPS = 100; private static ApplicationClassLoader[] loaders; private static Application[] applications; private static int index = 0; private static AtomicInteger hugeObjectsCounter = new AtomicInteger(0); public static void main(String[] args) throws Exception { loaders = new ApplicationClassLoader[APPLICATIONS_NUMBER]; applications = new Application[APPLICATIONS_NUMBER]; Thread[] threads = new Thread[THREADS_NUM]; for (int i = 0; i < APPLICATIONS_NUMBER; i ++) { //create Class Loader loaders[i] = new ApplicationClassLoader(); //create an "application" applications[i] = (Application)loader[i].loadClass ("special.ApplicationImpl") .newInstance(); } //create and start threads for(int i = 0; i < THREADS_NUM; i ++) { threads[i] = new SomeThread(); threads[i].start(); } for(int i = 0; i < THREADS_NUM; i ++) { threads[i].join(); } //So, the main point of this story is: //if loops number is big enough, //there will be created //APPLICATIONS_NUMBER * THREADS_NUM huge objects System.out.println("Total: " + hugeObjectsCounter.intValue() + " huge objects created"); } public static synchronized Application requestWork() { index = (index + 1) % APPLICATIONS_NUMBER; return applications[index]; } // As you can see further, each "application" //ThraedLocal contains huge objects. //So we do count them in this method public static void noteOneMoreHugeObjectCreated() { hugeObjectsCounter.incrementAndGet(); } }
package regular; public class SomeThread extends Thread { public void run() { for (int i = 0; i < Test.THREAD_LOOPS; i ++) { Test.requestWork().doSomething(); } } }
package regular; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; public class ApplicationClassLoader extends ClassLoader { private static final int BUFFER_SIZE = 1024; public synchronized Class loadClass(String className) throws ClassNotFoundException { if (!className.startsWith("special")) { return super.loadClass(className); } Class c = findLoadedClass(className); if (c != null) { return c; } try { InputStream in = getResourceAsStream(className.replace('.', '/') + ".class"); byte[] buffer = new byte[BUFFER_SIZE]; ByteArrayOutputStream out = new ByteArrayOutputStream(); int n = 0; while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) { out.write(buffer, 0, n); } byte[] bytes = out.toByteArray(); return defineClass(className, bytes, 0, bytes.length()); } catch (IOException e) { e.printStackTrace(); } return null; } }
package regular; public interface Application { void doSomething(); }
package special; public class AHugeClass { public static void doSomethingVeryImportant() { } }
package special; import regular.Application; import regular.Test; public class ApplicationImpl implements Application { public static Object field; static ThreadLocal hugeField = new ThreadLocal(){ @Override protected AHugeClass initialValue() { AHugeClass result = new AHugeClass(); Test.noteOneMoreHugeObjectCreated(); return result; } }; public void doSomething() { hugeField.get().doSomethingVeryImportant(); } }

[1]About Class Loaders: http://www.javaworld.com/javaworld/jw-10-1996/jw-10-indepth.html

[2]About ThreadLocal in Java: http://www.ibm.com/developerworks/java/library/j-threads3/index.html

Advertisements