Fix virtual method variant of invokedynamic reloading

Issue: #87
This commit is contained in:
Andy Clement
2015-06-28 19:30:52 -07:00
parent 773d25019b
commit 1eb2a1263c
13 changed files with 598 additions and 86 deletions

View File

@@ -44,15 +44,15 @@ import org.springsource.loaded.ri.JavaMethodCache;
/**
* Represents a type that has been processed such that it can be reloaded at runtime.
*
*
* @author Andy Clement
* @since 0.5.0
*/
public class ReloadableType {
// TODO when a field is shadowed or renamed and the old one never accessed again, it may be holding onto something and prevent it from GC.
// Thinking about a solution that involves a tag in the FieldAccessor object so that we can
// check whether a 'repair' is needed on a field accessor (because the type has been reloaded and
// Thinking about a solution that involves a tag in the FieldAccessor object so that we can
// check whether a 'repair' is needed on a field accessor (because the type has been reloaded and
// the map in the accessor hasnt been repaired yet)
private static Logger log = Logger.getLogger(ReloadableType.class.getName());
@@ -140,13 +140,14 @@ public class ReloadableType {
return clazz;
}
@Override
public String toString() {
return dottedtypename;
}
/**
* Construct a new ReloadableType with the specified name and the specified initial bytecode.
*
*
* @param dottedtypename the dotted name
* @param initialBytes the bytecode for the initial version
* @param id for this reloadable type, allocated by the registry
@@ -190,7 +191,7 @@ public class ReloadableType {
/**
* Gets the 'orignal' method corresponding to given name and method descriptor. This only considers methods that
* exist in the first (non-reloaded) version of the type.
*
*
* @param name method name
* @param descriptor method descriptor (e.g (Ljava/lang/String;)I)
* @return the MethodMember or an exception if not found
@@ -221,7 +222,7 @@ public class ReloadableType {
* This method will attempt to apply any pre-existing transforms to the provided bytecode, if it is thought to be
* necessary. Currently 'necessary' is determined by finding ourselves running under tcServer and Spring Insight
* being turned on.
*
*
* @param bytes the new bytes to be possibly transformed.
* @return either the original bytes or a transformed set of bytes
*/
@@ -300,7 +301,7 @@ public class ReloadableType {
/**
* Load a new version of this type, using the specified suffix to tag the newly generated artifact class names.
*
*
* @param versionsuffix the String suffix to append to classnames being created for the reloaded class
* @param newbytedata the class bytes for the new version of this class
* @return true if the reload succeeded
@@ -328,16 +329,17 @@ public class ReloadableType {
// Not allowed to change the type
reload = false;
s = new StringBuilder("Spring Loaded: Cannot reload new version of ").append(this.dottedtypename).append(
"\n");
s = new StringBuilder("Spring Loaded: Cannot reload new version of ").append(
this.dottedtypename).append(
"\n");
if (td.hasTypeAccessChanged()) {
s.append(" Reason: Type modifiers changed\n");
s.append(" Reason: Type modifiers changed from=0x" + Integer.toHexString(td.oAccess) + " to=0x"
+ Integer.toHexString(td.nAccess) + "\n");
cantReload = true;
}
if (td.hasTypeSupertypeChanged()) {
s.append(" Reason: Supertype changed from ").append(td.oSuperName).append(" to ").append(
td.nSuperName)
.append("\n");
td.nSuperName).append("\n");
cantReload = true;
}
if (td.hasTypeInterfacesChanged()) {
@@ -370,8 +372,8 @@ public class ReloadableType {
}
}
if (!justGroovyObjectMoved) {
s.append(" Reason: Interfaces changed from ").append(td.oInterfaces).append(" to ")
.append(td.nInterfaces).append("\n");
s.append(" Reason: Interfaces changed from ").append(td.oInterfaces).append(" to ").append(
td.nInterfaces).append("\n");
cantReload = true;
}
}
@@ -535,17 +537,19 @@ public class ReloadableType {
/**
* Go through proxies we know about in this registry and see if any of them are for the type we have just reloaded.
* If they are, regenerate them and reload them.
*
*
* @param versionsuffix the suffix to use when reloading the proxies (it matches what is being used to reload the
* type)
*/
private void reloadProxiesIfNecessary(String versionsuffix) {
ReloadableType proxy = typeRegistry.cglibProxies.get(this.slashedtypename);
if (proxy != null) {
Object[] strategyAndGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndClassGeneratorMap.get(getClazz());
Object[] strategyAndGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndClassGeneratorMap.get(
getClazz());
if (strategyAndGeneratorPair == null) {
if (log.isLoggable(Level.SEVERE)) {
log.severe("Unable to find regeneration methods for cglib proxies - proxies will be out of date for this type");
log.severe(
"Unable to find regeneration methods for cglib proxies - proxies will be out of date for this type");
}
return;
}
@@ -572,9 +576,10 @@ public class ReloadableType {
proxy = typeRegistry.cglibProxiesFastClass.get(this.slashedtypename);
if (proxy != null) {
Object[] strategyAndFCGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndFastClassGeneratorMap
.get(getClazz());
strategyAndFCGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndFastClassGeneratorMap.get(getClazz());
Object[] strategyAndFCGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndFastClassGeneratorMap.get(
getClazz());
strategyAndFCGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndFastClassGeneratorMap.get(
getClazz());
// System.out.println("need to reload fastclass " + proxy + " os=" + os);
if (strategyAndFCGeneratorPair != null) {
Object a = strategyAndFCGeneratorPair[0];
@@ -617,8 +622,8 @@ public class ReloadableType {
}
}
catch (Throwable t) {
new RuntimeException("Unexpected problem trying to reload proxy for interface " + this.dottedtypename, t)
.printStackTrace();
new RuntimeException("Unexpected problem trying to reload proxy for interface " + this.dottedtypename,
t).printStackTrace();
}
}
@@ -744,7 +749,7 @@ public class ReloadableType {
Class<?> class_ClassInfo = typeRegistry.getClass_ClassInfo();
Field field_globalClassSet = class_ClassInfo.getDeclaredField("globalClassSet");
field_globalClassSet.setAccessible(true);
Object/*ClassInfoSet*/instance_classInfoSet = field_globalClassSet.get(null);
Object/*ClassInfoSet*/ instance_classInfoSet = field_globalClassSet.get(null);
Method method_ClassInfoSetRemove = instance_classInfoSet.getClass().getMethod("remove", Object.class);
Object retval = method_ClassInfoSetRemove.invoke(instance_classInfoSet, this.clazz);
@@ -853,7 +858,7 @@ public class ReloadableType {
/**
* Gets the method corresponding to given name and descriptor, taking into consideration changes that have happened
* by reloading.
*
*
* @param name the member name
* @param descriptor the member descriptor (e.g. (Ljava/lang/String;)I)
* @return the MethodMember for that name and descriptor. Null if not found on a live version, or an exception if
@@ -870,7 +875,7 @@ public class ReloadableType {
/**
* Gets the method corresponding to given name and descriptor, from the original type descriptor.
*
*
* @param nameAndDescriptor the method name and descriptor (e.g. foo(Ljava/lang/String;)I)
* @return the MethodMember for the name and descriptor if it exists, otherwise null
*/
@@ -973,7 +978,7 @@ public class ReloadableType {
RewriteClassAdaptor rca = (RewriteClassAdaptor) cv;
if (rca.isEnum && rca.fieldcount > GlobalConfiguration.enumLimit) {
// that is too many fields, marking this as not reloadable
// TODO ...
// TODO ...
}
TypeRewriter.RewriteClassAdaptor a = (TypeRewriter.RewriteClassAdaptor) rca.getClassVisitor();
return ((ClassWriter) a.getClassVisitor()).toByteArray();
@@ -1028,7 +1033,7 @@ public class ReloadableType {
/**
* Check if the specified method is different to the original form from the type as loaded.
*
*
* @param methodId the ID of the method currently executing
* @return 0 if the method cannot have changed. 1 if the method has changed. 2 if the method has been deleted in a
* new version.
@@ -1116,7 +1121,7 @@ public class ReloadableType {
/**
* Intended to handle dynamic dispatch. This will determine the right type to handle the specified method and return
* a dispatcher that can handle it.
*
*
* @param instance the target instance for the invocation
* @param nameAndDescriptor an encoded method name and descriptor, e.g. foo(Ljava/langString;)V
* @return a dispatcher that can handle the method indicated
@@ -1255,7 +1260,7 @@ public class ReloadableType {
* the non-reloadable types. This method also avoids interfaces because it is looking for instance fields. This is
* slightly naughty but if we assume the code we are reloading is valid code, it should never be referring to
* interface fields.
*
*
* @param name the name of the field to locate
* @return the FieldMember or null if the field is not found
*/
@@ -1283,7 +1288,7 @@ public class ReloadableType {
* Search for a static field from this type upwards, as far as the topmost reloadable types. This is searching for a
* field, it is not checking the result. It is up to the caller to check they have not ended up with an instance
* field and throw the appropriate exception.
*
*
* @param name the name of the field to look for
* @return a FieldMember for the named field or null if not found
*/
@@ -1320,7 +1325,7 @@ public class ReloadableType {
/**
* Attempt to set the value of a field on an instance to the specified value.
*
*
* @param instance the object upon which to set the field (maybe null for static fields)
* @param fieldname the name of the field
* @param isStatic whether the field is static
@@ -1349,7 +1354,8 @@ public class ReloadableType {
}
}
private Set<WeakReference<Object>> liveInstances = Collections.synchronizedSet(new HashSet<WeakReference<Object>>());
private Set<WeakReference<Object>> liveInstances = Collections.synchronizedSet(
new HashSet<WeakReference<Object>>());
private ReferenceQueue<Object> liveInstancesRQ = new ReferenceQueue<Object>();
@@ -1361,7 +1367,7 @@ public class ReloadableType {
/**
* Attempt to set the value of a field on an instance to the specified value. Simply locate the field, which returns
* an object capable of reading/writing it, then use that to retrieve the value.
*
*
* @param instance the object upon which to set the field (maybe null for static fields)
* @param fieldname the name of the field
* @param isStatic whether the field is static or not
@@ -1607,7 +1613,7 @@ public class ReloadableType {
* Return the ReloadableType representing the superclass of this type. If the supertype is not reloadable, this
* method will return null. The ReloadableType that is returned may not be within the same type registry, if the
* supertype was loaded by a different classloader.
*
*
* @return the ReloadableType for the supertype or null if it is not reloadable
*/
public ReloadableType getSuperRtype() {

View File

@@ -26,11 +26,14 @@ import java.lang.reflect.Method;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.springsource.loaded.CurrentLiveVersion;
import org.springsource.loaded.MethodMember;
import org.springsource.loaded.ReloadableType;
import org.springsource.loaded.TypeRegistry;
/**
* This class encapsulates dependencies on Java 8 APIs (e.g. LambdaMetafactory).
*
*
* @author Andy Clement
* @since 1.2
*/
@@ -38,30 +41,30 @@ public class Java8 {
/**
* Notes:
*
*
* Useful to have an example of how this code behaves. Here is a bit of code:
*
*
* class basic.LambdaA { interface Foo { int m(); } static int run() { Foo f = null; f = () -> 77; return f.m(); } }
*
*
* Here is a bootstrap method entry in the constant pool:
*
*
* 0: #31 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:
* (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/
* lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;
* Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; Method arguments: #32
* ()I #33 invokestatic basic/LambdaA.lambda$run$0:()I #32 ()I
*
*
* At the invokedynamic site: bsmId = 0 nameAndDescriptor = m()Lbasic/LambdaA$Foo;
*
*
* When invoking the metafactory bootstrap method the first two parameters are stacked by the VM automatically,
* namely the MethodHandles$Lookup instance (caller) and the first String (invokedName). What the VM actually sees
* is this:
*
*
* metaFactory parameters: 0:MethodHandles$Lookup caller = basic.LambdaA 1:String invokedName = "m" 2:MethodType
* invokedType = "()Foo" 3:MethodType samMethodType = "()int" 4:MethodHandle implMethod = (actually a
* DirectMethodHandle where memberName is "basic.LambdaA.lambda$run$0()int/invokeStatic") 5:MethodType
* instantiatedMethodType = "()int"
*
*
* With all that information then the calls in this case are relatively straightforward: CallSite callsite =
* LambdaMetafactory.metafactory(caller, invokedName, invokedType, samMethodType, implMethod,
* instantiatedMethodType); callsite.dynamicInvoker().invokeWithArguments((Object[])null);
@@ -70,7 +73,7 @@ public class Java8 {
/**
* Programmatic emulation of INVOKEDYNAMIC so initialize the callsite via use of the bootstrap method then invoke
* the result.
*
*
* @param executorClass the executor that will contain the lambda function, null if not yet reloaded
* @param handle bootstrap method handle
* @param bsmArgs bootstrap method arguments
@@ -104,9 +107,9 @@ public class Java8 {
MethodType invokedType = MethodType.fromMethodDescriptorString(
indyNameAndDescriptor.substring(descriptorStart), callerLoader);
// Use bsmArgs to build the parameters
// Use bsmArgs to build the parameters
MethodType samMethodType = MethodType.fromMethodDescriptorString(
(String) (((Type) bsmArgs[0]).getDescriptor()), callerLoader);
(((Type) bsmArgs[0]).getDescriptor()), callerLoader);
Handle bsmArgsHandle = (Handle) bsmArgs[1];
String owner = bsmArgsHandle.getOwner();
@@ -121,7 +124,7 @@ public class Java8 {
implMethod = caller.findStatic(caller.lookupClass(), name, implMethodType);
break;
case Opcodes.H_INVOKESPECIAL:
// If there is an executor, the lambda function is actually modified from 'private instance' to 'public static' so adjust lookup. The method
// If there is an executor, the lambda function is actually modified from 'private instance' to 'public static' so adjust lookup. The method
// will be static with a new leading parameter.
if (executorClass == null) {
// TODO is final parameter here correct?
@@ -133,15 +136,56 @@ public class Java8 {
}
break;
case Opcodes.H_INVOKEVIRTUAL:
// If there is an executor, the lambda function is actually modified from 'private instance' to 'public static' so adjust lookup. The method
// will be static with a new leading parameter.
if (executorClass == null) {
// TODO when can this scenario occur? Aren't we only here if reloading has happened?
implMethod = caller.findVirtual(caller.lookupClass(), name, implMethodType);
// There is a possibility to 'shortcut' here. Basically we are trying to resolve a callsite reference
// to the method that satisfies it. The easiest option is to just find the method on the originally
// loaded version of the target class and return that. A more optimal shortcut could return the
// method on the executor class if the target has been reloaded (effectively bypassing the method
// on the originally loaded version since we know that it will be acting as a pass through). But this
// opens up a can of worms related to visibility. The executor is loaded into the child classloader,
// and if the caller has not been reloaded it will not be able to 'see' the executor (since it is in
// a child classloader). So, basically keep this dumb (but reliable) for now.
TypeRegistry typeRegistry = rtype.getTypeRegistry();
ReloadableType ownerRType = typeRegistry.getReloadableType(owner);
if (null == ownerRType || !ownerRType.hasBeenReloaded()) {
// target containing the reference/lambdaMethod has not been reloaded, no need to get over
// complicated.
Class<?> ownerClazz = ownerRType.getClazz();
implMethod = caller.findVirtual(ownerClazz, name, implMethodType);
}
else {
implMethod = caller.findStatic(caller.lookupClass(), name, MethodType.fromMethodDescriptorString(
"(L" + owner + ";" + descriptor.substring(1), callerLoader));
MethodMember targetReferenceMethodMember = ownerRType.getCurrentMethod(name, descriptor);
String targetReferenceDescriptor = targetReferenceMethodMember.getDescriptor();
MethodType targetReferenceMethodType = MethodType.fromMethodDescriptorString(
targetReferenceDescriptor, callerLoader);
Class<?> targetReferenceClass = ownerRType.getClazz();
MethodMember currentMethod = ownerRType.getCurrentMethod(name, descriptor);
if (currentMethod.original == null) {
// null means this method did not exist on the original version of the target.
// Assert that the caller must have been reloaded, otherwise how would it
// have a reference to something that did not exist on the first version of the type. In that
// case we know we can return the method on the executor class because both the reloaded
// caller and reloaded target are in the same child classloader (no visibility problem).
if (!rtype.hasBeenReloaded()) {
throw new IllegalStateException(
"Assertion violated: When a method added on reload is being referenced"
+ "in target type '" + ownerRType.getName()
+ "', expected the caller to also have been reloaded: '"
+ rtype.getName() + "'");
}
CurrentLiveVersion ownerLiveVersion = ownerRType.getLiveVersion();
Class<?> ownerExecutorClass = ownerLiveVersion.getExecutorClass();
Method executorMethod = ownerLiveVersion.getExecutorMethod(currentMethod);
String methodDescriptor = Type.getType(executorMethod).getDescriptor();
MethodType type = MethodType.fromMethodDescriptorString(methodDescriptor, callerLoader);
implMethod = caller.findStatic(ownerExecutorClass, name, type);
}
else {
// This finds the reference method on the originally loaded class. It will pass through
// to the actual code on the reloaded version.
implMethod = caller.findVirtual(targetReferenceClass, name, targetReferenceMethodType);
}
}
break;
case Opcodes.H_INVOKEINTERFACE:
@@ -150,7 +194,7 @@ public class Java8 {
// TODO Should there not be a more direct way to this than classloading?
// TODO What about when this is a method added to the interface on a reload? It won't really exist, should we point
// to the executor? or something else? (maybe just directly the real method that will satisfy the interface - if it can be worked out)
Class<?> interfaceClass = callerLoader.loadClass(interfaceOwner.replace('/', '.')); // interface type, eg StreamB$Foo
Class<?> interfaceClass = callerLoader.loadClass(interfaceOwner.replace('/', '.')); // interface type, eg StreamB$Foo
implMethod = caller.findVirtual(interfaceClass, name, implMethodType);
break;
default:
@@ -158,16 +202,17 @@ public class Java8 {
}
MethodType instantiatedMethodType = MethodType.fromMethodDescriptorString(
(String) (((Type) bsmArgs[2]).getDescriptor()), callerLoader);
(((Type) bsmArgs[2]).getDescriptor()), callerLoader);
return LambdaMetafactory.metafactory(caller, invokedName, invokedType, samMethodType, implMethod,
instantiatedMethodType);
}
/**
* The metafactory we are enhancing is responsible for generating the anonymous classes that will call the lambda
* methods in our type
*
*
* @param bytes the class bytes for the InnerClassLambdaMetaFactory that is going to be modified
* @return the class bytes for the modified InnerClassLambdaMetaFactory
*/