Search code examples
javajava-bytecode-asm

NoClassDefFoundError when invoking static method of a class by instrumenting byte code


I'm trying to invoke a static method whenever an object is being allocated but running into issues. I have reduced my code to smalled working example.

MemorySnifferAgent.java

public class MemorySnifferAgent {
    public static void premain(String agentArguments, Instrumentation instrumentation) {
        Callback.main(); //Make sure the class is loaded ?
        instrumentation.addTransformer(new MemoryAllocationTransformer());
    }
}

MemoryAllocationTransformer.java

public class MemoryAllocationTransformer implements ClassFileTransformer {
    public byte[] instrumentByteCode(byte[] bytecode) {
        ClassReader reader = new ClassReader(bytecode);
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        CustomClassReader customVisitor = new CustomClassReader(Opcodes.ASM4, writer);
        reader.accept(customVisitor, 0);
        return writer.toByteArray();
    }

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] bytes = instrumentByteCode(classfileBuffer);
        return  bytes;
    }
}

CustomClassReader.java

public class CustomClassReader extends ClassVisitor {
    public CustomClassReader(int api, ClassWriter classWriter) {
        super(api, classWriter);
    }

    public MethodVisitor visitMethod(final int access, final String name,
                                     final String desc, final String signature, final String[] exceptions) {
        MethodVisitor methodWritter = super.visitMethod(access, name, desc, signature, exceptions);
        return new CustomMethodWritter(api, name, methodWritter);
    }
}

CustomMethodWritter.java

class CustomMethodWritter extends MethodVisitor {
    String name;

    public CustomMethodWritter(int i, String name, MethodVisitor methodWriter) {
        super(i, methodWriter);
        this.name = name;
    }

    @Override
    public void visitTypeInsn(int opcode, String type) {
        super.visitTypeInsn(opcode, type);
        if (opcode == Opcodes.NEW) {
            super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/chasingnanos/Callback", "onAllocation", "()V", false);
        }
    }
}

Note:

  • I realize that tapping into all allocations require tapping all NEW* opcodes and reflection APIs
  • super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/chasingnanos/Callback", "onAllocation", "()V", false); seems to be the problem. Though I'm not able to find what the issue is.

Apologies if it's a very basic question. I'm a newbie to bytecode manipulation.

Error which I get:

java.lang.NoClassDefFoundError - klass: 'java/lang/NoClassDefFoundError'


Solution

  • I think the issue is related to the isolation provided by different ClassLoaders. Particularly agent's code and "main" code are loaded by different Classloaders and thus you can't call your Callback from the "main" code. The way to work this around is to move your Callback class to a different jar and add it to the bootstrap classpath using -Xbootclasspath/a option.

    See also similar issue at Java NoClassDefFoundError when calling own class from instrumented method

    Update (filtering)

    If instead of using bootstrap you want to filter out classes that can't access your Callback, you probably should do something like this instead of just check for null:

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // check if the class inside the loader can access Callback
        // by analyzing its hierarchy of parent class loaders
        for (ClassLoader curLoader = loader; ; curLoader = curLoader.getParent()) {
            if (curLoader == null) {
                System.out.println("Skip '" + className + "' for " + loader);
                return null;
            } else if (curLoader == Callback.class.getClassLoader())
                break;
        }
        byte[] bytes = instrumentByteCode(classfileBuffer);
        return bytes;
    }