[PATCH] 8246633: Improve the performance of ObjectInputStream.resolveClass(ObjectStreamClass)
Peter Kessler OS
peter.kessler at os.amperecomputing.com
Thu Jun 4 23:08:40 UTC 2020
ObjectInputStream.resolveClass(ObjectStreamClass) is specified to find "the first class loader on the current thread's stack (starting from the currently executing method) that is neither the platform class loader nor its ancestor." Finding that class loader is done with a virtual frame walk in JVM_LatestUserDefinedLoader(JNIEnv *env). The virtual frame walk is expensive. If an application does not define any ClassLoaders, then the virtual frame walk will always find the system class loader.
If an application does a significant amount of reading from an ObjectInputStream, the cost of ObjectInputStream.resolveClass(ObjectInputStreamClass) can be considerable. In a sample application, that method (and the methods it calls) have been measured to consume 5+% of the CPU cycles.
The proposal is to note the construction of any user-defined ClassLoader, and if none has been constructed, avoid the virtual frame walk returning ClassLoader.getSystemClassLoader(). But if any user-defined ClassLoader has been constructed, use the virtual frame walk to find the first user-defined ClassLoader on the thread stack.
User-defined ClassLoaders could be distinguished from non-user-defined ClassLoaders in several ways. The ClassLoader class hierarchy could be changed to make non-user-defined ClassLoaders inherit from a marker class. There is already a jdk.internal.loader.BuiltinClassLoader but it is used for module (and resource) loading. BuiltinClassLoader is not a super-class of jdk.internal.reflect.DelegatingClassLoader, which is used to load reflection classes, and reflection methods are also ignored by the virtual frame walk. ClassLoaders could have a boolean that distinguished whether they are user-defined or not. That would require a change to ClassLoader API. There are other ways to distinguish user-defined ClassLoaders. The proposal is to keep the decoration of the ClassLoaders out of the instances, so that it is only visible to the code that needs it.
The proposal is that each non-user-defined ClassLoader have a static block that registers the class as a non-user-defined ClassLoader. Then in the base ClassLoader constructor check if the ClassLoader being constructed is a user-defined ClassLoader and if so set a boolean that acts as the guard on the virtual frame walk. If additional non-user-defined ClassLoaders are declared in the future beyond the ones identified in this patch, they can be added to the registry.
There are currently 4 non-user-defined ClassLoader classes, so the registry is not difficult to maintain. Nor is the registry difficult to search when ClassLoaders are constructed. The guard boolean that records whether a user-defined ClassLoader has been constructed transitions from false to true and never becomes false again. (An extension to transition the boolean back to false when a user-defined ClassLoader becomes unreachable is beyond the scope of this proposal.)
This proposal slows loading of non-user-defined ClassLoaders by the time it takes to register them. It slows ClassLoader construction by the time it takes to consult the registry and conditionally set the guard boolean. At each invocation of ObjectInputStream.resolveClass(ObjectInputStreamClass), the guard boolean is used to possibly avoid the virtual frame walk.
Tested with `make run-test-tier1` on Linux (CentOS 7) on both aarch64 and x86_64. I had one failure on each of the runs
ACTION: main -- Failed. Execution failed: `main' threw exception: java.lang.Error: TESTBUG: the library has not been found in ${Repo}/build/linux-aarch64-server-release/images/test/hotspot/jtreg/native
REASON: User specified action: run main/native GTestWrapper
on both plaforms. But I get the same failure when I run the tier1 tests on clones of OpenJDK-15+25 on both aarch64 and x86_64.
Running a sample (representative?) application with the Oracle Developer Studio analyzer shows a performance comparison of
Stack Fragment sorted by metric: Attributed Total CPU Time
baseline.er experiment.er
Attributed Attributed Name
Total CPU Time Total CPU Time
sec. sec.
================================== Callers
3459.210 29.931 java.io.ObjectInputStream.readOrdinaryObject(boolean)
1139.727 3.532 java.io.ObjectInputStream.readArray(boolean)
875.262 9.116 java.io.ObjectInputStream.readNonProxyDesc(boolean)
================================== Stack Fragment
java.io.ObjectInputStream.readClassDesc(boolean)
java.io.ObjectInputStream.readNonProxyDesc(boolean)
java.io.ObjectInputStream.resolveClass(java.io.ObjectStreamClass)
java.io.ObjectInputStream.latestUserDefinedLoader()
4.173 3.953 jdk.internal.misc.VM.latestUserDefinedLoader()
================================== Callees
5470.026 0. jdk.internal.misc.VM.latestUserDefinedLoader0()
0. 38.627 java.lang.ClassLoader.getSystemClassLoader()
The `hg export -g` below is with respect to OpenJDK-15+25.
Thank you for your review comments. I will need a sponsor to get this change into the repository.
... peter
# HG changeset patch
# User Peter Kessler <Peter.Kessler at OS.AmpereComputing.COM>
# Date 1591310467 25200
# Thu Jun 04 15:41:07 2020 -0700
# Node ID 9a39488182c1dfc5bc7bb41d562d7ef16ee657f6
# Parent 90b266a84c06f1b3dc0ed8767856793e8c1c357e
Improve the performance of ObjectInputStream.resolveClass
diff --git a/src/java.base/share/classes/java/lang/ClassLoader.java b/src/java.base/share/classes/java/lang/ClassLoader.java
--- a/src/java.base/share/classes/java/lang/ClassLoader.java
+++ b/src/java.base/share/classes/java/lang/ClassLoader.java
@@ -385,6 +385,8 @@
}
this.package2certs = new ConcurrentHashMap<>();
this.nameAndId = nameAndId(this);
+ /* Note the construction of a ClassLoader. */
+ VM.noteClassLoaderConstruction(this.getClass());
}
/**
diff --git a/src/java.base/share/classes/jdk/internal/loader/ClassLoaders.java b/src/java.base/share/classes/jdk/internal/loader/ClassLoaders.java
--- a/src/java.base/share/classes/jdk/internal/loader/ClassLoaders.java
+++ b/src/java.base/share/classes/jdk/internal/loader/ClassLoaders.java
@@ -117,6 +117,11 @@
protected Class<?> loadClassOrNull(String cn, boolean resolve) {
return JLA.findBootstrapClassOrNull(this, cn);
}
+
+ static {
+ /* Register this ClassLoader as not a user-defined ClassLoader. */
+ VM.registerNotUserDefinedClassLoader(BootClassLoader.class);
+ }
};
/**
@@ -127,6 +132,8 @@
static {
if (!ClassLoader.registerAsParallelCapable())
throw new InternalError();
+ /* Register this ClassLoader as not a user-defined ClassLoader. */
+ VM.registerNotUserDefinedClassLoader(PlatformClassLoader.class);
}
PlatformClassLoader(BootClassLoader parent) {
@@ -142,6 +149,8 @@
static {
if (!ClassLoader.registerAsParallelCapable())
throw new InternalError();
+ /* Register this ClassLoader as not a user-defined ClassLoader. */
+ VM.registerNotUserDefinedClassLoader(AppClassLoader.class);
}
final URLClassPath ucp;
diff --git a/src/java.base/share/classes/jdk/internal/misc/VM.java b/src/java.base/share/classes/jdk/internal/misc/VM.java
--- a/src/java.base/share/classes/jdk/internal/misc/VM.java
+++ b/src/java.base/share/classes/jdk/internal/misc/VM.java
@@ -304,12 +304,44 @@
private static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 0x0010;
private static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 0x0020;
+ /** A registry of not user-defined ClassLoaders. */
+ private static final List<Class<? extends ClassLoader>> notUserDefinedClassLoaderRegistry =
+ Collections.synchronizedList(new ArrayList<>());
+
+ /** Register a ClassLoader class as being not a user-defined ClassLoader. */
+ public static void registerNotUserDefinedClassLoader(Class<? extends ClassLoader> classLoaderClass) {
+ notUserDefinedClassLoaderRegistry.add(classLoaderClass);
+ }
+
+ /** A flag for whether a user-defined ClassLoaders has been constructed. */
+ private static volatile boolean constructedUserDefinedClassLoaderFlag = false;
+
+ /**
+ * Note a ClassLoader construction, and record if a
+ * user-defined ClassLoader has been constructed.
+ */
+ public static void noteClassLoaderConstruction(Class<? extends ClassLoader> classLoaderClass) {
+ /* Check if the ClassLoader class not a user-defined ClassLoader. */
+ if (notUserDefinedClassLoaderRegistry.contains(classLoaderClass)) {
+ return;
+ }
+ constructedUserDefinedClassLoaderFlag = true;
+ return;
+ }
+
/*
* Returns the first user-defined class loader up the execution stack,
* or the platform class loader if only code from the platform or
* bootstrap class loader is on the stack.
*/
public static ClassLoader latestUserDefinedLoader() {
+ if (!constructedUserDefinedClassLoaderFlag) {
+ /*
+ * If no user-defined ClassLoader has been constructed,
+ * then I will not find a user-defined ClassLoader in the stack.
+ */
+ return ClassLoader.getSystemClassLoader();
+ }
ClassLoader loader = latestUserDefinedLoader0();
return loader != null ? loader : ClassLoader.getPlatformClassLoader();
}
diff --git a/src/java.base/share/classes/jdk/internal/reflect/ClassDefiner.java b/src/java.base/share/classes/jdk/internal/reflect/ClassDefiner.java
--- a/src/java.base/share/classes/jdk/internal/reflect/ClassDefiner.java
+++ b/src/java.base/share/classes/jdk/internal/reflect/ClassDefiner.java
@@ -30,6 +30,7 @@
import jdk.internal.access.JavaLangAccess;
import jdk.internal.access.SharedSecrets;
+import jdk.internal.misc.VM;
/** Utility class which assists in calling defineClass() by
creating a new class loader which delegates to the one needed in
@@ -73,4 +74,9 @@
DelegatingClassLoader(ClassLoader parent) {
super(parent);
}
+
+ static {
+ /* Register this ClassLoader as not a user-defined ClassLoader. */
+ VM.registerNotUserDefinedClassLoader(DelegatingClassLoader.class);
+ }
}
More information about the core-libs-dev
mailing list