日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

java class 文件分析_大概优秀的java程序员都要会分析class文件吧

發布時間:2025/3/15 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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文件吧的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。