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 don’t 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 let’s 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 it’s 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 it’s in a BLOCKED state, it wouldn’t progress forward. It will hamper application’s 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. It’s 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.