設(shè)置
  • 日夜間
    隨系統(tǒng)
    淺色
    深色
  • 主題色

你管這叫 class?

低并發(fā)編程 2022/10/11 17:37:41 責(zé)編:子非

本文來自微信公眾號:低并發(fā)編程 (ID:dibingfa),作者:閃客

我是一個(gè) .java 文件,名叫 FlashObject.java,叫我小渣就行。

public class FlashObject {

    private String name;
    private int age;
    
    public String getName() {
        return name;
    }

    public int add(int a, int b) {
        return a + b;
    }

}

我馬上就要被 JVM 虛擬機(jī)老大加載并運(yùn)行了,此時(shí)老虛走了過來。

老虛:小渣呀,我馬上就要把你載了,你先瘦身一下,別占太大地方。

小渣:好的,沒問題,等我十秒鐘。

public class FlashObject{private String name;private int age;public int add(int a,int b){return a+b;}

小渣:老虛,我瘦身好了,你看看。

老虛:...,你是不是有病。

小渣:怎么了,我把沒用的空格和回車啥的都去掉了,瘦身了好多呢!

老虛:行吧,看你這智商,我就給你解釋解釋。你現(xiàn)在仍然是個(gè)文本文件,讓你瘦身是讓你定一個(gè)緊湊的數(shù)據(jù)結(jié)構(gòu)來表示你這個(gè) Java 文件里的信息,然后告訴我這個(gè)數(shù)據(jù)結(jié)構(gòu)中每個(gè)字節(jié)都代表什么。

小渣:哦哦,這樣啊。

老虛:對啊,這樣一是方便我去加載,二是我這個(gè)虛擬機(jī)可不只是為你 Java 語言服務(wù)的,還有很多語言最終都可以轉(zhuǎn)換為我虛擬機(jī)識(shí)別的,你得設(shè)計(jì)一個(gè)通用的格式。

小渣:嗯嗯,這回我明白啦!

1 類信息

我的類名叫 FlashObject。

先找個(gè)地方把它存起來,放開頭吧。

這里的一個(gè)小方格是 1 個(gè)字節(jié),也就是 8 位。一個(gè)英文字母用 ASCII 碼表示為 1 個(gè)字節(jié),所以占一個(gè)方格,之后不再解釋。

嚴(yán)謹(jǐn)?shù)奈矣窒氲?,這個(gè)類應(yīng)該還有其父類。

雖然這個(gè) .java 文件中沒寫,但也有其默認(rèn)父類,Object。

當(dāng)然,我們得記錄下全類名

java/lang/Object

記在哪里呢?就緊跟在類名后面吧。

誒不對,我這個(gè)類名呀,父類名呀,都是變長的,這樣緊挨著放,誰知道分界點(diǎn)在哪。

不行不行,得分別在前面加個(gè)長度,就用兩字節(jié)表示吧。

除了父類之外,還有接口名呢!雖然我們這個(gè)類沒寫,但也得定義出來。

這個(gè)接口,和類名以及父類名稍有不同,因?yàn)榭赡苡卸鄠€(gè)。

但這不是事兒,先占用兩個(gè)字節(jié),表示接口的數(shù)量即可,之后一個(gè)一個(gè)的接口名仍然像上面那樣緊挨著排布。

嗯,完美。

2 常量池

慢慢地,我發(fā)現(xiàn)需要字符串名字的地方越來越多。

除了剛剛的類名、父類名、接口名,還有屬性名、方法名、屬性的類名、方法的入?yún)㈩愋兔⒎祷刂殿愋兔?,等等等等?/p>

一方面,要是每個(gè)都這么展開寫下去,那文件格式會(huì)很亂,很多結(jié)構(gòu)都是變長的。

另一方面,很多字符串都是重復(fù)的,比如屬性 name 的類名 String,與方法 getName 的返回值類名 String,重復(fù)寫兩遍,就浪費(fèi)了空間。

因此,我決定,之前的方案作廢,設(shè)計(jì)一個(gè)新的結(jié)構(gòu)來統(tǒng)一存儲(chǔ)這些字符串,我給他起名為常量池。

每個(gè)字符串都有一個(gè)索引與之對應(yīng),這個(gè)是可以計(jì)算出來的,不需要額外的字段。

這樣,剛剛的類、父類、接口,就都可以指向這個(gè)索引了,也因此可以將長度固定下來。

當(dāng)然,現(xiàn)在這個(gè)常量池,僅僅存放了字符串。

不難想到,還可能有整型、浮點(diǎn)型的值作為常量,甚至還有可能是個(gè)引用類型,然后這個(gè)引用類型再次指向常量池中的一個(gè)索引,有點(diǎn)像指針的指針。

那這么多類型,必然就還需要一個(gè)記錄類型信息的地方,看來我們得將之前的設(shè)計(jì)改改。

這樣,我們的常量池,就不單單可以存儲(chǔ)簡單的字符串常量了,而是可以根據(jù)不同類型,存儲(chǔ)與其相對應(yīng)的數(shù)據(jù)結(jié)構(gòu)的值。

當(dāng)然,我們常量池的整體結(jié)構(gòu)還是不變的,只不過里面是類型豐富的結(jié)構(gòu)。

同樣,我們的整個(gè)設(shè)計(jì),也沒有因?yàn)槌A砍氐男「膭?dòng),受到影響。

OK,總結(jié)一下我們目前的整體方案。

開頭存常量池,之后需要的常量就全往這里放,用一個(gè)索引指向它即可。

緊接著存放類本身的相關(guān)信息,我們存放了當(dāng)前類、父類以及接口的信息。

看來老虛要求的瘦身工作,已經(jīng)初具規(guī)模啦。

3 變量

現(xiàn)在類本身的信息,已經(jīng)找到合適的位置存放起來了,接下來我們存變量。

變量也可能有多個(gè),所以結(jié)構(gòu)依然仿照我們之前的思路,開頭存數(shù)量,后面緊跟著各個(gè)存放變量的數(shù)據(jù)結(jié)構(gòu)。

至于變量用什么數(shù)據(jù)結(jié)構(gòu)來存,是不是定長的,那就是我們接下來要設(shè)計(jì)的了。

我們把其中一個(gè)變量拿出來,看看它有什么?

private String name;

非常清晰,private 這部分是變量的標(biāo)記,String 是變量類型,name 是變量名字。

先看標(biāo)記部分

除了 private,還有 public、protected、static、final、volatile、transient 等,有的可以放在一起,比如

public static final String name;

有的不能放在一起,比如

public private String name; //錯(cuò)誤

我們用位圖的方式,每一個(gè)標(biāo)記用一個(gè)位來表示(比如 public 在第一個(gè)位,private 在第二個(gè)位,static 在第四個(gè)位,final 在第五個(gè)位...),這樣不論如何排列組合,最終的值都是不一樣的。

我們把這些標(biāo)記所對應(yīng)的值,都設(shè)計(jì)并記錄下來。

標(biāo)記

public

0x0001

private

0x0002

protected

0x0004

static

0x0008

final

0x0010

volatile

0x0040

transient

0x0080

復(fù)合型的標(biāo)記,就可以表現(xiàn)為將其相加,比如 public static,就是 0x0001 + 0x0008 = 0x0009。

而這樣的賦值方式,不同排列組合后的和沒有重復(fù)的,且也能根據(jù)值很方便地反推出標(biāo)記。

不錯(cuò)不錯(cuò),就這樣了。

哦對了,類信息本身也有 public 呀 private 這些標(biāo)記屬性,剛剛記錄類信息的時(shí)候忘了,先加上它,免得一會(huì)忘了!

再看類型部分

當(dāng)前類型為 String,屬于一個(gè)引用數(shù)據(jù)類型中的類類型。

private String name;

除此之外,還有八個(gè)基本數(shù)據(jù)類型,和引用類型中的數(shù)組類型

為了占用更少的空間,我們將其用最少的符號來表示。

符號表示

類型

B

byte

C

char

D

double

F

float

I

int

J

long

S

short

Z

boolean

LClassName ;

[

數(shù)組

這里的基本數(shù)據(jù)類型,和數(shù)組類型,都只占用一個(gè) char 來表示,就只占了 1 個(gè)字節(jié)。

如果是類,則占用了 L 和; 兩個(gè)字節(jié),再加上全類名所占的字節(jié)數(shù)。

比如這里的 String 類型,用符號表示,就是

Ljava/lang/String;

但注意,這里的符號,也都可以存放在常量池中,而我們的變量結(jié)構(gòu)中的類型描述符部分,只需要一個(gè)常量池索引即可。

ok,第二部分也搞定了。

再看名字部分

名字部分沒什么好說的,相信你直接能猜到了,直接上圖。

OK,兩字節(jié)的標(biāo)記、兩字節(jié)的類型描述符、兩字節(jié)的變量名稱,這個(gè)就是我們一個(gè)變量的數(shù)據(jù)結(jié)構(gòu)。

把它放到我們最終的總視圖里。

搞定!

4 方法

方法也可能會(huì)有很多,我目前只有兩個(gè)方法,我們拿 add 方法來分析。

public int add(int a, int b) {
    return a + b;
}

當(dāng)然更準(zhǔn)確地說,我還有個(gè)沒寫出來的構(gòu)造方法。

總之,可能會(huì)有很多。

不過有了設(shè)計(jì)變量的經(jīng)驗(yàn),方法的數(shù)據(jù)結(jié)構(gòu)很快就有了雛形。

標(biāo)記部分,和變量標(biāo)記部分的思路一樣,值也差不多,我們也給他們賦上值就好了。

標(biāo)記

public

0x0001

private

0x0002

protected

0x0004

static

0x0008

final

0x0010

volatile

0x0040

transient

0x0080

synchronized

0x0020

native

0x0100

abstract

0x0400

方法描述符,說的是方法的入?yún)⑴c返回值,比如我們的:

int add(int a, int b);

入?yún)⑴c返回值的類型符號表示,與上面變量類型的符號表示完全一樣,只不過多了一個(gè) void 類型。

符號表示

類型

B

byte

C

char

D

double

F

float

I

int

J

long

S

short

Z

boolean

LClassName ;

[

數(shù)組

V

void

由于有多個(gè)參數(shù)類型,所以要定一個(gè)整體的格式,而整個(gè)描述符的格式為:

(參數(shù) 1 類型 參數(shù) 2 類型 ...) 返回值類型

比如我們的

int add(int a, int b);

就表示為

(II)I

是不是非常精簡了?同樣,這也是個(gè)字符串,也可以存儲(chǔ)在常量池里,就不再贅述。

(至于參數(shù) a 和 b 這個(gè)名字,不需要保存起來,實(shí)際上在轉(zhuǎn)換的字節(jié)碼以及實(shí)際虛擬機(jī)中運(yùn)行時(shí),只需要知道局部變量表中的位置即可,叫什么名字都無所謂)

方法名稱,我們再熟悉不過了,放常量池!

ok,前三個(gè)說完了。最后一個(gè),就有意思了。

代碼、異常、注解等。可以看到,有相當(dāng)多的信息需要記錄。

比如我寫這樣的方法。

@RequestMing()
public String function(String a) throws Exception {
    return a;
}

那就會(huì)有代碼部分、異常、注解等需要錄入的信息。

但似乎除了代碼部分之外,其他部分都不是每個(gè)方法都有的,如果都定義出來,豈不是浪費(fèi)空間,那怎么辦呢?

我們效仿常量池的做法,把這些部分都叫“方法的屬性”,一個(gè)方法可能有多個(gè)屬性,設(shè)計(jì)結(jié)構(gòu)如下。

這樣,方法具有哪些屬性,按需添加進(jìn)來就好,如果不需要這個(gè)屬性,也不用浪費(fèi)空間,完美!

回過頭看我們的這個(gè)方法。

public int add(int a, int b) {
    return a + b;
}

剛剛方法簽名部分已經(jīng)都解決了,只剩下代碼

return a + b;

這個(gè)要怎樣存放呢?

之前聽老虛說過,JVM 識(shí)別的是一種叫字節(jié)碼的東西,所以我要把 Java 語言寫出的代碼,轉(zhuǎn)換為字節(jié)碼。

這部分很復(fù)雜,就不展開說我的過程了,經(jīng)過一番努力后,我把這一行簡簡單單的代碼轉(zhuǎn)換為了字節(jié)碼。

1B 1C 60 AC

一共占四個(gè)字節(jié)。

我把這四個(gè)字節(jié),就放在剛剛代碼類型的屬性中。

ok,大功告成。

回過頭,我們將之前的方法部分補(bǔ)充完整。

再將這個(gè)結(jié)構(gòu),添加到我們?nèi)纸Y(jié)構(gòu)中。

完美!

5、class

我把我轉(zhuǎn)換為了這樣的結(jié)構(gòu),并帶著這個(gè)最終的設(shè)計(jì)稿,去找了老虛。

老虛:嗯!還真不賴!

小渣:那當(dāng)然,我可是研究了好久呢。

老虛:不過,我再給你改改,在開頭加些東西把。

小渣:老虛,你這加的是啥呀?

老虛:一看你就沒經(jīng)驗(yàn)。

魔數(shù)一般用來識(shí)別這個(gè)文件的格式,通過文件名后綴的方式不靠譜,一般有格式的文件都會(huì)有個(gè)魔數(shù)的。

后面兩個(gè)用來標(biāo)識(shí)一下版本號,不同版本可能數(shù)據(jù)結(jié)構(gòu)和支持的功能不一樣,這個(gè)今后會(huì)有用的!

小渣:原來如此,還是你老虛見多識(shí)廣??墒悄阏f用來識(shí)別這個(gè)文件的格式,我這個(gè)文件是啥呀?

老虛:你這個(gè)破玩意,就叫它 class 文件吧!

FlashObject.class

后記

根據(jù) Java 虛擬機(jī)規(guī)范,Java Virtual Machine Specification Java SE 8 Edition,一個(gè) class 文件的標(biāo)準(zhǔn)結(jié)構(gòu),是這樣的。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

我們的設(shè)計(jì)與它幾乎相同。

只有后兩項(xiàng),我們沒有涉及到,本身也不是重點(diǎn)。

常量池中的類型,有以下幾種。

Constant Type
Value
CONSTANT_Class
7
CONSTANT_Fieldref
9
CONSTANT_Methodref
10
CONSTANT_InterfaceMethodref
11
CONSTANT_String
8
CONSTANT_Integer
3
CONSTANT_Float
4
CONSTANT_Long
5
CONSTANT_Double
6
CONSTANT_NameAndType
12
CONSTANT_Utf8
1
CONSTANT_MethodHandle
15
CONSTANT_MethodType
16
CONSTANT_InvokeDynamic
18

如果想了解 class 文件的全部細(xì)節(jié),最好的辦法就是閱讀官方文檔,也就是 Java 虛擬機(jī)規(guī)范的第四部分。

Chapter 4. The class File Format

這里的鏈接可以直接定位:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2

不要覺得官方文檔晦澀難懂,這個(gè)部分還是非常清晰明了的,大多數(shù)博客基本上對格式的講解都缺斤少兩,而且說得也不形象,還不如直接閱讀官方文檔呢。

還有一個(gè)好的方式,就是直接觀察 class 文件的二進(jìn)制結(jié)構(gòu)解析,這里推薦一個(gè)工具

classpy

用這個(gè)工具打開一個(gè) class 文件,是這個(gè)樣子。

左邊解析好的樹型結(jié)構(gòu),可以直接和右邊的 class 文件的二進(jìn)制內(nèi)容相對應(yīng),非常好用。

最后,希望大家找時(shí)間用這個(gè)工具分析一個(gè)復(fù)雜的 class 文件,會(huì)很有幫助的。祝大家學(xué)會(huì) class 文件。

完~

廣告聲明:文內(nèi)含有的對外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時(shí)間,結(jié)果僅供參考,IT之家所有文章均包含本聲明。

相關(guān)文章

關(guān)鍵詞:linuxclass

軟媒旗下網(wǎng)站: IT之家 最會(huì)買 - 返利返現(xiàn)優(yōu)惠券 iPhone之家 Win7之家 Win10之家 Win11之家

軟媒旗下軟件: 軟媒手機(jī)APP應(yīng)用 魔方 最會(huì)買 要知