如何追踪Java对象的访问?

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战java


1. 前言

在Java中,咱们该如何追踪一个对象呢? ​markdown

追踪对象,有意义吗? 不少时候,确实不必去追踪一个对象。对象完成它的使命后,GC会自动帮咱们进行垃圾回收,开发者不用担忧内存泄漏的问题。可是有时候,对象追踪又颇有用,当你须要本身维护一些比较宝贵的资源时,例如:内存、链接等,使用者一旦忘记归还,资源就会发生泄漏,产生严重后果。 ​ide

了解了追踪对象的意义后,接下来要思考的,就是该如何追踪对象了。 ​函数

需求很简单,要能知道对象具体是在哪里被建立的,在哪里被访问过,这里的【哪里】须要精确到具体代码的行数。 ​post

有的同窗可能会想到,经过打日志的方式来记录,可是那太麻烦了,也难以维护,今天咱们换个思路,经过堆栈信息来追踪。 ​性能

2. 前置知识

在实现追踪需求前,先熟悉一下Java基础知识,否则可能会有点懵哦~ ​测试

2.1 Throwable

Throwable相信你们都很熟悉,正如Object是全部对象的父类同样,Throwable是全部异常的父类。它有两个很是重要的直接子类:Exception和Error,这里就不细说。 ​this

Throwable中文译为【可抛出的】,为何会有这个类呢?首先,只要是程序就可能会有Bug,只要是程序就可能会有异常。这个【异常】不论是你手动抛出的,仍是运行时JVM自动抛出的,它的目的很简单,就是告诉开发者:程序异常了,你赶忙去排查解决。 ​spa

做为一个合格的异常,应该如何快速的帮助开发者定位问题呢?最直接的就是告诉你,在代码的哪一个位置发生了什么异常,异常信息是什么等等,这也被称为【堆栈信息】。 ​线程

所以,Throwable类有以下两个重要的属性:

// 异常详细信息
private String detailMessage;

// 堆栈列表
private StackTraceElement[] stackTrace;
复制代码

其中,detailMessage是须要你手动指定的,而stackTrace堆栈则由JVM自动抓取。 ​

何时会抓取堆栈呢?固然是Throwable被建立的时候了,所以它的构造函数以下:

public Throwable() {
    // 填充堆栈信息
    fillInStackTrace();
}
复制代码

惋惜的是,你没法看到堆栈抓取的源码,由于它是被native修饰的本地代码:

private native Throwable fillInStackTrace(int dummy);
复制代码

如今,你只须要知道,当一个Throwable被建立时,默认JVM会自动抓取堆栈信息。 ​

2.2 StackTraceElement

StackTraceElement是由Throwable自动抓取的,它其实表明的是当前线程运行的方法栈里的一个个的栈帧。 ​

回顾一下JVM知识,JVM运行时数据区被划分红五大块:线程共享的堆和方法区、线程私有的程序计数器、Java虚拟机栈、本地方法栈。当JVM要执行一个方法时,它首先会将该方法打包成一个【栈帧】,而后入栈执行,方法运行结束后出栈,方法执行的过程就是一个个栈帧入栈出栈的过程。 ​

StackTraceElement就是对虚拟机栈中栈帧的描述,stackTrace的第0个元素就是虚拟机栈中的栈顶方法。 ​

先来看属性:

  1. declaringClass:关联的类名。
  2. methodName:关联的方法名。
  3. fileName:文件名。
  4. lineNumber:关联的代码行数。

因而可知,经过StackTraceElement就能够定位到具体哪一个类的哪一个方法,甚至是第多少行代码。 ​

3. 实现追踪

能够为对象定义一个touch方法,当要追踪时就调用一次。也能够为对象生成代理对象,访问任意方法都自动追踪,这里采用后者。 ​

为了方便理解,直接采用JDK动态代理。所以要追踪的对象必须实现接口,这里以User接口为例,代码以下:

public interface User {
	// 吃饭
	void eat();

	// 睡觉
	void sleep();

	// 打印访问堆栈
	void print();
}
复制代码

编写一个超简单的UserImpl类,方法实现为输出一段话,这里代码就不贴代码了。

核心类TraceDetector能够为原生对象生成一个代理对象,拦截每个方法,自动抓取调用堆栈记录,最后能够在控制台输出堆栈的调用记录。

public class TraceDetector implements InvocationHandler {
	// 原生对象
	private final Object origin;
	// 堆栈追踪记录
	private Record traceRecord = new Record();
	
	public TraceDetector(Object origin) {
		this.origin = origin;
	}

	// 生成新的堆栈
	private void newRecord() {
		this.traceRecord = new Record(traceRecord);
	}

	// 生成代理对象
	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();// 添加追踪堆栈
			return method.invoke(origin, args);
		}
	}

	// 输出堆栈信息
	private void print() {
		// 输出 record.getStackTrace() 堆栈记录
	}

	// 堆栈记录,继承自Throwable
	private static class Record extends Throwable {
		private Record next;
		private int pos;

		public Record() {
			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;
		}
	}
}
复制代码

编写测试程序,建立一个User对象,经过TraceDetector生成代理对象,在几个地方调用一下User对象,调用user.print就能够在控制台输出对象访问堆栈数据了。这样,一旦User对象出现资源泄漏的问题,能够很快定位到。 ​

4. 总结

Throwable对象在建立时,JVM会自动抓取线程堆栈信息,有了堆栈信息咱们就能够快速定位到源代码。当咱们要追踪某个对象时,每次访问对象都建立一个Throwable对象便可,固然这也会带来另外一个问题,因为每次访问都须要抓取堆栈信息,程序的性能将受到很大影响,能够考虑分环境追踪,以及采样追踪。