java class 文件分析_大概优秀的java程序员都要会分析class文件吧
相信大家在學java的時候都會聽到這樣的一些結論:
enum 是一個類
泛型的實現使用了類型擦除技術
非靜態內部類持有外部類的引用
需要將自由變量聲明成final才能給匿名內部類訪問
...
初學的時候的時候可能在書上讀過,但是很容易就會忘記,等到踩坑踩多了,就會形成慢慢記住。但是很多的同學也只是記住了而已,對于實際的原理或者原因并不了解。
這篇文章的目的主要就是教會大家查看java的字節碼,然后懂得去分析這些結論背后的原理。
枚舉最后會被編譯成一個類
我們先從簡單的入手.
java的新手對于枚舉的理解可能是:存儲幾個固定值的集合,例如下面的Color枚舉,使用的時候最多也就通過ordinal()方法獲取下枚舉的序號或者從Color.values()里面使用序號拿到一個Color:
public enum Color {
RED,
GREEN,
BLUE
}
int index = Color.BLUE.ordinal();
Color color = Color.values()[index];
如果是從C/C++過來的人比如我,很容易形成這樣一種固定的思維:枚舉就是一種被命名的整型的集合。
在c/c++里面這種想法還能說的過去,但是到了java就大錯特錯了,錯過了java枚舉的一些好用的特性。
還是拿我們上面的Color枚舉,顏色我們經常使用0xFF0000這樣的16進制整型或者“#FF0000”這樣的字符串去表示。
在java中,我們可以這樣將這個Color枚舉和整型還有字符串關聯起來:
public enum Color {
RED(0xFF0000, "#FF0000"),
GREEN(0x00FF00, "#00FF00"),
BLUE(0x0000FF, "#0000FF");
private int mIntVal;
private String mStrVal;
Color(int intVal, String strVal) {
mIntVal = intVal;
mStrVal = strVal;
}
public int getIntVal() {
return mIntVal;
}
public String getStrVal() {
return mStrVal;
}
}
System.out.println(Color.RED.getIntVal());
System.out.println(Color.RED.getStrVal());
可以看到我們給Color這個枚舉,增加了兩個成員變量用來存整型和字符串的表示,然后還提供兩個get方法給外部獲取。
甚至進一步的,枚舉的一種比較常用的技巧就是在static塊中創建映射:
public enum Color {
RED(0xFF0000, "#FF0000"),
GREEN(0x00FF00, "#00FF00"),
BLUE(0x0000FF, "#0000FF");
private static final Map sMap = new HashMap<>();
static {
for (Color color : Color.values()) {
sMap.put(color.getStrVal(), color);
}
}
public static Color getFromStrVal(String strVal){
return sMap.get(strVal);
}
private int mIntVal;
private String mStrVal;
Color(int intVal, String strVal) {
mIntVal = intVal;
mStrVal = strVal;
}
public int getIntVal() {
return mIntVal;
}
public String getStrVal() {
return mStrVal;
}
}
System.out.println(Color.getFromStrVal("#FF0000").getIntVal());
System.out.println(Color.RED.getIntVal());
看起來是不是感覺和一個類的用法很像?"enum 是一個類"這樣句話是不是講的很有道理。
當然用法和類很像并不能說明什么。
接下來就到了我們這篇文章想講的第一個關鍵知識點了。
反編譯class文件
首先我們還是將Color簡化回最初的樣子,然后保存在Color.java文件中:
// Color.java
public enum Color {
RED,
GREEN,
BLUE
}
然后通過javac命令進行編譯,得到Color.class
javac Color.java
得到的class文件就是jvm可以加載運行的文件,里面都是一些java的字節碼。
java其實默認提供了一個javap命令,給我們去查看class文件里面的代碼。例如,在Color.class所在的目錄使用下面命令:
javap Color
可以看到下面的輸出:
Compiled from "Color.java"
public final class Color extends java.lang.Enum {
public static final Color RED;
public static final Color GREEN;
public static final Color BLUE;
public static Color[] values();
public static Color valueOf(java.lang.String);
static {};
}
是不是有種恍然大明白的感覺?Color在class文件里面實際上是被編譯成了一個繼承java.lang.Enum的類,而我們定義的RED、GREEN、BLUE實際上是這個類的靜態成員變量。
這么去看的話我們那些加成員變量、加方法的操作是不是就變得很常規了?
所以說"enum 是一個類"的意思其實是enum會被java編譯器編譯成一個繼承java.lang.Enum的類!
java運行時棧幀
相信大家都知道,java虛擬機里面的方法調用是以方法棧的形式去執行的.壓人棧內的元素就叫做棧幀.
一書中是這么介紹棧幀的:
棧幀(Stack Frame)是用于支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區的虛擬機棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表,操作數棧,動態連接和方法返回地址等信息。第一個方法從調用開始到執行完成,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
也就是說,java方法的調用,其實是一個個棧幀入棧出棧的過程,而棧幀內部又包含了局部變量表,操作數棧等部分:
1.png
局部變量表和操作數棧是棧幀內進行執行字節碼的重要部分.
局部變量表顧名思義,就是用來保存方法參數和方法內部定義的局部變量的一段內存區域.
而操作數棧也是一個后入先出的棧,程序運行過程中各種字節碼指令往其中壓入和彈出棧進行運算的.
java字節碼分析
我們用一個簡單的代碼做demo:
// Test.java
public class Test {
public static void main(String[] args) {
int a = 12;
int b = 21;
int c = a + b;
System.out.println(String.valueOf(c));
}
}
首先使用javac命令編譯代碼,然后使用javap命令查看字節碼:
javac Test.java
javap Test
得到下面的輸出:
Compiled from "Test.java"
public class Test {
public Test();
public static void main(java.lang.String[]);
}
可以看到這里只有方法的聲明,并沒有具體的代碼執行過程.這是因為執行過程都被編譯成一個個字節碼指令了.
我們可以用javap -c命令被這些指令也顯示出來:
javap -c Test
輸出為:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 12
2: istore_1
3: bipush 21
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokestatic #3 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return
}
我們來一步步分析main方法里面的字節碼指令:
// 將12這個常量壓入操作數棧
0: bipush 12
// 彈出操作數棧頂的元素,保存到局部變量表第1個位置中,即將12從棧頂彈出,保存成變量1,此時棧已空
2: istore_1
// 將21這個常量壓入操作數棧
3: bipush 21
// 彈出操作數棧頂的元素,保存到局部變量表第2個位置中,即將21從棧頂彈出,保存成變量2,此時棧已空
5: istore_2
// 從局部變量表獲取第1個位置的元素,壓入操作數棧中,即將12壓入棧中
6: iload_1
// 從局部變量表獲取第2個位置的元素,壓入操作數棧中,即將21壓入棧中
7: iload_2
// 彈出操作數棧頂的兩個元素,進行加法操作,得到的結果再壓入棧中,即彈出21和12相加得到33,再壓入棧中
8: iadd
// 彈出操作數棧頂的元素,保存到局部變量表第3個位置中,即將33從棧頂彈出,保存成變量3,此時棧已空
9: istore_3
// 讀取System中的靜態成員變量out壓入棧中
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 從局部變量表獲取第3個位置的元素,壓入操作數棧中,即將33壓入棧中
13: iload_3
// 彈出棧頂的33,執行String.valueOf方法,并將得到的返回值"33"壓回棧中
14: invokestatic #3 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
// 彈出棧頂的"33"和System.out變量去執行println方法
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 退出方法
20: return
上面的的流程比較復雜,可以結合下面這個動圖理解一下:
gif1.gif
如果看的比較仔細的同學可能會有疑問,為什么舉報變量表里一開始位置0就會有個String[]在那呢?
其實這個字符串數組就是傳入的參數args,jvm會把參數都壓如舉報變量表給方法去使用,如果調用的是非靜態方法,還會將該方法的調用對象也一起壓入棧中.
可能有同學一開始會對istore、iload...這些字節碼指令的作用不那么熟悉,或者有些指令不知道有什么作用。不過這個沒有關系,不需要死記硬背,遇到的時候搜索一下就是了。
類型擦除的原理
泛型是java中十分好用且常用的技術,之前也有寫過兩篇博客 《java泛型那些事》,《再談Java泛型》總結過.感興趣的同學可以去看看.
這里我們就從編譯出來的class文件里面看看泛型的實現:
public class Test {
public static void main(String[] args) {
foo(1);
}
public static T foo(T a){
return a;
}
}
讓我們使用"javap -c"命令看看它生成的class文件是怎樣的:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: invokestatic #3 // Method foo:(Ljava/lang/Object;)Ljava/lang/Object;
7: pop
8: return
public static T foo(T);
Code:
0: aload_0
1: areturn
}
可以看到雖然聲明部分還是可以看到泛型的影子:
public static T foo(T);
但是在調用的時候實際上是
Method foo:(Ljava/lang/Object;)Ljava/lang/Object;
main 方法中先用iconst_1將常量1壓入棧中,然后用Integer.valueOf方法裝箱成Integer最后調用參數和返回值都是Object的foo方法.
所以說泛型的實現原理實際上是將類型都變成了Obejct,所以才能接收所有繼承Object的類型,但是像int,char這種不是繼承Object的類型是不能傳入的.
然后由于類型最后都被擦除剩下Object了,所以jvm是不知道原來輸入的類型的,于是乎下面的這種代碼就不能編譯通過了:
public T foo(){
return new T(); // 編譯失敗,因為T的類型最后會被擦除,變成Object
}
非靜態內部類持有外部類的引用的原因
我們都知道非靜態內部類是持有外部類的引用的,所以在安卓中使用Handler的話一般會聲明成靜態內部類,然后加上弱引用去防止內存泄露.
接下來就讓我們一起看看非靜態內部類是怎么持有外部類的引用的。先寫一個簡單的例子:
public class Test {
public void foo() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(String.valueOf(Test.this));
}
};
}
}
通過javac命令編譯之后發現得到了兩個class文件:
Test$1.class Test.class
Test.class文件好理解應該就是Test這個類的定義,那Test$1.class定義的Test$1類又是從哪里來的呢?
這里還有個大家可能忽略的知識點,java里面變量名類名是可以包含$符號的,例如下面的代碼都是合法且可以通過編譯并且正常運行的
int x$y = 123;
System.out.println(x$y);
回到正題,讓我們先來用"javap -c"命令看看Test.class里面的內容:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public void foo();
Code:
0: new #2 // class Test$1
3: dup
4: aload_0
5: invokespecial #3 // Method Test$1."":(LTest;)V
8: astore_1
9: return
}
我們來解析下foo方法:
// new一個Test$1類的對象,壓入棧中
0: new #2 // class Test$1
// 復制一份棧頂的元素壓入棧中,即現在棧里面有兩個相同的Test\$1對象
3: dup
// 將局部變量表位置為0的元素壓入棧中,由于foo方法不是靜態方法,所以這個元素實際上就是Test對象,即this
4: aload_0
// 調用Test$1(Test)這個構造方法,它有一個Test類型的參數,我們傳入的就是棧頂的Test對象,同時我們會將棧頂第二個元素Test$1對象也傳進去(也就是說用這個Test$1對象去執行構造方法)。于是我們就彈出了棧頂的一個Test對象和一個Test$1對象
5: invokespecial #3 // Method Test$1."":(LTest;)V
// 將棧剩下的最后一個Test$1保存到局部變量表的位置1中。
8: astore_1
// 退出方法
9: return
根據上面的字節碼,我們可以逆向得到下面的代碼:
public class Test {
public void foo() {
Runnable r = new Test$1(this);
}
}
接著我們再來看看Test$1.class:
Compiled from "Test.java"
class Test$1 implements java.lang.Runnable {
final Test this$0;
Test$1(Test);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LTest;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."":()V
9: return
public void run();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #1 // Field this$0:LTest;
7: invokestatic #4 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: return
}
這里定義了一個實現Runnable接口的Test$1類,它有一個參數為Test的構造方法和一個run方法。然后還有一個Test類型的成員變量this$0。繼續解析這個兩個方法的字節碼:
Test$1(Test);
Code:
// 將局部變量表中位置為0的元素壓入棧中,由于這個方法不是靜態的,所以這個元素就是Test$1的this對象
0: aload_0
// 將局部變量表位置為1的元素壓入棧中,這個元素就是我們傳入的參數Test對象
1: aload_1
// 這里彈出棧頂的兩個元素,第一個Test對象,賦值給第二元素Test$1對象的this$0成員變量。也就是把我們傳進來的Test對象保存給成員變量 this$0
2: putfield #1 // Field this$0:LTest;
// 將局部變量表中位置為0的元素壓入棧中,還是Test$1的this對象
5: aload_0
// 使用棧頂Test$1的this對象去初始化
6: invokespecial #2 // Method java/lang/Object."":()V
// 退出方法
9: return
public void run();
Code:
//拿到System的靜態成員變量out壓入棧中
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
// 將局部變量表中位置為0的元素壓入棧中,由于這個方法不是靜態的,所以這個元素就是Test$1的this對象
3: aload_0
// 彈出棧頂Test$1的this對象,獲取它的this$0成員變量,壓入棧中
4: getfield #1 // Field this$0:LTest;
// 彈出棧頂的this$0對象執行String.valueOf方法,得到的String對象壓入棧中
7: invokestatic #4 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
// 彈出棧頂的String對象和System.out對象去執行println方法,即調用System.out.println打印這個String對象
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 退出方法
13: return
來來來,我們繼續腦補它的源代碼:
public class Test$1 implements java.lang.Runnable {
final Test this$0;
public Test$1(Test test) {
this$0 = test;
}
@Override
public void run() {
System.out.println(String.valueOf(this$0));
}
}
所以我們通過字節碼,發現下面這個代碼:
public class Test {
public void foo() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(String.valueOf(Test.this));
}
};
}
}
編譯之后最終會生成兩個類:
public class Test {
public void foo() {
Runnable r = new Test$1(this);
}
}
public class Test$1 implements java.lang.Runnable {
final Test this$0;
public Test$1(Test test) {
this$0 = test;
}
@Override
public void run() {
System.out.println(String.valueOf(this$0));
}
}
這就是非靜態內部類持有外部類的引用的原因啦。
到這里這篇文章想講的東西就已經都講完了,還剩下一個問題就當做作業讓同學們自己嘗試這去分析吧:
需要將自由變量聲明成final才能給匿名內部類訪問
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的java class 文件分析_大概优秀的java程序员都要会分析class文件吧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【待更】机器学习
- 下一篇: 【Matlab】怎么修改Excel单元格