8078641: MethodHandle.asTypeCache can retain classes from unloading

Co-authored-by: Peter Levart <plevart@openjdk.org>
Co-authored-by: Vladimir Ivanov <vlivanov@openjdk.org>
Reviewed-by: psandoz, mchung, plevart
This commit is contained in:
Vladimir Ivanov 2021-09-08 11:27:14 +00:00
parent 185557423d
commit 21012f2bbe
2 changed files with 106 additions and 21 deletions

View file

@ -26,6 +26,7 @@
package java.lang.invoke; package java.lang.invoke;
import jdk.internal.loader.ClassLoaders;
import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.IntrinsicCandidate;
import java.lang.constant.ClassDesc; import java.lang.constant.ClassDesc;
@ -33,6 +34,7 @@ import java.lang.constant.Constable;
import java.lang.constant.DirectMethodHandleDesc; import java.lang.constant.DirectMethodHandleDesc;
import java.lang.constant.MethodHandleDesc; import java.lang.constant.MethodHandleDesc;
import java.lang.constant.MethodTypeDesc; import java.lang.constant.MethodTypeDesc;
import java.lang.ref.SoftReference;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -449,12 +451,9 @@ public abstract class MethodHandle implements Constable {
@interface PolymorphicSignature { } @interface PolymorphicSignature { }
private final MethodType type; private final MethodType type;
/*private*/ /*private*/ final LambdaForm form; // form is not private so that invokers can easily fetch it
final LambdaForm form; private MethodHandle asTypeCache;
// form is not private so that invokers can easily fetch it private SoftReference<MethodHandle> asTypeSoftCache;
/*private*/
MethodHandle asTypeCache;
// asTypeCache is not private so that invokers can easily fetch it
private byte customizationCount; private byte customizationCount;
@ -855,34 +854,120 @@ public abstract class MethodHandle implements Constable {
* @throws WrongMethodTypeException if the conversion cannot be made * @throws WrongMethodTypeException if the conversion cannot be made
* @see MethodHandles#explicitCastArguments * @see MethodHandles#explicitCastArguments
*/ */
public MethodHandle asType(MethodType newType) { public final MethodHandle asType(MethodType newType) {
// Fast path alternative to a heavyweight {@code asType} call. // Fast path alternative to a heavyweight {@code asType} call.
// Return 'this' if the conversion will be a no-op. // Return 'this' if the conversion will be a no-op.
if (newType == type) { if (newType == type) {
return this; return this;
} }
// Return 'this.asTypeCache' if the conversion is already memoized. // Return 'this.asTypeCache' if the conversion is already memoized.
MethodHandle atc = asTypeCached(newType); MethodHandle at = asTypeCached(newType);
if (atc != null) { if (at != null) {
return atc; return at;
} }
return asTypeUncached(newType); return setAsTypeCache(asTypeUncached(newType));
} }
private MethodHandle asTypeCached(MethodType newType) { private MethodHandle asTypeCached(MethodType newType) {
MethodHandle atc = asTypeCache; MethodHandle atc = asTypeCache;
if (atc != null && newType == atc.type) { if (atc != null && newType == atc.type) {
return atc; return atc; // cache hit
}
SoftReference<MethodHandle> softCache = asTypeSoftCache;
if (softCache != null) {
atc = softCache.get();
if (atc != null && newType == atc.type) {
return atc; // soft cache hit
}
} }
return null; return null;
} }
private MethodHandle setAsTypeCache(MethodHandle at) {
// Don't introduce a strong reference in the cache if newType depends on any class loader other than
// current method handle already does to avoid class loader leaks.
if (isSafeToCache(at.type)) {
asTypeCache = at;
} else {
asTypeSoftCache = new SoftReference<>(at);
}
return at;
}
/** Override this to change asType behavior. */ /** Override this to change asType behavior. */
/*non-public*/ /*non-public*/
MethodHandle asTypeUncached(MethodType newType) { MethodHandle asTypeUncached(MethodType newType) {
if (!type.isConvertibleTo(newType)) if (!type.isConvertibleTo(newType)) {
throw new WrongMethodTypeException("cannot convert "+this+" to "+newType); throw new WrongMethodTypeException("cannot convert " + this + " to " + newType);
return asTypeCache = MethodHandleImpl.makePairwiseConvert(this, newType, true); }
return MethodHandleImpl.makePairwiseConvert(this, newType, true);
}
/**
* Returns true if {@code newType} does not depend on any class loader other than current method handle already does.
* May conservatively return false in order to be efficient.
*/
private boolean isSafeToCache(MethodType newType) {
ClassLoader loader = getApproximateCommonClassLoader(type);
return keepsAlive(newType, loader);
}
/**
* Tries to find the most specific {@code ClassLoader} which keeps all the classes mentioned in {@code mt} alive.
* In the worst case, returns a {@code ClassLoader} which relates to some of the classes mentioned in {@code mt}.
*/
private static ClassLoader getApproximateCommonClassLoader(MethodType mt) {
ClassLoader loader = mt.rtype().getClassLoader();
for (Class<?> ptype : mt.ptypes()) {
ClassLoader ploader = ptype.getClassLoader();
if (isAncestorLoaderOf(loader, ploader)) {
loader = ploader; // pick more specific loader
} else {
// Either loader is a descendant of ploader or loaders are unrelated. Ignore both cases.
// When loaders are not related, just pick one and proceed. It reduces the precision of keepsAlive, but
// doesn't compromise correctness.
}
}
return loader;
}
/* Returns true when {@code loader} keeps components of {@code mt} reachable either directly or indirectly through the loader delegation chain. */
private static boolean keepsAlive(MethodType mt, ClassLoader loader) {
for (Class<?> ptype : mt.ptypes()) {
if (!keepsAlive(ptype, loader)) {
return false;
}
}
return keepsAlive(mt.rtype(), loader);
}
/* Returns true when {@code loader} keeps {@code cls} either directly or indirectly through the loader delegation chain. */
private static boolean keepsAlive(Class<?> cls, ClassLoader loader) {
ClassLoader defLoader = cls.getClassLoader();
if (isBuiltinLoader(defLoader)) {
return true; // built-in loaders are always reachable
}
return isAncestorLoaderOf(defLoader, loader);
}
private static boolean isAncestorLoaderOf(ClassLoader ancestor, ClassLoader descendant) {
// Assume built-in loaders are interchangeable and all custom loaders delegate to one of them.
if (isBuiltinLoader(ancestor)) {
return true;
}
// Climb up the descendant chain until a built-in loader is encountered.
for (ClassLoader loader = descendant; !isBuiltinLoader(loader); loader = loader.getParent()) {
if (loader == ancestor) {
return true;
}
}
return false; // no direct relation between loaders is found
}
private static boolean isBuiltinLoader(ClassLoader loader) {
return loader == null ||
loader == ClassLoaders.platformClassLoader() ||
loader == ClassLoaders.appClassLoader();
} }
/** /**

View file

@ -493,12 +493,12 @@ abstract class MethodHandleImpl {
if (newArity == collectArg+1 && if (newArity == collectArg+1 &&
type.parameterType(collectArg).isAssignableFrom(newType.parameterType(collectArg))) { type.parameterType(collectArg).isAssignableFrom(newType.parameterType(collectArg))) {
// if arity and trailing parameter are compatible, do normal thing // if arity and trailing parameter are compatible, do normal thing
return asTypeCache = asFixedArity().asType(newType); return asFixedArity().asType(newType);
} }
// check cache // check cache
MethodHandle acc = asCollectorCache; MethodHandle acc = asCollectorCache;
if (acc != null && acc.type().parameterCount() == newArity) if (acc != null && acc.type().parameterCount() == newArity)
return asTypeCache = acc.asType(newType); return acc.asType(newType);
// build and cache a collector // build and cache a collector
int arrayLength = newArity - collectArg; int arrayLength = newArity - collectArg;
MethodHandle collector; MethodHandle collector;
@ -509,7 +509,7 @@ abstract class MethodHandleImpl {
throw new WrongMethodTypeException("cannot build collector", ex); throw new WrongMethodTypeException("cannot build collector", ex);
} }
asCollectorCache = collector; asCollectorCache = collector;
return asTypeCache = collector.asType(newType); return collector.asType(newType);
} }
@Override @Override
@ -737,7 +737,7 @@ abstract class MethodHandleImpl {
} else { } else {
wrapper = newTarget; // no need for a counting wrapper anymore wrapper = newTarget; // no need for a counting wrapper anymore
} }
return (asTypeCache = wrapper); return wrapper;
} }
boolean countDown() { boolean countDown() {
@ -1213,7 +1213,7 @@ abstract class MethodHandleImpl {
public MethodHandle asTypeUncached(MethodType newType) { public MethodHandle asTypeUncached(MethodType newType) {
// This MH is an alias for target, except for the MemberName // This MH is an alias for target, except for the MemberName
// Drop the MemberName if there is any conversion. // Drop the MemberName if there is any conversion.
return asTypeCache = target.asType(newType); return target.asType(newType);
} }
} }
@ -1276,7 +1276,7 @@ abstract class MethodHandleImpl {
public MethodHandle asTypeUncached(MethodType newType) { public MethodHandle asTypeUncached(MethodType newType) {
// This MH is an alias for target, except for the intrinsic name // This MH is an alias for target, except for the intrinsic name
// Drop the name if there is any conversion. // Drop the name if there is any conversion.
return asTypeCache = target.asType(newType); return target.asType(newType);
} }
@Override @Override