Skip to content
Permalink
Browse files
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
  • Loading branch information
Vladimir Ivanov and Peter Levart committed Sep 8, 2021
1 parent 1855574 commit 21012f2bbe214955d05f8bc583dcdceb0949b601
Showing with 106 additions and 21 deletions.
  1. +100 −15 src/java.base/share/classes/java/lang/invoke/MethodHandle.java
  2. +6 −6 src/java.base/share/classes/java/lang/invoke/MethodHandleImpl.java
@@ -26,13 +26,15 @@
package java.lang.invoke;


import jdk.internal.loader.ClassLoaders;
import jdk.internal.vm.annotation.IntrinsicCandidate;

import java.lang.constant.ClassDesc;
import java.lang.constant.Constable;
import java.lang.constant.DirectMethodHandleDesc;
import java.lang.constant.MethodHandleDesc;
import java.lang.constant.MethodTypeDesc;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
@@ -449,12 +451,9 @@
@interface PolymorphicSignature { }

private final MethodType type;
/*private*/
final LambdaForm form;
// form is not private so that invokers can easily fetch it
/*private*/
MethodHandle asTypeCache;
// asTypeCache is not private so that invokers can easily fetch it
/*private*/ final LambdaForm form; // form is not private so that invokers can easily fetch it
private MethodHandle asTypeCache;
private SoftReference<MethodHandle> asTypeSoftCache;

private byte customizationCount;

@@ -855,34 +854,120 @@ public Object invokeWithArguments(java.util.List<?> arguments) throws Throwable
* @throws WrongMethodTypeException if the conversion cannot be made
* @see MethodHandles#explicitCastArguments
*/
public MethodHandle asType(MethodType newType) {
public final MethodHandle asType(MethodType newType) {
// Fast path alternative to a heavyweight {@code asType} call.
// Return 'this' if the conversion will be a no-op.
if (newType == type) {
return this;
}
// Return 'this.asTypeCache' if the conversion is already memoized.
MethodHandle atc = asTypeCached(newType);
if (atc != null) {
return atc;
MethodHandle at = asTypeCached(newType);
if (at != null) {
return at;
}
return asTypeUncached(newType);
return setAsTypeCache(asTypeUncached(newType));
}

private MethodHandle asTypeCached(MethodType newType) {
MethodHandle atc = asTypeCache;
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;
}

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. */
/*non-public*/
MethodHandle asTypeUncached(MethodType newType) {
if (!type.isConvertibleTo(newType))
throw new WrongMethodTypeException("cannot convert "+this+" to "+newType);
return asTypeCache = MethodHandleImpl.makePairwiseConvert(this, newType, true);
if (!type.isConvertibleTo(newType)) {
throw new WrongMethodTypeException("cannot convert " + this + " to " + newType);
}
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();
}

/**
@@ -493,12 +493,12 @@ public MethodHandle asTypeUncached(MethodType newType) {
if (newArity == collectArg+1 &&
type.parameterType(collectArg).isAssignableFrom(newType.parameterType(collectArg))) {
// if arity and trailing parameter are compatible, do normal thing
return asTypeCache = asFixedArity().asType(newType);
return asFixedArity().asType(newType);
}
// check cache
MethodHandle acc = asCollectorCache;
if (acc != null && acc.type().parameterCount() == newArity)
return asTypeCache = acc.asType(newType);
return acc.asType(newType);
// build and cache a collector
int arrayLength = newArity - collectArg;
MethodHandle collector;
@@ -509,7 +509,7 @@ public MethodHandle asTypeUncached(MethodType newType) {
throw new WrongMethodTypeException("cannot build collector", ex);
}
asCollectorCache = collector;
return asTypeCache = collector.asType(newType);
return collector.asType(newType);
}

@Override
@@ -737,7 +737,7 @@ public MethodHandle asTypeUncached(MethodType newType) {
} else {
wrapper = newTarget; // no need for a counting wrapper anymore
}
return (asTypeCache = wrapper);
return wrapper;
}

boolean countDown() {
@@ -1213,7 +1213,7 @@ protected MethodHandle getTarget() {
public MethodHandle asTypeUncached(MethodType newType) {
// This MH is an alias for target, except for the MemberName
// Drop the MemberName if there is any conversion.
return asTypeCache = target.asType(newType);
return target.asType(newType);
}
}

@@ -1276,7 +1276,7 @@ Object intrinsicData() {
public MethodHandle asTypeUncached(MethodType newType) {
// This MH is an alias for target, except for the intrinsic name
// Drop the name if there is any conversion.
return asTypeCache = target.asType(newType);
return target.asType(newType);
}

@Override

1 comment on commit 21012f2

@openjdk-notifier

This comment has been minimized.

Copy link

@openjdk-notifier openjdk-notifier bot commented on 21012f2 Sep 8, 2021

Please sign in to comment.