JDK-8203480 : IncompatibleClassChangeError thrown at sites linked to default interface methods
  • Type: Bug
  • Component: hotspot
  • Sub-Component: compiler
  • Affected Version: 9,10,10.0.1,11
  • Priority: P2
  • Status: Closed
  • Resolution: Fixed
  • OS: generic
  • CPU: x86_64
  • Submitted: 2018-05-17
  • Updated: 2019-09-13
  • Resolved: 2018-06-07
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11
11 b18Fixed
Related Reports
Relates :  
Description
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



Comments
It's a regression introduced by JDK-8072008.
28-05-2018

I could only reproduce with a reflective invocations to the generated test method and not MethodHandle invocations. Call first with MethodHandle.invoke/invokeExact and the subsequent call to Method.invoke will fail (assuming -Xcomp).
24-05-2018

This may not be so rare if it is reflection inflation that triggers this.
24-05-2018

ILW = Incorrect behavior of JIT compiled code, rare but reproducible with regression test, disable compilation of affected method = HMM = P2
24-05-2018

I confirm with Paul that this is C2 issues, verified with Interpreter, C1 and C2 and below are the results Case 1: Interpreter mode -Xint //Pass Case 2: running with C1 -XX:+TieredCompilation -XX:TieredStopAtLevel=1 //Pass Case 3: running with only C2 XX:-TieredCompilation //Fail Above observation confirms that it is C2 issue. Hi [~psandoz],I have moved this issue to hotspot/compiler. Kindly unassign yourself for compiler team's review.
24-05-2018

This is only reproducible with the C2 compiler, it can be reproduced on the second iteration of invocation with HotSpot options "-XX:-TieredCompilation -Xcomp" and with the following method default: default Object foo(Object x) { return x; } Its very likely a HotSpot compiler bug.
23-05-2018

This is regression started from 9 ea b103 onwards 8u172 - Pass 9 ea b102 - Pass 9 ea b103 - Fail --> Regression introduced here. 9 GA - Fail 10 GA - Fail 11 ea b13 - Fail
23-05-2018

After the code modification as suggested by submitter, i am able to reproduce this issue in 11 ea b13 == invocation 6608 invocation 6609 invocation 6610 invocation 6611 invocation 6612 invocation 6613 invocation 6614 invocation 6615 invocation 6616 invocation 6617 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:569) at com.foo.bar.FailureTrigger.main(FailureTrigger.java:65) Caused by: java.lang.IncompatibleClassChangeError: Found interface com.foo.bar.FailureTrigger$InterfaceWithDefaultMethod, but class was expected at Test.test(Unknown Source) ==
23-05-2018

Additional Information from submitter == The issue is still present in JDK 11 and can be reproduced by changing the method InterfaceWithDefaultMethod#foo() as follows (one line added compared to the original submitted version). Again, the failure is random and there is a small probability (in my trials, less than 5%) of a particular single run completing successfully. default Object foo(Object x) { List<Object> stuff = new ArrayList<>(); for (int i = 0; i < 3000; i++) { stuff.add(new Object()); } // The following line brings back the failure in JDK 11 (build 11-ea+14) stuff.add(x); System.out.println(x); return stuff; } } Build info: openjdk version "11-ea" 2018-09-25 OpenJDK Runtime Environment 18.9 (build 11-ea+14) OpenJDK 64-Bit Server VM 18.9 (build 11-ea+14, mixed mode)
23-05-2018

This issue is verified on linux and mac on jdk10 and jdk10.0.1 builds it is reproducible. Where as this issue is not reproducible on jdk11. 8uxx - Pass (Including 8u172) 10 GA - Fail 10.0.1 GA - Fail 11 ea b14 - Pass Closing as cannot reproduce
21-05-2018