This is the fifth day of my participation in the August More text Challenge. For details, see: August More Text Challenge


1. Introduction

How do we track an object in Java?

Does it make sense to track subjects? A lot of times, it’s really not necessary to track an object. After the object has done its job, GC will automatically garbage collect for us, and developers won’t have to worry about memory leaks. But sometimes, object tracking is useful, when you need to maintain some more valuable resources, such as memory, connection, etc., users once forget to return, resources will leak, have serious consequences.

Now that you know what it means to track objects, the next thing to think about is how to track objects.

The requirements are simple. You need to know exactly where the object was created and where it was accessed, down to the number of lines of code.

Some of you might think of logging it, but that’s too cumbersome and hard to maintain, so today we’re going to try to trace it through stack information.

2. Pre-knowledge

Before implementing the tracking requirements, familiarize yourself with the Java basics, otherwise you may be confused

2.1 Throwable

Throwable is the parent class of all exceptions, just as Object is the parent class of all objects. It has two very important direct subclasses, which I won’t go into here: Exception and Error.

Why is there a Throwable class? First of all, as long as the program may have bugs, as long as the program may have exceptions. Whether it’s thrown manually or automatically by the JVM at runtime, the purpose of this exception is simply to tell the developer that there is an exception and you need to fix it.

As a qualified exception, how can you quickly help the developer locate the problem? The most straightforward is to tell you where in the code the exception happened, what the exception message is, etc. This is also known as the stack message.

Therefore, the Throwable class has two important properties:

// Exception details
private String detailMessage;

// Stack list
private StackTraceElement[] stackTrace;
Copy the code

The detailMessage is manually specified, while the stackTrace stack is automatically captured by the JVM.

When do we grab the stack? When Throwable is created, of course, so its constructor looks like this:

public Throwable(a) {
    // Fill the stack information
    fillInStackTrace();
}
Copy the code

Unfortunately, you can’t see the source of the stack grab because it is native code decorated with native:

private native Throwable fillInStackTrace(int dummy);
Copy the code

For now, all you need to know is that when a Throwable is created, the DEFAULT JVM automatically grabs stack information.

2.2 StackTraceElement

A StackTraceElement is automatically grabbed by Throwable and represents a stack frame in the stack of methods running on the current thread.

As a review of JVM knowledge, the JVM runtime data area is divided into five large chunks: thread-shared heap and method areas, thread-private program counters, The Java virtual Machine stack, and the native method stack. When the JVM executes a method, it first packages the method into a stack frame, then pushes it into the stack. When the method finishes running, it exits the stack. The process of executing the method is stack frame by stack frame.

A StackTraceElement is a description of a stack frame in the virtual machine stack, and the 0th element of the stackTrace is the top method in the virtual machine stack.

Let’s look at the attributes first:

private String declaringClass;
private String methodName;
private String fileName;
private int    lineNumber;
Copy the code
  1. DeclaringClass: The name of the associated class.
  2. MethodName: the associated methodName.
  3. FileName: indicates the fileName.
  4. LineNumber: Number of associated lines of code.

As you can see, StackTraceElement allows you to locate which method in which class, and even the number of lines of code.

3. Implement tracking

You can define a touch method for the object and call it once when you want to trace it. You can also generate proxy objects for objects, and access to any method is automatically traced, the latter being used here.

For ease of understanding, use the JDK dynamic proxy directly. Therefore, the object to be traced must implement the interface. Here, the User interface is taken as an example. The code is as follows:

public interface User {
	/ / to eat
	void eat(a);

	/ / sleep
	void sleep(a);

	// Print the access stack
	void print(a);
}

public class UserImpl implements User {

	@Override
	public void eat(a) {
		System.out.println("Eat...");
	}

	@Override
	public void sleep(a) {
		System.out.println("Sleep...");
	}

	@Override
	public void print(a) {
		// NOP}}Copy the code

The core TraceDetector class generates a proxy object for the native object, intercepts every method, automatically captures the call stack record, and finally outputs the call stack record on the console.

public class TraceDetector implements InvocationHandler {
	// Native objects
	private final Object origin;
	// Stack trace
	private Record traceRecord = new Record();
	
	public TraceDetector(Object origin) {
		this.origin = origin;
	}

	// Generate a new stack
	private void newRecord(a) {
		this.traceRecord = new Record(traceRecord);
	}

	// Generate proxy objects
	public static <T> T newProxy(Class<T> clazz, T origin) {
		return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new TraceDetector(origin));
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		if ("print".equals(method.getName())) {
			this.print();
			return null;
		} else {
			this.newRecord();// Add trace stack
			returnmethod.invoke(origin, args); }}// Output the stack information
	private void print(a) {
		List<Record> list = new ArrayList<>();
		Record node = traceRecord;
		while(node ! =null) {
			list.add(node);
			node = node.next;
		}
		List<StackTraceElement> elements = new ArrayList<>();
		for (Record record : list) {
			for (int i = record.pos; i >= 0; i--) {
				elements.add(record.getStackTrace()[record.getStackTrace().length - i - 1]);
			}
		}
		StringBuilder sb = new StringBuilder();
		StackTraceElement[] arr = elements.toArray(new StackTraceElement[]{});
		for (int i = 0; i < arr.length; i++) {
			StackTraceElement stackTraceElement = arr[i];
			if (stackTraceElement.getClassName().contains("Proxy")
					|| stackTraceElement.getClassName().contains("TraceDetector")) {
				continue;
			}
			for (int j = 0; j < i; j++) {
				sb.append("_");
			}
			sb.append(stackTraceElement);
			sb.append(System.lineSeparator());
		}
		System.out.println(sb);
	}

	// Stack record, inherited from Throwable
	private static class Record extends Throwable {
		private Record next;
		private int pos;

		public Record(a) {
			this.pos = getStackTrace().length - 3;
		}

		public Record(Record next) {
			int diff = Math.abs(getStackTrace().length - next.getStackTrace().length);
			this.next = next;
			this.pos = diff + 1; }}}Copy the code

Write the test program, create a User object, generate the proxy object through TraceDetector, call the User object in several places, and finally output the traced stack record, as follows:

public class TraceDemo {
	public static void main(String[] args) {
		User user = TraceDetector.newProxy(User.class, new UserImpl());
		funcA(user);
		funcB(user);
		user.print();
	}

	static void funcA(User user) {
		user.eat();
	}

	static void funcB(User user) { user.sleep(); }}Copy the code

The console output is as follows:

Have a meal... Go to bed... top.javap.trace.TraceDemo.funcB(TraceDemo.java:21)
_top.javap.trace.TraceDemo.main(TraceDemo.java:12)
____top.javap.trace.TraceDemo.funcA(TraceDemo.java:17)
_____top.javap.trace.TraceDemo.main(TraceDemo.java:11)
______top.javap.trace.TraceDemo.main(TraceDemo.java:10)
Copy the code

As shown in the console, the User object is created in line 10 of the TraceDemo class, and then accessed in several other places successively. The entire call trace is printed out, and if a resource leak occurs on the User object, it can be quickly located.

4. To summarize

When Throwable objects are created, the JVM automatically grabs the thread stack information, which allows us to quickly locate the source code. When we want to trace an object, we can create a Throwable object every time we access the object. Of course, this brings another problem. Since we need to grab the stack information every time we access the object, the performance of the application will be greatly affected.