java.lang.ClassLoader#loadClass() API is used by 3rd party libraries, JDBC Drivers, frameworks, application servers to load a java class into the memory. Application developers dont use this API frequently. However when they use the APIs such as java.lang.Class.forName() or org.springframework.util.ClassUtils.forName(), they internally call this java.lang.ClassLoader#loadClass() API.
Frequent usage of this API amongst different threads at runtime can slow down your application performance. Sometimes it can even make the entire application unresponsive. In this post lets understand this API a little bit more and its performance impact.
What is the purpose of ClassLoader.loadClass() API?
Typically, if we want to instantiate a new object, we write the code like this:
However, you can use ClassLoader.loadClass() API and also instantiate the object. Here is how code will look:1. new io.ycrash.DummyObject();
1. ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 2. Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject"); 3. myClass.newInstance();
You can notice in line #2 classLoader.loadClass() is invoked. This line will load the io.ycrash.DummyObject class into memory. In line #3 io.ycrash.DummyObject class is instantiated using the newInstance() API.
This way of instantiating the object is like touching the nose with your hand, by going through the back of your neck. You might wonder why someone might do this? You can instantiate the object using new only if you know the name of the class at the time of writing the code. In certain circumstances you might know the name of the class only during run-time. Example if you are writing frameworks (like Spring Framework, XML parser, ) you will know the class names to be instantiated only during runtime. You will not know what classes you will be instantiating at the time of writing the code. In such circumstances you will have to end-up using ClassLoader.loadClass() API.
Where ClassLoader.loadClass() is used?
ClassLoader.loadClass() is used in several popular 3rd party libraries, JDBC Drivers, frameworks & application servers. This section highlights a few popular frameworks where ClassLoader.loadClass() API is used.
Apache Xalan
When you use Apache Xalan framework to serialize and deserialize XML, ClassLoader.loadClass() API will be used. Below is the stacktrace of a thread which is using ClassLoader.loadClass() API from the Apache Xalan framework.
at java.lang.ClassLoader.loadClass(ClassLoader.java:404) - locked <0x6d497769> (a com.wm.app.b2b.server.ServerClassLoader) at com.wm.app.b2b.server.ServerClassLoader.loadClass(ServerClassLoader.java:1175) at com.wm.app.b2b.server.ServerClassLoader.loadClass(ServerClassLoader.java:1108) at org.apache.xml.serializer.ObjectFactory.findProviderClass(ObjectFactory.java:503) at org.apache.xml.serializer.SerializerFactory.getSerializer(SerializerFactory.java:129) at org.apache.xalan.transformer.TransformerIdentityImpl.createResultContentHandler(TransformerIdentityImpl.java:260) at org.apache.xalan.transformer.TransformerIdentityImpl.transform(TransformerIdentityImpl.java:330) at org.springframework.ws.client.core.WebServiceTemplate$4.extractData(WebServiceTemplate.java:441) : :
Google GUICE Framework
When you use Google GUICE framework, ClassLoader.loadClass() API will be used. Below is the stacktrace of a thread which is using ClassLoader.loadClass() API from the Google GUICE framework.
at java.lang.Object.wait(Native Method) - waiting on hudson.remoting.RemoteInvocationHandler$RPCRequest@1e408f0 at hudson.remoting.Request.call(Request.java:127) at hudson.remoting.RemoteInvocationHandler.invoke(RemoteInvocationHandler.java:160) at $Proxy5.fetch2(Unknown Source) at hudson.remoting.RemoteClassLoader.findClass(RemoteClassLoader.java:122) at java.lang.ClassLoader.loadClass(ClassLoader.java:321) - locked hudson.remoting.RemoteClassLoader@15c7850 at java.lang.ClassLoader.loadClass(ClassLoader.java:266) at com.google.inject.internal.BindingProcessor.visit(BindingProcessor.java:69) at com.google.inject.internal.BindingProcessor.visit(BindingProcessor.java:43) at com.google.inject.internal.BindingImpl.acceptVisitor(BindingImpl.java:93) at com.google.inject.internal.AbstractProcessor.process(AbstractProcessor.java:56) at com.google.inject.internal.InjectorShell$Builder.build(InjectorShell.java:183) at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:104) - locked com.google.inject.internal.InheritingState@1c915a5 at com.google.inject.Guice.createInjector(Guice.java:94) at com.google.inject.Guice.createInjector(Guice.java:71) at com.google.inject.Guice.createInjector(Guice.java:61) : :
Oracle JDBC Driver
If you use Oracle JDBC Driver, ClassLoader.loadClass() API will be used. Below is the stacktrace of a thread which is using ClassLoader.loadClass() API from the Oracle JDBC Driver.
at com.ibm.ws.classloader.CompoundClassLoader.loadClass(CompoundClassLoader.java:482) - waiting to lock <0xffffffff11a5f7d8> (a com.ibm.ws.classloader.CompoundClassLoader) at java.lang.ClassLoader.loadClass(ClassLoader.java:247) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:170) at oracle.jdbc.driver.PhysicalConnection.safelyGetClassForName(PhysicalConnection.java:4682) at oracle.jdbc.driver.PhysicalConnection.addClassMapEntry(PhysicalConnection.java:2750) at oracle.jdbc.driver.PhysicalConnection.addDefaultClassMapEntriesTo(PhysicalConnection.java:2739) at oracle.jdbc.driver.PhysicalConnection.initializeClassMap(PhysicalConnection.java:2443) at oracle.jdbc.driver.PhysicalConnection.ensureClassMapExists(PhysicalConnection.java:2436) : :
AspectJ library
If you use AspectJ library, ClassLoader.loadClass() API will be used. Below is the stacktrace of a thread which is using ClassLoader.loadClass() API from the AspectJ framework.
: : at java.base@11.0.7/java.lang.ClassLoader.loadClass(ClassLoader.java:522) at java.base@11.0.7/java.lang.Class.forName0(Native Method) at java.base@11.0.7/java.lang.Class.forName(Class.java:398) at app//org.aspectj.weaver.reflect.ReflectionBasedReferenceTypeDelegateFactory.createDelegate(ReflectionBasedReferenceTypeDelegateFactory.java:38) at app//org.aspectj.weaver.reflect.ReflectionWorld.resolveDelegate(ReflectionWorld.java:195) at app//org.aspectj.weaver.World.resolveToReferenceType(World.java:486) at app//org.aspectj.weaver.World.resolve(World.java:321) - locked java.lang.Object@1545fe7d at app//org.aspectj.weaver.World.resolve(World.java:231) at app//org.aspectj.weaver.World.resolve(World.java:436) at app//org.aspectj.weaver.internal.tools.PointcutExpressionImpl.couldMatchJoinPointsInType(PointcutExpressionImpl.java:83) at org.springframework.aop.aspectj.AspectJExpressionPointcut.matches(AspectJExpressionPointcut.java:275) at org.springframework.aop.support.AopUtils.canApply(AopUtils.java:225) : :
Studying Performance impact
Now I assume you have got sufficient understanding about the Java class loading. Now its time to study its performance impact. To facilitate our study, I created this simple program:
1. package io.ycrash.classloader; 2. 3. public class MyApp extends Thread { 4. 5. @Override 6. public void run() { 7. 8. try { 9. 10. while (true) { 11. 12. ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 13. Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject"); 14. myClass.newInstance(); 15. } 16. } catch (Exception e) { 17. 18. } 19. } 20. 21. public static void main(String args[]) throws Exception { 22. 23. for (int counter = 0; counter < 10; ++counter) { 24. 25. new MyApp().start(); 26. } 27. } 28. }
If you notice this program, I am creating 10 threads in the main() method.
Each thread goes on an infinite loop and instantiates io.ycrash.DummyObject in the run() method, using the classLoader.loadClass() API in line# 13. It means classLoader.loadClass() going to be called repeatedly again and again by all these 10 threads.
ClassLoader.loadClass() BLOCKED threads
We executed the above program. While the program was executing we ran the open source yCrash script. This script captures 360-degree data (thread dump, GC log, heap dump, netstat, VMstat, iostat, top, kernel logs, ) from the application. We analyzed the captured thread dump using fastThread a thread dump analysis tool. Thread dump analysis report generated by this tool for this program can be found here. Tool reported that 9 threads out of 10 were in BLOCKED state. If a thread is in BLOCKED state, it indicates that it is stuck for a resource. When its in a BLOCKED state, it wouldnt progress forward. It will hamper applications performance. You might wonder Why does the above simple program make the threads enter into the BLOCKED state.
Fig: transitive graph showing 9 BLOCKED threads (generated by fastThread)
Above is the excerpt from the thread dump analysis report. You can see that 9 threads (Thread-0, Thread-1, Thread-2, Thread-3, Thread-4, Thread-5, Thread-7, Thread-8, Thread-9) are BLOCKED by the Thread-6. Below is the stack trace of the one BLOCKED state thread (i.e. Thread-9):
You can notice that Thread-9 is BLOCKED on the java.lang.ClassLoader.loadClass() method. Its waiting to acquire a lock on <0x00000003db200ae0>. All other remaining 8 threads which are in BLOCKED state also have the exact same stacktrace.Thread-9 Stack Trace is: java.lang.Thread.State: BLOCKED (on object monitor) at java.lang.ClassLoader.loadClass(ClassLoader.java:404) - waiting to lock <0x00000003db200ae0> (a java.lang.Object) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at io.ycrash.classloader.MyApp.run(MyApp.java:13) Locked ownable synchronizers: - None
Below is the stack trace of Thread-6 who is blocking all other 9 threads:
You can notice that Thread-6 was able to acquire the lock (i.e. <0x00000003db200ae0>) and progress further. However, all other 9 threads are stuck waiting to acquire this lock.Thread-6 java.lang.Thread.State: RUNNABLE at java.lang.ClassLoader.findLoadedClass0(Native Method) at java.lang.ClassLoader.findLoadedClass(ClassLoader.java:1038) at java.lang.ClassLoader.loadClass(ClassLoader.java:406) - locked <0x00000003db200ae0> (a java.lang.Object) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at io.ycrash.classloader.MyApp.run(MyApp.java:13) Locked ownable synchronizers: - None
Why do threads become BLOCKED when invoking ClassLoader.loadClass()?
To understand why threads enter into BLOCKED state when invoking ClassLoader.loadClass() method, we will have to look at its source code. Below is the source code excerpt of ClassLoader.loadClass() method. If you would like to see the complete source code of java.lang.ClassLoader, you may refer here:
In the highlighted line of the source code, you will see the usage of synchronized code block. When a block of code is synchronized, only one thread will be allowed to enter that block. In our above example 10 threads are trying to access ClassLoader.loadClass() concurrently. Only one thread will be allowed to enter into the synchronized code block, remaining 9 thread will be put into BLOCKED state.protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } : :
Below is the source code of getClassLoadingLock() method which returns an object and upon which synchronization happens.
You can notice that the getClassLoadingLock() method will return the same object every time for the same class name. i.e. if the class name is io.ycrash.DummyObject it will return the same object every time. Thus all the 10 threads will be getting back the same object. And on this one single object, synchronization will happen. It will put all the threads into the BLOCKED state.protected Object getClassLoadingLock(String className) { Object lock = this; if (parallelLockMap != null) { Object newLock = new Object(); lock = parallelLockMap.putIfAbsent(className, newLock); if (lock == null) { lock = newLock; } } return lock; }
How to fix this problem?
This problem is stemming because io.ycrash.DummyObject class is loaded again & again on every loop iteration. This causes the threads to enter into the BLOCKED state. This problem can be short-circuited, if we can load the class only once during application startup time. This can be achieved by modifying the code as shown below.
Making this code change resolved the issue. If you see now myClass is initialized in line# 5. Unlike the earlier approach where myClass was initialized every single loop iteration, now myClass is initialized only once when the Thread is instantiated. Because of this shift in the code, ClassLoader.loadClass() API will not be called multiple times. Thus it will prevent threads from entering into the BLOCKED state.1. package io.ycrash.classloader; 2. 3. public class MyApp extends Thread { 4. 5. private Class<?> myClass = initClass(); 6. 7. private Class<?> initClass() { 8. 9. try { 10. ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 11. return classLoader.loadClass("io.ycrash.DummyObject"); 12. } catch (Exception e) { 13. } 14. 15. return null; 16. } 17. 18. @Override 19. public void run() { 20. 21. while (true) { 22. 23. try { 24. myClass.newInstance(); 25. } catch (Exception e) { 26. } 27. } 28. } 29. 30. public static void main(String args[]) throws Exception { 31. 32. for (int counter = 0; counter < 10; ++counter) { 33. 34. new MyApp().start(); 35. } 36. } 37. }
Solutions
If your application also encounters this classloading performance problem, then here are the potential solutions to resolve it.
a. Try to see whether you can invoke ClassLoader.loadClass() API during application startup time instead of run-time.
b. If your application is loading the same class again & again at runtime, then try to load the class only once. After that point, cache the class and re-use it, as shown in the above example.
c. Use the troubleshooting tools like fastThread, yCrash, to detect which framework or 3rd party library or code path is triggering the problem. Check whether frameworks have given any fixes in their latest version, if so upgrade to the latest version.