如何追蹤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對象便可,固然這也會帶來另外一個問題,因爲每次訪問都須要抓取堆棧信息,程序的性能將受到很大影響,能夠考慮分環境追蹤,以及採樣追蹤。

相關文章
相關標籤/搜索