[PATCH] ObjectInputStream Reading Performance Optimisation
Ben Walsh
ben_walsh at uk.ibm.com
Wed Feb 7 14:18:20 UTC 2018
As per the guidance here -
http://mail.openjdk.java.net/pipermail/valhalla-dev/2018-February/003772.html
, I am resubmitting this patch to this mailing list ...
The main advantage of this optimisation is that a suitably "aware"
compiler can avoid stack walking by using a private method named
redirectedReadObject that can be called from a call site that was calling
readObject originally (in some cases). The conditions when this can be
done are essentially when we know that the caller method's class is a
"user defined class" because we have that caller method/class at compile
time and can check if it was loaded by a user defined class loader. If so,
the compiler emits code to call the private redirectedReadObject method
(instead of readObject that was there in the byte codes) and it avoids
doing a stack walk to discover the latest user class loader because it has
an extra parameter that is a class loader that can be passed in, in cases
when the compiler knows the answer at compile time. In such cases, the
compiler generates code to pass in the latest user defined class loader
known to redirectedReadObject.
I would like to pair with a sponsor to contribute this patch ...
-----------------------------------------------------------------------------------------------------------------------
diff -r 60c19c384333 src/java.base/share/classes/java/io/ClassCache.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.base/share/classes/java/io/ClassCache.java Fri Feb 02
11:52:41 2018 +0000
@@ -0,0 +1,403 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but
WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file
that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License
version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065
USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package java.io;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+/* ClassCache is Primarily responsible for Caching the results of the
className lookups and hence to avoid
+ * multiple Lookup for same Class Instance.
+ * ClassCache provides a ConcurrentHash based ClassCache which is looked
up prior to calling the class.forName
+ * Method resolveClass() from ObjectInputStream uses this Cache.
+ *
+ * Caching is done only when the actually used loader for a Class is one
of the Sytem loaders (ie) App Class Loader,
+ * System Extension loader and BootStrap loader
+ *
+ */
+final class ClassCache {
+/* Main Cache for storing the Class.forName results Here Key used would
be CacheKey */
+ private final ConcurrentHashMap<Key,Object> cache =
+ new ConcurrentHashMap<Key,Object>();
+/* Initiating Loader to CacheKey mapping in the Cache used by Reaper
thread for removal on stale Loaders */
+ private final ConcurrentHashMap<LoaderRef,CacheKey> loaderKeys =
+ new ConcurrentHashMap<LoaderRef,CacheKey>();
+ /* Keeps a link of Actual System Loader used to Initiating Loader
mapping */
+ private final ConcurrentHashMap<LoaderRef,LoaderRef>
canonicalLoaderRefs =
+ new ConcurrentHashMap<LoaderRef,LoaderRef>();
+ /* Reference Queue registered for notification on stale Loaders */
+ private final ReferenceQueue<Object> staleLoaderRefs =
+ new ReferenceQueue<Object>();
+/*
+ * Constructor Populates Canonical Loader Refs with System Loader Entries
and initializes the reaper thread which
+ * monitors the ReferenceQueue for stale loaders
+ */
+
+ public ClassCache() {
+ ClassLoader loader = ClassLoader.getSystemClassLoader();
+ while (loader != null) {
+ setCanonicalSystemLoaderRef(loader);
+ loader = loader.getParent();
+ }
+ setCanonicalSystemLoaderRef(null);
+ AccessController.doPrivileged(
+ new CreateReaperAction(this, staleLoaderRefs)).start();
+ }
+ /*
+ * sets Canonical Loader reference for the loader
+ */
+
+ private void setCanonicalSystemLoaderRef(ClassLoader loader) {
+ LoaderRef newKey = new LoaderRef(loader, staleLoaderRefs, true);
+ assert (canonicalLoaderRefs.put(newKey, newKey) == null);
+ }
+
+ /*
+ * get Canonical Loader reference for the loader
+ */
+
+
+ LoaderRef getCanonicalLoaderRef(Object loaderObj) {
+ LoaderRef newKey = new LoaderRef(loaderObj, staleLoaderRefs);
+
+ LoaderRef key = canonicalLoaderRefs.get(newKey);
+ if (key == null) {
+ key = canonicalLoaderRefs.putIfAbsent(newKey, newKey);
+ if (key == null) {
+ return newKey;
+ }
+ }
+
+ newKey.clear();
+ return key;
+ }
+/*
+ * Remove unused LoaderKey, and initiates corresponding CacheKey entry
the main Cache
+ */
+ void removeStaleRef(LoaderRef loaderRef) {
+ canonicalLoaderRefs.remove(loaderRef);
+ CacheKey key = loaderKeys.remove(loaderRef);
+ while (key != null) {
+ cache.remove(key);
+ key = key.next;
+ }
+ }
+
+/*
+ * Identifies if the loader used to load is one of the system loaders,
+ * if so updates the cache and the LoaderKey, and also a
StaleLoaderReference for the initiating LoaderObject
+ * via LoaderRef Constructor
+ */
+ void update(CacheKey key, Class<?> result) {
+ Object resultLoaderObj =
+ LoaderRef.getLoaderObj(result.getClassLoader());
+ if (getCanonicalLoaderRef(resultLoaderObj).isSystem == false) {
+ return;
+ }
+
+ Object oldValue = cache.replace(key, result);
+ assert (oldValue instanceof FutureValue) :
+ ("Value replaced is of type '" +
oldValue.getClass().getName() +
+ "', not of type '" + FutureValue.class.getName() + "'.");
+
+ LoaderRef loaderRef = key.loaderRef;
+ if (loaderRef.isSystem == false) {
+ key.next = loaderKeys.get(loaderRef);
+ if (key.next == null) {
+ key.next = loaderKeys.putIfAbsent(loaderRef, key);
+ if (key.next == null) return;
+ }
+ while (!loaderKeys.replace(loaderRef, key.next, key)) {
+ key.next = loaderKeys.get(loaderRef);
+ }
+ }
+ }
+/*
+ * Creates a New Entry in the Cache
+ */
+ private Object createEntry(CacheKey key) {
+ FutureValue newValue = new FutureValue(key, this); //Does actual
call to class.forName as required.
+ Object value = cache.putIfAbsent(key, newValue);
+ if (value == null) value = newValue;
+ return value;
+ }
+/*
+ * This is the entry point in to the cache from ObjectInputStream. First
Lookup is done based on the className and Loader
+ */
+ public Class<?> get(String className, ClassLoader loader)
+ throws ClassNotFoundException {
+ LookupKey key = new LookupKey(className, loader, this);
+ Object value = cache.get(key);
+ if (value == null) {
+ value = createEntry(key.createCacheKey());
+ }
+
+ if (value instanceof FutureValue) {
+
+ return ((FutureValue)value).get();
+ }
+
+ return (Class<?>)value;
+ }
+
+ /*
+ * FutureValue implements Future Mechanics that is required for
addressing contention issues in the HashMap
+ */
+
+ private static final class FutureValue {
+ private final CacheKey key;
+ private final LoaderRef loaderRef;
+ private final ClassCache cache;
+ private Class<?> value = null;
+
+ FutureValue(CacheKey key, ClassCache cache) {
+ this.key = key;
+ this.loaderRef = key.loaderRef;
+ this.cache = cache;
+ }
+
+ /*
+ * tries to get the value from Cache, if not available tries to
do get class.forName with active loader
+ * and replace the entry in the cache as required
+ */
+ Class<?> get() throws ClassNotFoundException {
+ synchronized (this) {
+ if (value != null) return value;
+ value = Class.forName(key.className, false,
+ loaderRef.getActiveLoader());
+ }
+ if (value != null) {
+ cache.update(key, value);
+ }
+
+ return value;
+ }
+ }
+
+ private static final class CreateReaperAction
+ implements PrivilegedAction<Thread> {
+ private final ClassCache cache;
+ private final ReferenceQueue<Object> queue;
+
+ CreateReaperAction(ClassCache cache, ReferenceQueue<Object>
queue) {
+ this.cache = cache;
+ this.queue = queue;
+ }
+
+ public Thread run() {
+ return new Reaper(cache, queue);
+ }
+ }
+
+ private static final class Reaper extends Thread {
+ private final WeakReference<ClassCache> cacheRef;
+ private final ReferenceQueue<Object> queue;
+
+ Reaper(ClassCache cache, ReferenceQueue<Object> queue) {
+ super("ClassCache Reaper");
+ this.queue = queue;
+ cacheRef = new WeakReference<ClassCache>(cache, queue);
+ setDaemon(true);
+ setContextClassLoader(null);
+ }
+/*
+ * Blocks on remove() on queur reference and calls processStaleRef() when
any loader is removed.(non-Javadoc)
+ * @see java.lang.Thread#run()
+ */
+ public void run() {
+ Object staleRef = null;
+ do {
+ try {
+ staleRef = queue.remove();
+ if (staleRef == cacheRef) break;
+
+ processStaleRef((LoaderRef)staleRef);
+ } catch (InterruptedException e) { }
+ } while (true);
+ }
+
+ private void processStaleRef(LoaderRef staleRef) {
+ ClassCache cache = cacheRef.get();
+ if (cache == null) return;
+
+ cache.removeStaleRef(staleRef);
+ }
+ }
+
+ /*
+ * The use of the loaderRefs map is to allow efficient processing
+ * of one Weak Reference for each stale ClassLoader, rather than one
WR for each entry in the cache.
+ *
+ * CacheKey as well as Lookup Key will be refering to this LoaderRef.
+ *
+ * Initiating Class Loaders needs to be referred for both lookup and
Caching,so for performance reasons
+ * a LoaderRef is maintained which would be used by both LookupKey
and CachingKey
+ * (ie) LookupKey will actually store the LoaderObj, but it will be
canonically refered via a loaderRed
+ * by the caching Key
+ * LoaderKey has LoaderRef Objects as well and is used to Link the
Initiating Loader with the actual cache Entries
+ * which is used to remove Stale reference entries.
+ */
+
+ private static class LoaderRef extends WeakReference<Object> {
+ private static final String NULL_LOADER = new String("");
+ private final int hashcode;
+ public final boolean isSystem;
+
+ static Object getLoaderObj(ClassLoader loader) {
+ return ((loader == null) ? NULL_LOADER : loader);
+ }
+
+ LoaderRef(Object loaderObj, ReferenceQueue<Object> queue) {
+ this(false, Objects.requireNonNull(loaderObj), queue);
+ }
+
+ LoaderRef(ClassLoader loader, ReferenceQueue<Object> queue,
+ boolean isSystem) {
+ this(isSystem, getLoaderObj(loader), queue);
+ }
+
+ /*
+ * Creates a new weak reference that refers to the given object
and is registered with the given queue.
+ */
+
+ private LoaderRef(boolean isSystem, Object loaderObj,
+ ReferenceQueue<Object> queue) {
+ super(loaderObj, queue);
+ String loaderClassName = ((loaderObj == NULL_LOADER) ?
+ NULL_LOADER : loaderObj.getClass().getName());
+ hashcode = (loaderClassName.hashCode() +
+ System.identityHashCode(loaderObj));
+ this.isSystem = isSystem;
+ }
+
+ public final boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof LoaderRef)) return false;
+
+ Object loader = get();
+ return ((loader != null) && (loader ==
((LoaderRef)o).get()));
+ }
+
+ public final int hashCode() {
+ return hashcode;
+ }
+
+ ClassLoader getActiveLoader() {
+ Object loaderObj = Objects.requireNonNull(get());
+ return ((loaderObj == NULL_LOADER) ? null :
(ClassLoader)loaderObj);
+ }
+ }
+ /*
+ * For better clarity and to avoid multiple lookups to the cache. Key
is implemented to have
+ * one abstract key to final sub classes which serve specific purpose
+ * LookupKey - This is a short lived key, not part of any hashmap and
stores the strong reference to
+ * loaderobject
+ * CachingKey - uses the same hash as LookupKey and has a means to be
generated from LookupKey and has reference
+ * to the Loaderobj via a weakreference.
+ */
+
+ private static abstract class Key {
+ public final String className;
+ protected final int hashcode;
+
+ protected Key(String className, int hashcode) {
+ this.className = className;
+ this.hashcode = hashcode;
+ }
+
+ abstract Object getLoaderObj();
+
+ public final boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Key)) return false;
+
+ Key k = (Key)o;
+ Object loaderObj = getLoaderObj();
+ return (className.equals(k.className) &&
+ (loaderObj != null) && (loaderObj ==
k.getLoaderObj()));
+ }
+
+ public final int hashCode() {
+ return hashcode;
+ }
+ }
+ /*
+ * Lookup Key hash code is framed using loadername hash + loader's
system identity hashcode. This
+ * is same as the hashcode maintained in CacheKey
+ */
+
+ private static final class LookupKey extends Key {
+ private final Object loaderObj;
+ private final ClassCache cache;
+
+ private static int hashCode(String className, ClassLoader loader)
{
+ int hashcode = className.hashCode();
+ if (loader != null) {
+ hashcode += (loader.getClass().getName().hashCode() +
+ System.identityHashCode(loader));
+ }
+ return hashcode;
+ }
+
+ public LookupKey(String className, ClassLoader loader,
+ ClassCache cache) {
+ super(Objects.requireNonNull(className),
+ hashCode(className, loader));
+ loaderObj = LoaderRef.getLoaderObj(loader);
+ this.cache = cache;
+ }
+
+ Object getLoaderObj() {
+ return loaderObj;
+ }
+
+ CacheKey createCacheKey() {
+ return new CacheKey(className, hashcode,
+ cache.getCanonicalLoaderRef(loaderObj));
+ }
+ }
+
+ /*
+ * CacheKey is the actual key that is stored in the cache, and it
stores the weakreference
+ * of the Initiating loader object via loaderRef
+ *
+ */
+ private static final class CacheKey extends Key {
+ public final LoaderRef loaderRef;
+ public CacheKey next = null;
+
+ CacheKey(String className, int hashcode, LoaderRef loaderRef) {
+ super(className, hashcode);
+ this.loaderRef = loaderRef;
+ }
+
+ Object getLoaderObj() {
+ return loaderRef.get();
+ }
+ }
+}
diff -r 60c19c384333
src/java.base/share/classes/java/io/ObjectInputStream.java
--- a/src/java.base/share/classes/java/io/ObjectInputStream.java Wed Jan
31 15:15:09 2018 -0800
+++ b/src/java.base/share/classes/java/io/ObjectInputStream.java Fri Feb
02 11:52:41 2018 +0000
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1996, 2017, Oracle and/or its affiliates. All rights
reserved.
+ * Copyright (c) 1996, 2018, Oracle and/or its affiliates. All rights
reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -308,6 +308,32 @@
private ObjectInputFilter serialFilter;
/**
+ * cache LUDCL (Latest User Defined Class Loader) till completion of
+ * read* requests
+ */
+
+ /* ClassCache Entry for caching class.forName results upon
enableClassCaching */
+ private static final ClassCache classCache;
+ private static final boolean isClassCachingEnabled;
+ static {
+ isClassCachingEnabled =
+ AccessController.doPrivileged(new
GetClassCachingSettingAction());
+ classCache = (isClassCachingEnabled ? new ClassCache() : null);
+ }
+
+
+ /** if true LUDCL/forName results would be cached, true by default */
+ private static final class GetClassCachingSettingAction
+ implements PrivilegedAction<Boolean> {
+ public Boolean run() {
+ String property =
+ System.getProperty("openjdk.enableClassCaching",
"true");
+ return property.equalsIgnoreCase("true");
+ }
+ }
+ private ClassLoader cachedLudcl;
+
+ /**
* Creates an ObjectInputStream that reads from the specified
InputStream.
* A serialization stream header is read from the stream and
verified.
* This constructor will block until the corresponding
ObjectOutputStream
@@ -412,10 +438,64 @@
public final Object readObject()
throws IOException, ClassNotFoundException
{
+ return readObjectImpl(null);
+ }
+
+ /**
+ * Whenever jit compiler encounters processing readObject() method
+ * it will replace the call with redirectedReadObject() method to
improve
+ * the performance for custom serialisation. JIT provide the class
loader through
+ * the caller parameter to avoid the stack walk through while calling
+ * latestUserDefinedLoader().
+ *
+ * @throws ClassNotFoundException if the class of a serialized
object
+ * could not be found.
+ * @throws IOException if an I/O error occurs.
+ *
+ */
+
+ private static Object redirectedReadObject(ObjectInputStream iStream,
Class<?> caller)
+ throws ClassNotFoundException, IOException
+ {
+ return iStream.readObjectImpl(caller);
+ }
+
+ /**
+ * Actual implementation of readObject method which fetches
classloader using
+ * latestUserDefinedLoader() method if caller is null. If caller is
not null which means
+ * jit passes the class loader info and hence avoids calling
latestUserDefinedLoader()
+ * method call to improve the performance for custom serialisation.
+ *
+ * @throws ClassNotFoundException if the class of a serialized
object
+ * could not be found.
+ * @throws IOException if an I/O error occurs.
+ */
+ private Object readObjectImpl(Class<?> caller)
+ throws ClassNotFoundException, IOException
+ {
+
if (enableOverride) {
return readObjectOverride();
}
+ ClassLoader oldCachedLudcl = null;
+ boolean setCached = false;
+
+ if ((curContext == null) && (isClassCachingEnabled)) {
+ oldCachedLudcl = cachedLudcl;
+
+ // If caller is not provided, follow the standard path to get
the cachedLudcl.
+ // Otherwise use the class loader provided by JIT as the
cachedLudcl.
+
+ if (caller == null) {
+ cachedLudcl = latestUserDefinedLoader();
+ } else {
+ cachedLudcl = caller.getClassLoader();
+ }
+
+ setCached = true;
+ }
+
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
@@ -432,6 +512,9 @@
return obj;
} finally {
passHandle = outerHandle;
+ if (setCached) {
+ cachedLudcl = oldCachedLudcl;
+ }
if (closed && depth == 0) {
clear();
}
@@ -511,6 +594,16 @@
* @since 1.4
*/
public Object readUnshared() throws IOException,
ClassNotFoundException {
+
+ ClassLoader oldCachedLudcl = null;
+ boolean setCached = false;
+
+ if ((curContext == null) && (isClassCachingEnabled)) {
+ oldCachedLudcl = cachedLudcl;
+ cachedLudcl = latestUserDefinedLoader();
+ setCached = true;
+ }
+
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
@@ -527,6 +620,9 @@
return obj;
} finally {
passHandle = outerHandle;
+ if (setCached) {
+ cachedLudcl = oldCachedLudcl;
+ }
if (closed && depth == 0) {
clear();
}
@@ -682,7 +778,10 @@
{
String name = desc.getName();
try {
- return Class.forName(name, false, latestUserDefinedLoader());
+ return ((classCache == null) ?
+ Class.forName(name, false,
latestUserDefinedLoader()) :
+ classCache.get(name, cachedLudcl));
+
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
-----------------------------------------------------------------------------------------------------------------------
Regards,
Ben Walsh
Unless stated otherwise above:
IBM United Kingdom Limited - Registered in England and Wales with number
741598.
Registered office: PO Box 41, North Harbour, Portsmouth, Hampshire PO6 3AU
More information about the core-libs-dev
mailing list