主頁 > 後端開發 > 【JVM故事】一個Java位元組碼檔案的誕生記

【JVM故事】一個Java位元組碼檔案的誕生記

2020-10-15 16:18:17 後端開發

萬字長文,完全虛構,

 

 


組里來了個實習生,李大胖面完之后,覺得水平一般,但還是留了下來,為什么呢?各自猜去吧,

李大胖也在心里開導自己,學生嘛,不能要求太高,只要肯上進,慢慢來,就稱呼為小白吧,

小白每天來的很早,走的很晚,都在用功學習,時不時也向別人請教,只是好像天資差了點,

都快一周了,才能寫些“簡單”的代碼,一個注解,一個介面,一個類,都來看看吧:

public @interface Health {

    String name() default "";
}


public interface Fruit {

    String getName();

    void setName(String name);

    int getColor();

    void setColor(int color);
}


@Health(name = "健康水果")
public class Apple implements Fruit {

    private String name;
    private int color;
    private double weight = 0.5;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getColor() {
        return color;
    }

    @Override
    public void setColor(int color) {
        this.color = color;
    }

    public double weight() {
        return weight;
    }

    public void weight(double weight) {
        this.weight = weight;
    }
}


與周圍人比起來,小白進步很慢,也許是自己不夠聰明,也許是自己不適合干這個,小白好像有點動搖了,

這幾天,小白明顯沒有一開始那么上進了,似乎有點想放棄,這不,趴在桌子上竟然睡著了,

 

(二)

 


在夢中,小白來到一個奇怪又略顯陰森的地方,眼前有一個破舊的小房子,從殘缺不全的門縫里折射出幾束光線,

小白有些害怕,但還是鎮定了下,深呼吸幾口,徑直朝著小房子走去,

小白推開門,屋里沒有人,只有一個“機器”在桌子旁大口大口“吃著”東西,背后也不時的“拉出”一些東西,

小白很好奇,就湊了上去,準備仔細打量一番,

“你要干嘛,別影響我作業”,突然冒出一句話,把小白嚇了一大跳,慌忙后退三步,媽呀,心都快蹦出來了,

“你是誰呀?”,驚慌中小白說了句話,

“我是編譯器”,哦,原來這個機器還會說話,小白這才緩了過來,

“編譯器”,小白好像聽說過,但一時又想不起,于是猜測到,

“網上評論留言里說的小編是不是就是你啊”?

“你才是呢”,編譯器白了一眼,沒好聲氣的說到,

要不是看在長得還行的份上,早就把你趕走了,編譯器心想,

“哦,我想起來了,編譯器嘛,就是編譯代碼的那個東西”,小白恍然大悟到,

“請注意你的言詞,我不是個東西,哦,不對,我是個東西,哦,好像也不對,我,我,”,編譯器自己也快暈了,

編譯器一臉的無奈,遇上這樣的人,今天我認栽了,

小白才不管呢,心想,今天我竟然見到了編譯器,我得好好請教請教他,

那編譯器會幫助她嗎?

 

 

(三)

 


小白再次走上前來,定睛一看,才看清楚,編譯器吃的是Java原始碼,拉的是class(位元組碼)檔案,

咦,為啥這個代碼這么熟悉呢,不就是我剛剛寫的那些,“停,停,快停下來了”,編譯器被小白叫停了,

“你又要干嘛啊”?編譯器到,

“嘻嘻,這個代碼是我寫的,我想看看它是怎么被編譯的”,小白到,

編譯器看了看這個代碼,這么“簡單”,她絕對是個菜鳥,哎,算了,還是讓她看看吧,

不過編譯器又到,“整個編譯程序是非常復雜的,想要搞清楚里面的門道是不可能的,今天也就只能看個熱鬧了”,

“編譯后的內容都是二進制資料,再通俗點說,就是一個長長的位元組陣列(byte[])”,編譯器繼續說,“通常把它寫入檔案,就是class檔案了”,

“但這不是必須的,也可以通過網路傳到其它地方,或者保存在記憶體中,用完之后就丟棄”,

“哇,還可以這樣”,小白有些驚訝,編譯器心想,你是山溝里出來的,沒見過世面,大驚小怪,

繼續到,“從資料結構上講,陣列就是一段連續的空間,是‘沒有結構’的,就像一個線段一樣,唯一能做的就是按索引訪問”,

小白到,“編譯后的內容一定很繁多,都放到一個陣列里面,怎么知道什么東西都在哪呢?不都亂套了嘛”,

編譯器覺得小白慢慢上道了,心里有一絲安慰,至少自己的講解不會完全白費,于是繼續到,

“所以JVM的那些大牛們早就設計好了位元組碼的格式,而且還把它們放入到了一個位元組陣列里面”,

小白很好奇到,“那是怎么實作的呢”?

“其實也沒有太高深的內容,既然陣列是按位置的,那就規定好所有內容的先后順序,一個接一個往陣列里放唄”,

“如果內容的長度是固定(即定長)的,那最簡單,直接放入即可”,

“如果內容長度是不固定(即變長)的,也很簡單,在內容前用一到兩個位元組存一下內容的長度不就OK了”,

 

 

(四)

 


“位元組碼的前4個位元組必須是一個固定的數字,它的十進制是3405691582,大部分人更熟悉的是它的十六進制,0xCAFEBABE”,

“通常稱之為魔術數字(Magic),它主要是用來區分檔案型別的”,編譯器到,

“擴展名(俗稱后綴名)不是用來區分檔案型別的嗎”?小白說到,“如.java是Java檔案,.class是位元組碼檔案”,

“擴展名確實可以區分,但大部分是給作業系統用的,或給人看到,如我們看到.mp3時知道是音頻、.mp4是知道是視頻、.txt是文本檔案”,

“作業系統可以用擴展名來關聯打開它的軟體,比如.docx就會用word來打開,而不會用文本檔案”,編譯器繼續到,

“還有一個問題就是擴展名可以很容易被修改,比如把一個.java手動改為.class,此時讓JVM來加載這個假的class檔案會怎樣呢”?

“那JVM先讀取開頭4個位元組,發現它不是剛剛提到的那個魔數,說明它不是合法的class檔案,就直接拋例外唄”,小白說到,

“很好,真是孺子可教”,編譯器說道,“不過還有一個問題,不知你是否注意到?4個位元組對應Java的int型別,int型別的最大值是2147483647”,

“但是魔數的值已經超過了int的最大值,那怎么放得下呢,難道不會溢位嗎”?

“確實啊,我怎么沒發現呢,那它到底是怎么放的呢”?小白到,

“其實說穿了不值得一提,JVM是把它當作無符號數對待的,而Java是作為有符號數對待的,無符號數的最大值基本上是有符號數最大值的兩倍”,

“接下來的4個位元組是版本號,不同版本的位元組碼格式可能會略有差異,其次在運行時會校驗,如JDK8編譯后的位元組碼是不能放到JDK7上運行的”,

“這4個位元組中的前2個是次(minor)版本,后2個是主(major)版本”,編譯器繼續到,“比如我現在用的JDK版本是1.8.0_211,那次版本就是0,主版本就是52”,

“所以前8個位元組的內容是,0xCAFEBABE,0,52,它們并不是源代碼里的內容”,

Magic [getMagic()=0xcafebabe]
MinorVersion [getVersion()=0]
MajorVersion [getVersion()=52]



 

(五)

 


當編譯器讀到原始碼中的public class的時候,然后就就去查看一個表格,如下圖:


自顧自的說著,“public對應的是ACC_PUBLIC,值為0x0001,class默認就是,然后又讀ACC_SUPER的值0x0020”,

“最后把它倆合起來(按位或操作),0x0001 | 0x0020 => 0x0021,然后把這個值存起來,這就是這個類的訪問控制標志”,

小白這次算是開了眼界了,只是還有一事不明,“這個ACC_SUPER是個什么鬼”?

編譯器解釋到,“這是歷史遺留問題,它原本表達在呼叫父類方法時會特殊處理,不過現在已經不再管它了,直接忽略”,

接著讀到了Apple,它是類名,編譯器首先要獲取類的全名,org.cnt.java.Apple,

然后對它稍微轉換一下形式,變成了,org/cnt/java/Apple,“這就是類名在位元組碼中的表示”,

編譯器發現這個Apple類沒有顯式繼承父類,表明它繼承自Object類,于是也獲取它的全名,java/lang/Object,

接著讀到了implements Fruit,說明該類實作了Fruit介面,也獲取全名,org/cnt/java/Fruit,

小白說到,“這些比較容易理解,全名中把點號(.)替換為正斜線(/)肯定也是歷史原因了,但是這些資訊如何存到陣列里呢”?

“把點號替換為正斜線確實是歷史原因”,編譯器繼續到,“這些字串雖然都是類名或介面名,但本質還是字串,類名、介面名只是賦予它的意義而已”,

“除此之外,像欄位名、方法名也都是字串,同理,欄位名、方法名也是賦予它的意義,所以字串是一種基本的資料,需要得到支持”,

“除了字串之外,還有整型數字,浮點數字,這些也是基本的資料,也需要得到支持”,

因此,設計者們就設計出了以下幾種型別,如圖:


“左邊是型別名稱,方便理解,右邊是對應的值,用于存盤”,編譯器繼續到,

“這里的Integer/Long/Float/Double和Utf8都是具體保存資料用的,表示整型數/浮點數和字串,其它的型別大都是對字串的參考,并賦予它一定的意義”,

“所以類名首先被存盤為一個字串,也就是Utf8,它的值對應的是1”,編譯器接著到,“由于字串是一個變長的,所以就先用兩個位元組存盤字串的長度,接著跟上具體的字串內容”,

所以字串的結構就是這樣,如圖:


“類名字串的存盤資料為,1、18、org/cnt/java/Apple,第一個位元組為1,表明是Utf8型別,第2、3兩個位元組存盤18,表示字串長度是18,接著存盤真正的字串,所以共用去1 + 2 + 18 => 21個位元組”,

“父類名字串存盤為,1、16、java/lang/Object,共用去19個位元組”,

“介面名字串存盤為,1、18、org/cnt/java/Fruit,共用去21個位元組”,

小白聽的不住點頭,編譯器喘口氣,繼續講解,

“字串存好后,就該賦予它們意義了,在后續的操作中肯定涉及到對這些字串的參考,所以還要給每個字串分配一個編號”,

如Apple為#2,即2號,Object為#4,Fruit為#6,

“由于這三個字串都是類名或介面名,按照設計規定應該使用Class表示,對應的值為7,然后再指定一個字串的編號即可”,

因此類或介面的表示如下圖:


“先用1個位元組指明是類(介面),然后再用2個位元組存盤一個字串的編號,整體意思很直白,就是把這個編號的字串當作類名或介面名”,

“類就表示為,7、#2,7表示是Class,#2表示類名稱那個字串的存盤編號,共用去3個位元組”,

“父類就表示,7、#4,共用去3個位元組,介面就表示為,7、#6,共用去3個位元組”,

其實這三個Class也分別給它們一個編號,方便別的地方再參考它們,

 

 

(六)

 


“其實上面這些內容都是常量,它們都位于常量池中,它們的編號就是自己在常量池中的索引”,編譯器說到,

“常量池很多人都知道,起碼至少是聽說過,但絕大多數人對它并不十分熟悉,因為很少有人見過它”,

編譯器繼續到,“今天你可算是來著了”,說著就把小白寫的類編譯后生成的常量池擺到了桌子上,

“這是什么東西啊,這么多,又很奇怪”,小白說到,這也是她第一次見,

ConstantPoolCount [getCount()=46]
ConstantPool [
#0 = null
#1 = ConstantClass [getNameIndex()=2, getTag()=7]
#2 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Apple, getTag()=1]
#3 = ConstantClass [getNameIndex()=4, getTag()=7]
#4 = ConstantUtf8 [getLength()=16, getString()=java/lang/Object, getTag()=1]
#5 = ConstantClass [getNameIndex()=6, getTag()=7]
#6 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Fruit, getTag()=1]
#7 = ConstantUtf8 [getLength()=4, getString()=name, getTag()=1]
#8 = ConstantUtf8 [getLength()=18, getString()=Ljava/lang/String;, getTag()=1]
#9 = ConstantUtf8 [getLength()=5, getString()=color, getTag()=1]
#10 = ConstantUtf8 [getLength()=1, getString()=I, getTag()=1]
#11 = ConstantUtf8 [getLength()=6, getString()=weight, getTag()=1]
#12 = ConstantUtf8 [getLength()=1, getString()=D, getTag()=1]
#13 = ConstantUtf8 [getLength()=6, getString()=<init>, getTag()=1]
#14 = ConstantUtf8 [getLength()=3, getString()=()V, getTag()=1]
#15 = ConstantUtf8 [getLength()=4, getString()=Code, getTag()=1]
#16 = ConstantMethodRef [getClassIndex()=3, getNameAndTypeIndex()=17, getTag()=10]
#17 = ConstantNameAndType [getNameIndex()=13, getDescriptorIndex()=14, getTag()=12]
#18 = ConstantDouble [getDouble()=0.5, getTag()=6]
#19 = null
#20 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=21, getTag()=9]
#21 = ConstantNameAndType [getNameIndex()=11, getDescriptorIndex()=12, getTag()=12]
#22 = ConstantUtf8 [getLength()=15, getString()=LineNumberTable, getTag()=1]
#23 = ConstantUtf8 [getLength()=18, getString()=LocalVariableTable, getTag()=1]
#24 = ConstantUtf8 [getLength()=4, getString()=this, getTag()=1]
#25 = ConstantUtf8 [getLength()=20, getString()=Lorg/cnt/java/Apple;, getTag()=1]
#26 = ConstantUtf8 [getLength()=7, getString()=getName, getTag()=1]
#27 = ConstantUtf8 [getLength()=20, getString()=()Ljava/lang/String;, getTag()=1]
#28 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=29, getTag()=9]
#29 = ConstantNameAndType [getNameIndex()=7, getDescriptorIndex()=8, getTag()=12]
#30 = ConstantUtf8 [getLength()=7, getString()=setName, getTag()=1]
#31 = ConstantUtf8 [getLength()=21, getString()=(Ljava/lang/String;)V, getTag()=1]
#32 = ConstantUtf8 [getLength()=16, getString()=MethodParameters, getTag()=1]
#33 = ConstantUtf8 [getLength()=8, getString()=getColor, getTag()=1]
#34 = ConstantUtf8 [getLength()=3, getString()=()I, getTag()=1]
#35 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=36, getTag()=9]
#36 = ConstantNameAndType [getNameIndex()=9, getDescriptorIndex()=10, getTag()=12]
#37 = ConstantUtf8 [getLength()=8, getString()=setColor, getTag()=1]
#38 = ConstantUtf8 [getLength()=4, getString()=(I)V, getTag()=1]
#39 = ConstantUtf8 [getLength()=3, getString()=()D, getTag()=1]
#40 = ConstantUtf8 [getLength()=4, getString()=(D)V, getTag()=1]
#41 = ConstantUtf8 [getLength()=10, getString()=SourceFile, getTag()=1]
#42 = ConstantUtf8 [getLength()=10, getString()=Apple.java, getTag()=1]
#43 = ConstantUtf8 [getLength()=25, getString()=RuntimeVisibleAnnotations, getTag()=1]
#44 = ConstantUtf8 [getLength()=21, getString()=Lorg/cnt/java/Health;, getTag()=1]
#45 = ConstantUtf8 [getLength()=12, getString()=健康水果, getTag()=1]
]


“在常量池前面會用2個位元組來存盤常量池的大小,需要記住的是,這個大小不一定就是池中常量的個數,但它減去1一定是最大的索引”,

“因為,常量池中為0的位置(#0)永遠不使用,還有Long和Double型別一個常量占2個連續索引(沒錯,又是歷史原因),實際只是用了第1個索引,第2個索引永遠空著(參見#18、#19)”,

編譯器繼續到,“#0是特殊的,用來表示‘沒有’的意思,其它地方如果想表達沒有的話,可以指向它,如Object是沒有父類的,所以它的父類指向#0,即沒有”,

“所以常量都是從#1開始,可以看看#1到#6的內容,就是剛剛上面講的”,編譯器說到,

“真是學到不少知識啊”,小白說到,“關于常量池能不能再多講點”?編譯器只好繼續講,

 

 

(七)

 


“常量池就是一個容器,它里面放了各種各樣的所有資訊,并且為每個資訊分配一個編號(即索引),如果想要在其它地方使用這些資訊,直接使用這個編號就行了”,

編譯器繼續到,“這個常量池在一些語言中也被稱為‘符號表’,通過編號來使用的這種方式也被稱為‘符號參考’”,

相信很多愛學習的同學對符號表和符號參考這兩個詞都很熟悉,不管之前是不是真懂,至少現在應該是真的搞懂了,因為你已經看到了,

“采用這種常量池和常量參考方式的好處其實很多,就說個最容易想到的,就是重復利用,節省空間,便于管理”,編譯器繼續說,

“比如一個類里有10個方法,每個方法里都定義一個length的區域變數,那么length這個名字就會出現在常量池里面,且只會出現一次,那10個方法都是對它的參考而已”,

“如果有一個方法的名字也叫length的話,那也是對同一個常量的參考,因為這個length常量只是個字串資料而已,本身沒有明確含義,它的含義來自于參考它的常量”,

“哦,原來如此”,小白開悟到,“‘符號表、符號參考’這些‘高大上’的叫法,不過就是根據索引去串列里獲取元素罷了”,哈哈,

編譯器看到小白這么開心,就準備拋出一個問題,“打壓”一下她,于是說到,

“常量池看上去和陣列/串列非常相似,都是容器且都是基于索引訪問的,為啥常量池只被稱為符號表,而不是符號陣列或符號串列呢”?

小白自然回答不上來,編譯器繼續說,“表的英文單詞是Table,它和陣列/串列的唯一區別就是,陣列/串列里的元素長度都是固定的,表里的元素長度是不固定的”,

“常量池中的好幾種常量的長度都是變長的,所以自然是表了”,

小白點了點頭,心里想,這編譯器就是厲害,我這輩子看來都無法達到他的高度了,

編譯器繼續說到,“位元組碼的前8個位元組存盤魔數和版本,接著的2個(9和10)位元組存盤常量池的大小,后面接著(從11開始)就是整個常量池的內容了”,

“之所以把常量池放這么靠前,是因為后面的所有內容都要依賴它、參考它”,

緊跟在常量池之后的就是這個類的基本資訊,如下:

“首先用2個位元組存盤上面已經計算好的訪問控制標志,即0x0021”,

“然后用2個位元組存盤這個類在常量池中的索引,就是#1”,

“然后用2個位元組存盤該類的父類在常量池中的索引,就是#3”,

“由于介面可以有多個,所以再用2個位元組存盤介面的個數,因為只實作了1個介面,所以就存盤數字1”,

“接著存盤所有介面在常量池中的索引,每個介面用2個位元組,因為只實現了1個介面,所以存盤的索引就是#5”,

AccessFlags [getAccessFlags()=0x21, getAccessFlagsString()=[ACC_PUBLIC, ACC_SUPER]]
ThisClass [getClassIndex()=1, getClassName()=org/cnt/java/Apple]
SuperClass [getClassIndex()=3, getClassName()=java/lang/Object]
InterfacesCount [getCount()=1]
Interfaces [getClassIndexes()=[5], getClassNames()=[org/cnt/java/Fruit]]

 

 

 

(八)

 


編譯器繼續到,“接下來該讀取欄位資訊了”,當讀到private時,就去下面這張表里找:


找到ACC_PRIVATE,把它的值0x0002保存以下,這就是該欄位的訪問控制標志,

接著讀到的是String,這是欄位的型別,然后會把這個String型別存入常量池,對應的索引是#8,

可以看到是一個Utf8,說明是字串,內容是 Ljava/lang/String; ,以大寫L開頭,已分號;結尾,中間是型別全名,這是在位元組碼中表示類(物件)型別的方式,

接著讀到的是name,這是欄位名稱,也是個字串,同樣也把它放入常量池,對應的索引是#7,

編譯器說到,“現在一個欄位的資訊已經讀取完畢,按照相同的方式把剩余的兩個欄位也讀取完畢”,

“那欄位的資訊又該怎么存盤呢”?小白問到,“不要著急嘛”,編譯器說著就拿出了欄位的存盤格式:


首先2個位元組是訪問控制標志,接著2個位元組是欄位名稱在常量池中的索引,接著2個位元組是欄位描述(即型別)在常量池中的索引,

接著2個位元組就是屬性個數,然后就是具體的屬性資訊了,例如欄位上標有注解的話,這個注解資訊就會放入屬性資訊里,

編譯器繼續說到,“屬性資訊是位元組碼中比較復雜的內容,這里就不說太多了”,接著就可以按格式整理資料了,

因為一個類的欄位可以有多個,所以先用2個位元組存盤一下欄位數目,本類有3個欄位,所以就存盤個3,

第一個欄位,0x0002、#7、#8、0,共用去8個位元組,因為自動沒有屬性內容,

第二個欄位,0x0002、#9、#10、0,共用去8個位元組,

第二個欄位,0x0002、#11、#12、0,共用去8個位元組,

編譯器接著說,“所以存盤這3個欄位資訊共用去2 + 8 + 8 + 8 => 26個位元組”,

小白說到,“我現在基本已經搞明白套路了,其實有些東西沒有想象中的那么復雜啊”,

“復雜的東西還是有的,我們現在先不考慮”,編譯器說到,“還有一個問題,不知你發現了沒有”,

欄位color的型別是int,但是在常量池中卻變為大寫字母I,同樣weight的型別是double,常量池中卻是大寫字母D,

小白說到,“我來猜測一下吧,int、double是Java中的資料型別,I、D是與之對應的在JVM中的表示形式,對吧”?

“算你聰明”,編譯器說到,“其實Java和JVM之間關于型別這塊有一個映射表”,如下:


有兩個需要注意,“第一點上面已經說過了,就是類都會映射成LClassName;這種形式,如Object映射為Ljava/lang/Object;”,

第二點是陣列,“陣列在Java中用一對中括號([])表示,在JVM中只用左中括號([)表示,也就是[]映射為[”,

“多維陣列也一樣,[][][]映射為[[[”,然后還有型別,“Java是把型別放到前面,JVM是把型別放到后面”,如double[]映射為[D,

“double[][][]映射為[[[D”,同理,“String[]映射為[Ljava/lang/String;,Object[][]映射為[[Ljava/lang/Object;”,

“我似乎又明白了一些,Java有自己的規范,位元組碼也有自己的規范,它們之間的映射關系早都已經定義好了”,小白繼續到,

“只要按照這種映射關系,就能把Java原始碼給轉換為位元組碼,是吧”?

“粗略來說,可以這么理解,其實這就是編譯了,但一定要清楚,真正的編譯是非常復雜的一個事情”,編譯器到,

小白說到,“欄位完了之后,肯定該方法了,就交給我吧,讓我也試試”,

“年輕人啊,就是生猛,你來試試吧”,編譯器說到,

FieldsCount [getCount()=3]
Fields [
#0 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=7, getName()=name, getDescriptorIndex()=8, getDescriptor()=Ljava/lang/String;, getAttributesCount()=0, getAttributes()=[]]
#1 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=9, getName()=color, getDescriptorIndex()=10, getDescriptor()=I, getAttributesCount()=0, getAttributes()=[]]
#2 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=12, getDescriptor()=D, getAttributesCount()=0, getAttributes()=[]]
]

 

 

 

(九)

 


小白說,“方法呢肯定也有自己的格式,你把它找出來我看看”,

“好好,我這就找”,編譯器苦笑到,我堂堂一個編譯器,今天竟然成了小白的助手,慚愧啊,

說著編譯器就找到了,于是放到了桌子上:


“咦,怎么和欄位的一模一樣”,小白到,那這就更簡單了,

先是訪問控制標志,接著是方法名稱索引,然后是方法描述索引,最后是和方法關聯的屬性,于是照貓畫虎,小白就開始了,

先讀到public關鍵字,這是個訪問控制修飾符,肯定也有一張表和它對應,可以找到這個關鍵字對應的數值,

還沒等小白開口,編譯器就趕緊把表找出來了:


小白繼續,ACC_PUBLIC對應的值是0x0001,就把這個值先保存起來,

然后是方法的名字,getName,是一個字串,照例把它存入常量池,并且有一個索引,就是#26,

接著該方法的描述了,小白認為方法和欄位是不同的,除了有回傳型別之外,還有引數呢,這該咋整呢?

于是就問編譯器,“方法的描述應該也有格式吧”?

“你越來越聰明了”,編譯器說到,“其實也很簡單,我來簡單說下吧”,

“在Java中如果把訪問控制符、方法名、引數名、方法體都去掉,其實就剩下‘方法簽名’了”,

例如,沒有入參沒有回傳值的,就是這個樣子,void(),

回傳值為String,入參為int,double,String的,其實就是這樣個子,String(int, double, String),

“這個方法簽名其實就是在Java中對方法的描述,在位元組碼中和它差不多,就是把回傳型別放到后面,把引數間的逗號去掉”,

因此void()映射為()V,這里要注意的是void對應的是大寫字母V,

String(int, double, String)映射為(IDLjava/lang/String;)Ljava/lang/String;

“不難,不難”,小白說到,于是又繼續開始了,

小白按照這種格式,把剛剛的那個方法描述也存入了常量池,得到的索引就是#27,

小白按這個套路把6個方法都整理好了,接下來該按格式把資料寫入位元組陣列了,

編程新說注:方法的代碼對應的是JVM的指令,這里就忽略不談了,后續可能會單獨再說,

編譯器提醒小白說,“你是不是還漏掉了一個方法啊”?

小白又看了一遍Java原始碼,仔細數了數,是6個呀,沒錯啊,

編譯器說到,你在學習時有沒有見過這樣一句話,“當類沒有定義建構式時,編譯器會為它生成一個默認的無參建構式”,

小白連忙點頭,“嗯嗯嗯,見過的”,

“這就是了”,編譯器說道,“不過需要注意的是,在位元組碼中構造方法的名字都是<init>,回傳型別都是V”,

“這也是規定的吧”,小白說到,編譯器點了點頭,

編譯器又說到,“其實還有方法的引數資訊,如引數位置,引數型別,引數名稱,引數的訪問控制標志等”,

“這些資訊都是放在方法格式里最后的屬性資訊中的,咱們也暫時不說它們了”,

編程新說注

在JDK7及以前,位元組碼中不包含方法的引數名,因為JVM執行指令時,引數是按位置傳入的,所以引數名對代碼的執行沒有用處,

由于越來越多的框架采用按方法引數名進行數值系結,Java也只好在JDK8時加入了對引數名的支持,

不過需要設定一下編譯器的–parameters引數,這樣才能把方法引數名也放入位元組碼中,

可以看看常量池中的#32是“MethodParameters”字串,說明位元組碼中已經包含引數名了,

常量池中#7、#9、#11三個字串就是引數名,同時也是欄位名,這就是復用的好處,

編程新說注方法的格式和欄位的格式完全一樣,就不再演示寫入程序了,

因此這個類共有7個方法,

MethodsCount [getCount()=7]
Methods [
#0 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=13, getName()=<init>, getDescriptorIndex()=14, getDescriptor()=()V, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=1, getCodeLength()=12, getJvmCode()=JvmCode [getCode()=12], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=3, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=8], LineNumTable [getStartPc()=4, getLineNumber()=12], LineNumTable [getStartPc()=11, getLineNumber()=8]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=12, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#1 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=26, getName()=getName, getDescriptorIndex()=27, getDescriptor()=()Ljava/lang/String;, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=16]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#2 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=30, getName()=setName, getDescriptorIndex()=31, getDescriptor()=(Ljava/lang/String;)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=21], LineNumTable [getStartPc()=5, getLineNumber()=22]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=7, getDescriptorIndex()=8, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=7, getAccessFlags()=0x0]]]]]
#3 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=33, getName()=getColor, getDescriptorIndex()=34, getDescriptor()=()I, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=26]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#4 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=37, getName()=setColor, getDescriptorIndex()=38, getDescriptor()=(I)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=31], LineNumTable [getStartPc()=5, getLineNumber()=32]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=9, getDescriptorIndex()=10, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=9, getAccessFlags()=0x0]]]]]
#5 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=39, getDescriptor()=()D, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=35]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#6 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=40, getDescriptor()=(D)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=3, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=39], LineNumTable [getStartPc()=5, getLineNumber()=40]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=11, getDescriptorIndex()=12, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=11, getAccessFlags()=0x0]]]]]
]


編程新說注方法部分的輸出內容很多,是因為包含了方法體的代碼的資訊,

 

 

(十)

 


“真是后生可畏啊”,編譯器感慨到,“小白竟然也能按照套路去在做點事情了”,

不過編譯器并不自危,因為最核心的內容是,可執行代碼如何轉換為JVM指令集中的指令,這可是“壓箱底”的干貨,可不能隨便告訴別人,長得再好看也不行,哈哈,O(∩_∩)O,

接著編譯器拿出一個完整的位元組碼檔案格式圖給小白看:


小白看完后說,“和剛剛講的一樣,只是最后也有這個屬性資訊啊”,

編譯器說,“屬性資訊是位元組碼檔案中非常復雜的內容,可以暫時不管用了”,

上面已經說了,至少注解的相關內容是放在屬性資訊里的,

那就看看你寫的這個類的屬性資訊都是什么吧:

AttributesCount [getCount()=2]
Attributes [
#0 = SourceFile [getSourcefileIndex()=42]
#1 = RuntimeVisibleAnnotations [getNumAnnotations()=1, getAnnotations()=[Annotation [getTypeIndex()=44, getNumElementValuePairs()=1, getElementValuePairs()=[ElementValuePair [getElementNameIndex()=7, getElementValue()=ElementValue [getTag()=ElementValueTag [getTagChar()=s], getUnion()=ElementValueUnion [getConstValueIndex()=45]]]]]]]
]


編譯器繼續說,共有2條屬性資訊,第一條是源代碼檔案的名字,在常量池中的#42,其實就是Apple.java了,

第二條是運行時可見的注解資訊,本類共有1個注解,注解型別是常量池中的#44,其實就是Lorg/cnt/java/Health;了,

該注解共顯式設定了1對屬性值,屬性名稱是常量池中的#7,就是name了,型別是小寫的s,表示String型別,屬性值是#45,也就是“健康水果”了,

下圖中的這些型別,都是可以用于注解屬性的型別:


最后,編譯器列印出一行資訊:

-----bytes=1085-----

小白說,“這是什么意思”?“這是編譯后產生的位元組碼的總長度,是1085個位元組”,編譯器到,

小白剛想表達對編譯器的感謝,忽然聞到一陣香味,而且是肉香,

PS:最后幾句話就不寫了,請你來補充完整吧,嘻嘻,

 

 

>>> 熱門文章集錦 <<<

 

畢業10年,我有話說

【面試】我是如何面試別人List相關知識的,深度有點長文

我是如何在畢業不久只用1年就升為開發組長的

爸爸又給Spring MVC生了個弟弟叫Spring WebFlux

【面試】我是如何在面試別人Spring事務時“套路”對方的

【面試】Spring事務面試考點吐血整理(建議珍藏)

【面試】我是如何在面試別人Redis相關知識時“軟懟”他的

【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(干貨 | 建議珍藏)

【面試】如果你這樣回答“什么是執行緒安全”,面試官都會對你刮目相看(建議珍藏)

【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這么清楚的好文章(快快珍藏)

【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)

【面試】如果把執行緒當作一個人來對待,所有問題都瞬間明白了

Java多執行緒通關———基礎知識挑戰

品Spring:帝國的基石

 

 

 

作者是作業超過10年的碼農,現在任架構師,喜歡研究技術,崇尚簡單快樂,追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂并記住,下面是公眾號的二維碼,歡迎關注!

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/172511.html

標籤:Java

上一篇:秒殺系統必須考慮的 3 個技術問題!

下一篇:JSP+SSM+Mysql實作的學生成績管理系統

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more