Developer Tools
Reading the classloader hierarchy with a debugging example
A practical guide to the Java classloader hierarchy, showing how delegation works and how to use it when a class resolves from an unexpected location. It ends with a worked debugging case.
01
Step one: map the loaders
Start by listing which loaders exist in your process. In a standard Java setup the bootstrap loader handles core classes, the platform loader handles platform modules, and the application loader handles your classpath. Frameworks and servers add their own children. For any suspect class, print its loader, then walk up through each parent and note the order. This gives you a written chain rather than a guess. Many mysterious errors become obvious once you see that two parts of the application sit under different loaders and therefore cannot see each other's classes, even when the names match exactly.
02
Step two: apply parent-first delegation
Next, reason about delegation. By default a loader asks its parent before trying itself, so a class present in a higher loader wins over a copy lower down. That rule explains why bundling your own copy of a library sometimes appears to be ignored. Check whether the class exists higher in the chain, then check whether your framework reverses the order, as some web containers do for application code. Write down which direction applies to the class you are investigating. This decision determines where a fix belongs, whether in packaging, loader configuration or dependency scope.
03
Step three: confirm with a worked example
Suppose a service throws a NoSuchMethodError for a JSON library method you know exists in your dependency. Print the class's code source location and its loader. The output shows the class came from a server-provided older jar, not your bundled newer one, because the parent loader supplied it first. You now have evidence, not a theory. Remove the duplicate from the server, adjust the delegation setting, or rename the shaded package, then rerun the same print statement to confirm the new origin. Keep that check as a small startup diagnostic so future conflicts surface before release.