Reputation: 43
While adding code to a class, I encountered a runtime exception "Class File too large!" triggered by ClassWriter.toByteArray()
.
Is there a workaround to fix this issue? For instance, if a method is too big, we can split it to solve the problem but what about a class.
PS: using asm 5.0.1
Initialization of the ClassReader and ClassWriter
ClassReader classReader = new ClassReader(in);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
MyClassVisitor myClassVisitor = new MyClassVisitor(Opcodes.ASM5, classWriter);
classReader.accept(myClassVisitor, ClassReader.SKIP_DEBUG);
For the instrumentation, I am simply logging which instruction are being executed by pushing the instruction number on the stack and adding a call after each instruction (so +2 for every instruction)
@Override
public void visitVarInsn(int opcode, int var) {
logInstruction();
super.visitVarInsn(opcode, var);
count++; // instruction number
}
private void logInstruction(){
super.visitLdcInsn(count);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "Logger", "logInstruction",
Type.getMethodDescriptor(Type.VOID_TYPE, Type.INT_TYPE), false);
}
Upvotes: 0
Views: 1425
Reputation: 298459
The RuntimeException("Class file too large!")
is not thrown when the class file size is too large (and it would be really strange if your class file exceeds 2GiB anyway), but rather when the number of items in the constant pool exceeds 65534.
Hitting this class file limitation still is rather unusual. So I recommend to recheck whether you are using the optimized instrumentation. When you pass the ClassReader
to the ClassWriter
’s constructor, it will copy the entire constant pool of the old class, which is suitable if you are making only minor changes.
However, if you are performing major changes, e.g. renaming members or types, you may end up with a lot of unused old entries in the constant pool while adding a lot of new entries. Not passing the reader to the writer’s constructor will sacrifice performance, but create a fresh constant pool only containing needed entries. If that alone is not sufficient, you may pass SKIP_DEBUG
to the ClassReader
’s constructor to remove debug information, reducing the number of constant pool items further.
If that still doesn’t help, you have to redesign whatever you’re injecting. For such code, the same rules apply as for ordinary handwritten code. Split code into methods of a reasonable size, reuse common code, organize the methods in classes. After all, even the code injection itself becomes simpler and more efficient if you just inject invocations of already existing methods.
The problem is your super.visitLdcInsn(count);
instruction. This will create a new constant pool entry for every distinct integer value. Since instruction numbers can be any integer number between 0
and 65535
, this can easily exceed the number of possible constant pool entries.
For values in that range, you don’t need to use a constant pool entries. There are dedicated bytecode instructions for small integer values. But generally, it pays off to create a utility method create the optimal instruction for every int
value:
public final void push(final int value) {
if(value >= -1 && value <= 5) {
super.visitInsn(Opcodes.ICONST_0 + value);
} else if(value == (byte)value) {
super.visitIntInsn(Opcodes.BIPUSH, value);
} else if(value == (short)value) {
super.visitIntInsn(Opcodes.SIPUSH, value);
} else {
super.visitLdcInsn(value);
}
}
Then, generally use push(intNumber)
instead of visitLdcInsn(intNumber)
.
Upvotes: 2