Java Class Loading: Performance Impact
Learn more about class loading in Java and its performance impact.
Join the DZone community and get the full member experience.
Join For Freejava.lang.ClassLoader#loadClass() API is used by 3rd party libraries, JDBC Drivers, frameworks, and 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:
new io.ycrash.DummyObject();
However, you can use ClassLoader.loadClass() API and also instantiate the object. Here is how the code will look:
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
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. For 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 instantiated at the time of writing the code. In such circumstances, you will have to end up using the ‘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 the ‘ClassLoader.loadClass()
’ API is used.
Apache Xalan
When you use the Apache Xalan framework to serialize and deserialize XML, ‘ClassLoader.loadClass()
’ API will be used. Below is the stack trace of a thread that 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.j
ava: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 the Google GUICE framework, ‘ClassLoader.loadClass()
’ API will be used. Below is the stack trace of a thread that 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, the ‘ClassLoader.loadClass()
’ API will be used. Below is the stack trace of a thread that 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 the AspectJ library, ‘ClassLoader.loadClass()
’ API will be used. Below is the stack trace of a thread that 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(ReflectionBase
dReferenceTypeDelegateFactory.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(PointcutExpressi
onImpl.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 of the Java class loading. Now it’s time to study its performance impact. To facilitate our study, I created this simple program:
package io.ycrash.classloader;
public class MyApp extends Thread {
@Override
public void run() {
try {
while (true) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
myClass.newInstance();
}
} catch (Exception e) {
}
}
public static void main(String args[]) throws Exception {
for (int counter = 0; counter < 10; ++counter) {
new MyApp().start();
}
}
}
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. The 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 the BLOCKED state. If a thread is in the 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 the 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):
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
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 the BLOCKED state also have the exact same stack trace.
Below is the stack trace of ‘Thread-6’ who is blocking all other 9 threads:
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
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.
Why Do Threads Become Blocked When Invoking Classloader.loadclass()?
To understand why threads enter into the 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 to it here:
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);
}
:
:
In the highlighted line of the source code, you will see the usage of the ‘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, the remaining 9 threads will be put into a BLOCKED state.
Below is the source code of ‘getClassLoadingLock()
’ method which returns an object and upon which synchronization happens.
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;
}
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.
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.
package io.ycrash.classloader;
public class MyApp extends Thread {
private Class<?> myClass = initClass();
private Class<?> initClass() {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
return classLoader.loadClass("io.ycrash.DummyObject");
} catch (Exception e) {
}
return null;
}
@Override
public void run() {
while (true) {
try {
myClass.newInstance();
} catch (Exception e) {
}
}
}
public static void main(String args[]) throws Exception {
for (int counter = 0; counter < 10; ++counter) {
new MyApp().start();
}
}
}
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, the ‘ClassLoader.loadClass()
’ API will not be called multiple times. Thus it will prevent threads from entering into the BLOCKED state.
Solutions
If your application also encounters this classloading performance problem, then here are the potential solutions to resolve it.
- Try to see whether you can invoke the ‘
ClassLoader.loadClass()
’ API during application startup time instead of run-time. - 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.
- 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.
Published at DZone with permission of Ram Lakshmanan, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments