ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

如何追踪Java对象的访问?

2021-08-04 21:34:06  阅读:225  来源: 互联网

标签:Java 对象 private 访问 堆栈 Throwable public 追踪


1. 前言

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

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

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

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

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

2. 前置知识

在实现追踪需求前,先熟悉一下Java基础知识,不然可能会有点懵哦~

2.1 Throwable

Throwable相信大家都很熟悉,正如Object是所有对象的父类一样,Throwable是所有异常的父类。它有两个非常重要的直接子类:Exception和Error,这里就不细说。

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

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

因此,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个元素就是虚拟机栈中的栈顶方法。

先来看属性:

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

由此可见,通过StackTraceElement就可以定位到具体哪个类的哪个方法,甚至是第多少行代码。

3. 实现追踪

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

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

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

	// 睡觉
	void sleep();

	// 打印访问堆栈
	void print();
}

public class UserImpl implements User {

	@Override
	public void eat() {
		System.out.println("吃饭...");
	}

	@Override
	public void sleep() {
		System.out.println("睡觉...");
	}

	@Override
	public void print() {
		// NOP
	}
}

核心类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() {
		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);
	}

	// 堆栈记录,继承自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对象,最后输出追踪到的堆栈记录,如下:

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();
	}
}

控制台输出如下:

吃饭...
睡觉...
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)

如控制台所示,User对象在TraceDemo类的第10行被创建,然后相继在其他多个地方被访问,整个调用轨迹都完整的输出了,一旦User对象出现资源泄漏的问题,可以很快定位到。

4. 总结

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

标签:Java,对象,private,访问,堆栈,Throwable,public,追踪
来源: https://blog.csdn.net/qq_32099833/article/details/119393562

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有