一、背景
今天技術群裡@段段同學提了一個很有意思的問題, IDEA的調試時, threads頁籤裡,方法後面的 數字是啥意思??
有些同學說是代碼行數。但是我們發現有些代碼并不是代碼行數,而且還有 -1, 這是什麼鬼??
我們從這個很不起眼的問題,來講述如何分析問題,如何學習。
二、研究
2.1 猜測
猜測要有上下文,首先這是調試界面,顯然是給你提供調試的一些參考。
而數字的前面是一個 冒号,是以 這個數字應該代表 這個函數或者和這個函數有關系,最直接的了解就是源碼或者位元組碼的函數行号。
但是 -1 解釋不通啊?
2.2 查閱資料
此時根據我們的風格,肯定要去查 JLS 和 JVMS (我認為這兩個規範是JAVA工程師人手必備的,但是我相信甚至工作一兩年的人,都沒必備上,囧)。
https://docs.oracle.com/javase/specs/index.html但是這顯然是 IDEA 提供的特性,殺雞焉用宰牛刀,先從IDEA自身下手。
2.3 IDEA 調試工具自身
當然最簡單直接的就是直接查IDEA使用文檔的調試器部分,應該可以找到答案。
https://www.jetbrains.com/help/idea/debug-tool-window-threads.html https://www.jetbrains.com/help/idea/customize-threads-view.html我們假裝沒看見,自己分析:
一般某個功能想修改或者進行一些額外的操作,就可以右鍵調出菜單,是以我們嘗試一下。
發現 有 Drop Frame (很重要,很好用,但是不在本文讨論範圍之内), Export Threads , Add Stepping Filter.., Customize Threads View.. 四個選項。
眼前一亮,“Customize Threads View” 即 “自定義 Threads 視圖”,會不會有啥線索呢?
顯然 這個 “Show line number” 最可疑,因為視圖中就這個選項是和數字相關。
是以我們可以去掉這個選項後觀察 threads 的顯示效果,發現的确之前的數字消失。
是以可以斷定,這個數字就是 函數的 line number (行号)。
另外我們恢複回去,輕按兩下對應的函數觀察行号和源碼的對應關系。
我們可以看到,在第三方 Jar 包 或本地代碼的行數上,該 行号對應的就是源碼的行号。
但是對于 JDK 的源碼,此 行号和 源碼的行号不對應,輕按兩下下圖中 62 對應的函數,跳轉到了 源碼中 27行,這是咋回事呢?
是以我們設想,會不會是位元組碼中函數的行号呢?
是以需要 javap 反彙編看下源碼中的行号:
javap -c -l sun.reflect.NativeMethodAccessorImpl
Compiled from "NativeMethodAccessorImpl.java"
class sun.reflect.NativeMethodAccessorImpl extends sun.reflect.MethodAccessorImpl {
sun.reflect.NativeMethodAccessorImpl(java.lang.reflect.Method);
Code:
0: aload_0
1: invokespecial #1 // Method sun/reflect/MethodAccessorImpl."<init>":()V
4: aload_0
5: aload_1
6: putfield #2 // Field method:Ljava/lang/reflect/Method;
9: return
LineNumberTable:
line 39: 0
line 40: 4
line 41: 9
public java.lang.Object invoke(java.lang.Object, java.lang.Object[]) throws java.lang.IllegalArgumentException, java.lang.reflect.InvocationTargetException;
Code:
0: aload_0
1: dup
2: getfield #3 // Field numInvocations:I
5: iconst_1
6: iadd
7: dup_x1
8: putfield #3 // Field numInvocations:I
11: invokestatic #4 // Method sun/reflect/ReflectionFactory.inflationThreshold:()I
14: if_icmple 94
17: aload_0
18: getfield #2 // Field method:Ljava/lang/reflect/Method;
21: invokevirtual #5 // Method java/lang/reflect/Method.getDeclaringClass:()Ljava/lang/Class;
24: invokestatic #6 // Method sun/reflect/misc/ReflectUtil.isVMAnonymousClass:(Ljava/lang/Class;)Z
27: ifne 94
30: new #7 // class sun/reflect/MethodAccessorGenerator
33: dup
34: invokespecial #8 // Method sun/reflect/MethodAccessorGenerator."<init>":()V
37: aload_0
38: getfield #2 // Field method:Ljava/lang/reflect/Method;
41: invokevirtual #5 // Method java/lang/reflect/Method.getDeclaringClass:()Ljava/lang/Class;
44: aload_0
45: getfield #2 // Field method:Ljava/lang/reflect/Method;
48: invokevirtual #9 // Method java/lang/reflect/Method.getName:()Ljava/lang/String;
51: aload_0
52: getfield #2 // Field method:Ljava/lang/reflect/Method;
55: invokevirtual #10 // Method java/lang/reflect/Method.getParameterTypes:()[Ljava/lang/Class;
58: aload_0
59: getfield #2 // Field method:Ljava/lang/reflect/Method;
62: invokevirtual #11 // Method java/lang/reflect/Method.getReturnType:()Ljava/lang/Class;
65: aload_0
66: getfield #2 // Field method:Ljava/lang/reflect/Method;
69: invokevirtual #12 // Method java/lang/reflect/Method.getExceptionTypes:()[Ljava/lang/Class;
72: aload_0
73: getfield #2 // Field method:Ljava/lang/reflect/Method;
76: invokevirtual #13 // Method java/lang/reflect/Method.getModifiers:()I
79: invokevirtual #14 // Method sun/reflect/MethodAccessorGenerator.generateMethod:(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;I)Lsun/reflect/MethodAccessor;
82: checkcast #15 // class sun/reflect/MethodAccessorImpl
85: astore_3
86: aload_0
87: getfield #16 // Field parent:Lsun/reflect/DelegatingMethodAccessorImpl;
90: aload_3
91: invokevirtual #17 // Method sun/reflect/DelegatingMethodAccessorImpl.setDelegate:(Lsun/reflect/MethodAccessorImpl;)V
94: aload_0
95: getfield #2 // Field method:Ljava/lang/reflect/Method;
98: aload_1
99: aload_2
100: invokestatic #18 // Method invoke0:(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
103: areturn
LineNumberTable:
line 49: 0
line 50: 21
line 51: 30
line 53: 41
line 54: 48
line 55: 55
line 56: 62
line 57: 69
line 58: 76
line 53: 79
line 59: 86
// 看這裡!
line 62: 94
void setParent(sun.reflect.DelegatingMethodAccessorImpl);
Code:
0: aload_0
1: aload_1
2: putfield #16 // Field parent:Lsun/reflect/DelegatingMethodAccessorImpl;
5: return
LineNumberTable:
line 66: 0
line 67: 5
}
反彙編之後一個很明顯的單詞映入眼簾:“LineNumberTable” 顯然,是 line number 的 表。
行号表中清晰地顯示, 62 行 對應上面的 code 中的 94。
而且從 94 代碼偏移 到 103 所表示的函數正是 27 行對應的源碼。
是以可以看出 JDK 中的代碼的行号對應的是反彙編後的行号而不是源碼中的行号。
那麼 -1 又代表着什麼呢?
輕按兩下 Invoke0 進入源碼,發現對應 jdk 中的 native 方法, 輕按兩下 execute 進入源碼,發現未知錯亂。
是以可以推測, -1 表示 native 函數 或者 未知的函數的位置(如 lambda表達式文法)。
此時回到 2.2 閱讀官方文檔部分
找到 JVMS 對應的 “LineNumber” 部分章節:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.12The LineNumberTable attribute is an optional variable-length attribute in the attributes table of a Code attribute (§4.7.3). It may be used by debuggers to determine which part of the code array corresponds to a given line number in the original source file.
If multiple LineNumberTable attributes are present in the attributes table of a Code attribute, then they may appear in any order.
There may be more than one LineNumberTable attribute per line of a source file in the attributes table of a Code attribute. That is, LineNumberTable attributes may together represent a given line of a source file, and need not be one-to-one with source lines.
// 省略部分
line_number_table[]
Each entry in the line_number_table array indicates that the line number in the original source file changes at a given point in the code array. Each line_number_table entry must contain the following two items:
start_pc
The value of the start_pc item must indicate the index into the code array at which the code for a new line in the original source file begins.
The value of start_pc must be less than the value of the code_length item of the Code attribute of which this LineNumberTable is an attribute.
line_number
The value of the line_number item must give the corresponding line number in the original source file.
It may be used by debuggers to determine which part of the code array corresponds to a given line number in the original source file.
這句話一語中的:可能被調試器用來關聯 源碼中的 line number 和 code array 的對應關系。
也就是說:調試器可以通過 LineNumberTable 來關聯,源碼和反彙編後的代碼對應關系。
一個 LineNumberTable 的記錄表示 源檔案中的行号 到 代碼起始位置的映射。
即 line 62 對應 反彙編後的 code 94 行。
三、思考
一個不起眼的問題可能隐藏着不少知識點,要多問幾個為什麼,收獲完全不一樣。
大膽猜測,小心驗證。很多人會把猜測當做事實,也有很多人遇到問題就直接問不思考。遇到問題先根據上下文和已有知識猜想最應該是怎樣,然後驗證。
要熟悉 IDEA, 對不熟悉的菜單要有一定的好奇心
官方的手冊可以說是最好的參考資料(包括Java 語言規範,JVM規範、Spring官方文檔等),可惜很多人其實并不重視!
要敢于走出舒适區,嘗試使用好的工具,比如javap反彙編,可以幫助你學的更多,更深入。但是很多工作幾年的人甚至都沒主動用過這個指令。調試代碼萬年隻用單步,不會“回退”,不會多線程調試,不會注意左下角的調用棧等等。隻有懂得方法多了,才有更多的機會去嘗試各種突破口,而不是教條般地成為百度俠。
排查問題的思路很重要,甚至超過答案本身。記住問題的答案隻是一個資訊,方法規律才是能夠通用的知識。很多人遇到一個問題束手無策,也有一些人可以有N種解決辦法;很多人解決一個問題要好幾個小時甚至一兩天,有些人能夠快速找到問題的突破口。主要是基礎是否紮實,邏輯是否嚴謹。
有些問題,很多人不屑,人最可怕的是不知道自己不知道,但是真得未必真得懂,真功夫往往展現在小問題上。我們要保持謙虛的态度去求知,去提升自己。