FULL PRODUCT VERSION :
java version "1.6.0_03"
Java(TM) SE Runtime Environment (build 1.6.0_03-b05)
Java HotSpot(TM) Client VM (build 1.6.0_03-b05, mixed mode, sharing)
ADDITIONAL OS VERSION INFORMATION :
Ubuntu Gutsy:
Linux giordino 2.6.22-14-generic #1 SMP Sun Oct 14 23:05:12 GMT 2007 i686 GNU/Linux
A DESCRIPTION OF THE PROBLEM :
JDK 6 added capability to add/remove private (static | final) methods.
A method added in a second or subsequent retransformation throws an exception when invoked via reflection, provided that the first retransformation redefined a method in a way that it is not anymore EMCP (equivalent modulo constant pool) with its older version.
I did not try, but I guess constructors have the same problem, though probably more difficult to reproduce.
My evaluation of the problem (though I'm not a hotspot expert) led me to hotspot/src/share/vm/runtime/reflection.cpp, method Reflection::invoke_method(). This is where the exception InternalError is thrown.
This method assumes that the methods of a class have strictly consecutive "slot" numbers.
However, when redefining a class this is not the case, since the redefinition of a method losing EMCP causes the class' method_idnum to increase (the new idnum is assigned to the old version of the method).
A fix that seems to work is to change Reflection::invoke_method() implementation to use:
klass->method_with_idnum(slot)
instead of:
klass->methods()->obj_at(slot)
Thanks !
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1) Class A has method m()
2) Redefine A:
a) change m() code so that m() is not EMCP to its old version
b) add method m1(); method m1() gets idnum == 2 (this is not strictly necessary, but illustrates better what happens with idnums and why on first redefinition the problem does not show up)
3) Redefine again A:
a) add method m2()
4) m2() cannot be invoked via reflection
My evaluation is:
At step 1) method m() gets idnum == 1 (where idnum is the method's idnum)
At step 2) method m() is redefined in a non EMCP way and m1() gets idnum == 2. m() is marked as obsolete and its older version gets idnum == 3.
At step 3) m2() is added and gets idnum == 4.
At step 4) some code tries to call m2() via reflection, but there is a check that its "slot" (i.e. its idnum) is within the class' methods array (which is of length 3: m(), m1() and m2()).
The check fails and the reported exception is thrown.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
No exception is thrown by java.lang.reflect.Method.invoke()
ACTUAL -
Exception in thread "main" java.lang.InternalError: invoke
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
Download and use ASM 3.0 from http://asm.objectweb.org.
Here's the source code to reproduce the problem; use -XX:TraceRedefineClasses=65535 for better logging.
Compiled code must be jarred; the jar must have the relevant attributes for class retransformation.
Agent is the agent class; Main is the main class.
package jdk6.bug;
import java.lang.instrument.Instrumentation;
/**
* @version $Revision$ $Date$
*/
public class Agent
{
private static Instrumentation instrumentation;
public static void premain(String agentArgs, Instrumentation instrumentation) throws Exception
{
agentmain(agentArgs, instrumentation);
}
public static void agentmain(String agentArgs, Instrumentation instrumentation) throws Exception
{
Agent.instrumentation = instrumentation;
instrumentation.addTransformer(new FooTransformer(), true);
}
public static Instrumentation getInstrumentation()
{
return instrumentation;
}
}
package jdk6.bug;
import java.lang.reflect.Method;
/**
* @version $Revision$ $Date$
*/
public class Foo
{
public void test(int counter) throws Exception
{
Method method = getClass().getDeclaredMethod("transform" + counter);
method.setAccessible(true);
method.invoke(this);
}
public void transform()
{
new Exception().printStackTrace();
}
}
package jdk6.bug;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* @version $Revision$ $Date$
*/
public class FooTransformer implements ClassFileTransformer
{
private int count = 0;
private String method = "transform";
public byte[] transform(ClassLoader loader, String internalClassName, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException
{
String className = internalClassName.replace('/', '.');
if ("jdk6.bug.Foo".equals(className)) return trasform(internalClassName, classBytes);
return null;
}
private byte[] trasform(String internalClassName, byte[] classBytes)
{
ClassReader classReader = new ClassReader(classBytes);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
classReader.accept(new FooAdapter(classWriter), ClassReader.SKIP_FRAMES);
byte[] newBytes = classWriter.toByteArray();
dump(internalClassName, newBytes);
return newBytes;
}
private void dump(String className, byte[] bytes)
{
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
File baseDir = new File(tmpDir, "jdk6-bug");
baseDir.mkdir();
try
{
File dir = baseDir;
int lastSlash = className.lastIndexOf("/");
if (lastSlash >= 0)
{
String packageName = className.substring(0, lastSlash);
className = className.substring(lastSlash + 1);
dir = new File(baseDir, packageName);
dir.mkdirs();
}
File file = new File(dir, className + (count - 1) + ".class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(bytes);
fos.close();
}
catch (IOException x)
{
x.printStackTrace();
}
}
private class FooAdapter extends ClassAdapter
{
private String className;
public FooAdapter(ClassVisitor classVisitor)
{
super(classVisitor);
}
@Override
public void visit(int version, int modifiers, String className, String signature, String superClassName, String[] interfaces)
{
super.visit(version, modifiers, className, signature, superClassName, interfaces);
this.className = className;
}
@Override
public MethodVisitor visitMethod(int modifiers, String name, String descriptor, String signature, String[] exceptions)
{
if (method.equals(name))
{
// Create a new method with the current method's code
String newMethodName = method + count;
MethodVisitor newMethod = super.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL, newMethodName, descriptor, signature, exceptions);
// Create the method chain
for (int i = 0; i < count; ++i)
{
MethodVisitor mv = super.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL, method + i, descriptor, signature, exceptions);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, method + (i + 1), descriptor);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
// Replace current method's code with a call to the first method in the chain
MethodVisitor mv = super.visitMethod(modifiers, name, descriptor, signature, exceptions);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, method + "0", descriptor);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
// Prepare for next redefinition
++count;
return newMethod;
}
return super.visitMethod(modifiers, name, descriptor, signature, exceptions);
}
}
}
package jdk6.bug;
/**
* @version $Revision$ $Date$
*/
public class Main
{
public static void main(String[] args) throws Exception
{
Foo foo = new Foo();
foo.test(0);
// First transformation
retransform();
foo.test(1);
// Second transformation
retransform();
foo.test(2);
}
private static void retransform() throws Exception
{
Agent.getInstrumentation().retransformClasses(Foo.class);
}
}
---------- END SOURCE ----------
Once this bug is fixed, the following test will fail until the
fix is available in a promoted JDK:
java/lang/instrument/RedefineMethodAddInvoke.sh