I'm trying to create some instrumentation tool. I want to track each object allocation. The simplest idea that came to my mind was to retransform Object constructor as each object calls it (I know that arrays are initialized differently).
I tried use java agent mechanism, but it caused java.lang.instrument.UnmodifiableClassException
. Obviously java agent cannot transform Object class at it is unmodifable.
Then I tried use JDI, where my debugged program looked like:
public class First {
public static int value = 1;
public static void main(String... args) throws InterruptedException {
while (true) {
print();
Thread.sleep(1000);
}
}
public static void print() {
System.out.println("Hello" + new Integer(value));
}
}
And debugger did only this:
VirtualMachine vm = new VMAcquirer().connect(8000);
List<ReferenceType> referenceTypes1 = vm.classesByName("java.lang.Object");
ReferenceType object = referenceTypes1.get(0);
if (vm.canRedefineClasses()) {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("java.lang.Object");
CtConstructor constructor = ctClass.getConstructors()[0];
constructor.insertAfter("First.value += 1;");
HashMap<ReferenceType, byte[]> redefine = new HashMap<>();
redefine.put(object, ctClass.toBytecode());
ctClass.writeFile();
vm.redefineClasses(redefine);
}
vm.resume();
After that target program exits with message:
ERROR: JDWP Transport dt_socket failed to initialize, OUT_OF_MEMORY(110)
Exception: java.lang.StackOverflowError thrown from the UncaughtExceptionHandler in thread "main"
Do I do something wrong here? Is it possible to transform Object class that way? I know about JVMTI but I wanted avoid C code, so is there any other way that does not require native code?
DISCLAIMER I'm aware of some similar questions already asked here (e.g. Hacking into java.lang.Object: calling custom external class crashes JVM or JDI, Java Byte code instrumentation and Java agents (JWDP, JVMTI)), but they doesn't explain to me everything.
---EDIT---
Transformed Object
class looks like this:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package java.lang;
import jdk.internal.HotSpotIntrinsicCandidate;
public class Object {
private static native void registerNatives();
@HotSpotIntrinsicCandidate
public Object() {
Object var2 = null;
++First.value;
}
@HotSpotIntrinsicCandidate
public final native Class<?> getClass();
@HotSpotIntrinsicCandidate
public native int hashCode();
public boolean equals(Object obj) {
return this == obj;
}
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return this.getClass().getName() + "@" +
Integer.toHexString(this.hashCode());
}
@HotSpotIntrinsicCandidate
public final native void notify();
@HotSpotIntrinsicCandidate
public final native void notifyAll();
public final void wait() throws InterruptedException {
this.wait(0L);
}
public final native void wait(long var1) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0L) {
throw new IllegalArgumentException("timeout value is negative");
} else if (nanos >= 0 && nanos <= 999999) {
if (nanos > 0) {
++timeout;
}
this.wait(timeout);
} else {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
}
/** @deprecated */
@Deprecated(
since = "9"
)
protected void finalize() throws Throwable {
}
static {
registerNatives();
}
}
I did also more tests and if I put something like int i = 1; int j = 2 + i;
it works.
I also tried modify Integer
constructor - this caused another exception:
Exception in thread "main" java.lang.NoClassDefFoundError: First
at java.base/java.lang.Integer.<init>(Integer.java:1075)
at First.print(First.java:13)
at First.main(First.java:7)
Class was successfully transformed, but at runtime there was a problem with linking to the class. I don't know if it is linked somehow. Maybe something similar happens with Object
when some internal stuff tries to create new instance.
I was curious about Object var2 = null;
line. Javaassist always puts ACONST_NULL
bytecode, but it is not the cause of the problem.
---EDIT2---
I tried to transform another Object
method. Transformation went succcessfully, but again error occured at runtime:
Exception in thread "main" java.lang.NoClassDefFoundError: First
at java.base/java.lang.Object.toString(Object.java:246)
at First.print(First.java:15)
at First.main(First.java:7)
For me it looks like the real problem is with NoClassDefFoundError
. My assumption is it somehow related with classloader system in java (?). Could I somehow avoid such error? I don't know much about classloaders :/