ADDITIONAL SYSTEM INFORMATION :
Verified to fail in JDK 9 and 10, Linux 64-bit and OS X.
Verified NOT to fail in JDK 1.8.
One of test setups (failure not specific to this setup):
Linux krazyglue 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
java 10.0.1 2018-04-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)
A DESCRIPTION OF THE PROBLEM :
An invokedynamic instruction whose call site target is a virtual method defined as a default method of an interface may randomly fail with an IncompatibleClassChangeError complaining about an interface found when a class was expected. The chance of failure is very small for each particular execution of the instruction, however the included executable test case reproduces the issue fairly reliably.
REGRESSION : Last worked in version 8u172
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Compile the class shown in the executable test case, linking against any ASM version supporting at least JDK 1.8. Run the class under JDK 10 or 9.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The code should run without failures.
ACTUAL -
IncompatibleClassChangeError is often (though not every time) thrown as described, resulting in an output similar to one below. Execution may occasionally complete successfully. The likelihood of failure for this case seems to correlate with allocation activity in the InterfaceWithDefaultMethod#foo() method.
[...]
invocation 7818
invocation 7819
invocation 7820
java.lang.reflect.InvocationTargetException
at jdk.internal.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at com.foo.bar.FailureTrigger.main(FailureTrigger.java:64)
invocation 7821
Caused by: java.lang.IncompatibleClassChangeError: Found interface com.foo.bar.FailureTrigger$InterfaceWithDefaultMethod, but class was expected
at Test.test(Unknown Source)
... 4 more
Process finished with exit code 0
---------- BEGIN SOURCE ----------
package com.foo.bar;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
import static org.objectweb.asm.Opcodes.V1_8;
/**
* This mini-application reproduces (when planets align just right) the spurious
* {@code IncompatibleClassChangeError} thrown at a call site linked to a
* virtual method defined as a default method of an interface.
*
* <p>Failing in JDK 10 and 9; not failing in 1.8.
*/
public class FailureTrigger {
interface InterfaceWithDefaultMethod {
/**
* NOTE: the failure is much more likely to trigger when the called method
* does a lot of busy work with allocations. A simple {@code return x;}
* doesn't fail.
*/
default Object foo(Object x) {
List<Object> stuff = new ArrayList<>();
for (int i = 0; i < 3000; i++) {
stuff.add(new Object());
}
System.out.println(x);
return stuff;
}
}
static class DefaultMethodInheritor implements InterfaceWithDefaultMethod {
}
public static void main(String[] args) {
byte[] bytecode = generateClass();
Loader loader = new Loader("Test", bytecode);
try {
Class<?> testClass = loader.loadClass("Test");
Method testMethod = testClass.getMethod("test", Object.class, Object.class);
System.out.println("Invoking test method...");
DefaultMethodInheritor subject = new DefaultMethodInheritor();
for (int i = 0; i < 100_000; i++) {
testMethod.invoke(null, subject, "invocation " + i);
}
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* Generate a class with a test method performing an {@code invokedynamic}
* bootstrapped by the bootstrap method below. The class is essentially this:
*
* <pre>{@code
* public final class Test {
* public static Object test(Object x, Object y) {
* return <invokedynamic>(x, y);
* }
* }
* }</pre>
*/
private static byte[] generateClass() {
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classWriter.visit(
V1_8, // can be V9 or V10 in corresponding JDKs, fails just the same
ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
"Test",
null,
"java/lang/Object",
null);
MethodVisitor methodWriter = classWriter.visitMethod(
ACC_PUBLIC | ACC_STATIC,
"test",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
null,
null);
methodWriter.visitCode();
methodWriter.visitVarInsn(ALOAD, 0);
methodWriter.visitVarInsn(ALOAD, 1);
methodWriter.visitInvokeDynamicInsn("invokeTest", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", BOOTSTRAP);
methodWriter.visitInsn(ARETURN);
methodWriter.visitMaxs(-1, -1);
methodWriter.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}
/**
* The bootstrapper of the {@code invokedynamic} instruction in the test method.
* The site is permanently linked to {@link InterfaceWithDefaultMethod#foo(Object)},
* obtained by a lookup in {@link DefaultMethodInheritor}.
*/
public static CallSite bootstrap(MethodHandles.Lookup lookup, String methodName, MethodType callSiteType) {
try {
MethodHandle handler = MethodHandles.lookup()
.findVirtual(DefaultMethodInheritor.class, "foo", MethodType.methodType(Object.class, Object.class));
return new ConstantCallSite(handler.asType(callSiteType));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
private static final Handle BOOTSTRAP = new Handle(
H_INVOKESTATIC,
"com/foo/bar/FailureTrigger",
"bootstrap",
MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class)
.toMethodDescriptorString(),
false);
private static class Loader extends ClassLoader {
private final String name;
private final byte[] bytecode;
private Class<?> loadedClass;
private Loader(String name, byte[] bytecode) {
this.name = name;
this.bytecode = bytecode;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (this.name.equals(name)) {
if (loadedClass == null) {
loadedClass = defineClass(name, bytecode, 0, bytecode.length);
}
return loadedClass;
}
return super.findClass(name);
}
}
}
---------- END SOURCE ----------
FREQUENCY : often