Java能發展到如今,其「一次編譯,多處運行」的功能功不可沒,這裏最主要的功勞就是JVM和字節碼了,在不一樣平臺和操做系統上根據JVM規範的定製JVM能夠運行相同字節碼(.Class文件),並獲得相同的結果。之因此被稱之爲字節碼,是由於字節碼文件由十六進制值組成,而JVM以兩個十六進制值爲一組,即以字節爲單位進行讀取。在Java中通常是用javac命令編譯源代碼爲字節碼文件,將java文件編譯後生成.class文件交由Java虛擬機去執行,在android上,class文件被包裝成.dex文件交由DVM執行。html
經過學習Java字節碼指令能夠對代碼的底層運行結構有所瞭解,能更深層次瞭解代碼背後的實現原理,例如字符串的相加的實現原理就是經過StringBuilder
的append
進行相加。用過字節碼的視角看它的執行步驟,對Java代碼的也能有更深的瞭解,知其然,也要知其因此然。java
經過學習字節碼知識還能夠實現字節碼插樁功能,例如用ASM 、AspectJ
等工具對字節碼層面的代碼進行操做,實現一些Java代碼很差操做的功能。android
下面舉個簡單的例子,分析其字節碼的結構程序員
public class Main {
public static void main(String[] args) {
System.out.println("HelloWorld");
}
}
複製代碼
上圖中純數字字母就是字節碼,右邊的是具體代碼執行的字節碼指令。web
上面看似一堆亂碼,可是JVM對字節碼是有規範的,下面一點一點分析其代碼結構面試
魔數惟一的做用是肯定這個文件是否爲一個能被虛擬機接收的Class
文件。不少文件存儲標準中都使用魔數來進行身份識別,譬如gif和jpeg文件頭中都有魔數。魔數的定義能夠隨意,只要這個魔數尚未被普遍採用同時又不容易引發混淆便可。數組
這裏字節碼中的魔數爲0xCafeBabe
(咖啡寶貝),這個魔數值在Java還被稱做Oak
語言的時候就已經肯定下來了,據原開發成員所說是爲了尋找一些好玩的、容易記憶的東西,選擇0xCafeBabe
是由於它象徵着著名咖啡品牌Peet`s Coffee
中深受喜歡的Baristas
咖啡,咖啡一樣也是Java的logo標誌。bash
緊接着魔數的四個字節(00 00 00 33)存儲的是Class文件的版本號。前兩個是次版本號(Minor Version),轉化爲十進制爲0;後兩個爲主版本號(Major Version),轉化爲十進制爲52,序號52對應的主版本號爲1.8,因此編譯該文件的Java版本號爲1.8.0。高版本的JDK能向下兼容之前的版本的Class文件,但不能運行之後版本的Class文件,及時文件格式並未發生變化,虛擬機也必須拒絕執行超過其版本號的Class文件。數據結構
這部份內容前面作了一個簡要的筆記,感興趣的能夠去看看。併發
緊接着版本號以後的是常量池入口,常量池能夠理解爲Class文件之中的資源倉庫,它是Class文件結構中與其餘項目關聯最多的數據結構,也是佔用Class文件控件最大的數據項目之一,同事也是在Class文件中第一個出現的表類型數據項目。
常量池的前兩個字節(00 22)表明的是常量池容量計數器,與Java中語言習慣不同的是,這個容量計數是從1開始的,這裏的22轉換成十進制後爲34,去除一個下標計數即表示常量池中有33個常量,這一點從字節碼中的Constant pool
也能夠看到,最後一個是#33 = Utf8 (Ljava/lang/String;)V
容量計數器後存儲的是常量池的數據。 常量池中存儲兩類常量:字面量與符號引用。字面量爲代碼中聲明爲Final的常量值(例如字符串),符號引用如類和接口的全侷限定名、字段的名稱和描述符、方法的名稱和描述符,當虛擬機運行時,須要從常量池得到對應的符號引用,再在類建立時或者運行時解析、翻譯到內存地址中。以下圖。
常量池的每一項常量都是一個表,在JDK71.7以前共有11中結構不一樣的表結構數據,在JDK1.7以後爲了更好底支持動態語言調用,又額外增長了三種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info
),總計14中,表結構以下圖
上圖中tag
是標誌位,用於區分常量類型,length
表示這個UTF-8編碼的字符串長度是多少節,它後面緊更着的長度爲length
字節的連續數據是一個使用UTF-8縮略編碼表示的字符串。上圖的u1,u2,u4,u8表示比特數量,分別爲1,2,4,8個byte。
UTF-8縮略編碼與普通UTF-8編碼的區別是:從\u0001
到\u007f
之間的字符(至關於1-127的ASCII碼)的縮略編碼使用一個字節表示,從\u0080
到\u07ff
之間的全部字符的縮略編碼用兩個字節表示,從\u0800
到\uffff
之間的全部字符的縮略編碼就按照普通UTF-8編碼規則使用三個字節表示,這麼作的主要目的仍是爲了節省空間。
因爲Class文件中方法、字段等都須要引用CONSTANT_Utf8_info
型常量來描述名稱,因此CONSTANT_Utf8_info
型常量的最大長度就是Java中的方法、字段名的最大長度。這裏的最大長度就是length的最大值,即u2
類型能表達的最大值65535,因此Java程序中若是定義了超過64K英文字符的變量或發放名,將會沒法編譯。
回到上面那個例子,00 22後面跟着的是 0A 0006 0014,第一個字節0A轉化爲十進制爲10,表示的常量類型爲CONSTANT_Methodref_info
,這從常量表中能夠看到這個類型後面會兩個u2
來表示index
,分別表示CONSTANT_Class_info
和CONSTANT_NameAndType_info
。因此0006和0014轉化爲10進制分別是6和20。這裏可能不知道這些數字指代什麼意思,下面展現的是編譯後的字節碼指令就能夠清楚了。
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // HelloWorld
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/verzqli/snake/Main
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/verzqli/snake/Main;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Main.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 HelloWorld
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/verzqli/snake/Main
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
複製代碼
從上面能夠看到Constant pool
中一共有33個常量,第一個常量類型爲Methodref
,他其實指代的是這個Main
類,它是最基礎的Object類,而後這裏它有兩個索引分別指向6和20,分別是Class和NameAndType類型,和上面十六進制字節碼描述的同樣。
在常量池結束後,緊接着的兩個字節表明訪問標誌,這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個Class是類仍是接口;是否認義爲public類型;是否認義爲abstract類型,若是是類的話,是否被聲明爲final等,具體的標誌位以及標誌的含義見下表。
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 標識是否爲public類型 |
ACC_FINAL | 0x0010 | 標識是否被聲明爲final,只有類可設置 |
ACC_SUPER | 0x0020 | 用於兼容早期編譯器,新編譯器都設置改標誌,以在使用invokespecial指令時對子類方法作特殊處理 |
ACC_SYNTHETIC | 0x1000 | 標識這個類並不是由用戶代碼產生,而是由編譯器產生 |
ACC_INTERFACE | 0x0200 | 標識是否爲一個接口,接口默認同事設置ACC_ABSTRACT |
ACC_ABSTRACT | 0x0400 | 標識是否爲一個抽象類,不可與ACC_FINAL同時設置 |
ACC_ANNOTATION | 0x2000 | 標識這是不是一個註解類 |
ACC_ENUM | 0x4000 | 標識這是不是一個枚舉 |
ACCESS_FLAGS中一共有16個標誌位可用,當前只定義了其中8個(上面顯示了比8個多,是由於ACC_PRIVATE,ACC_PROTECTED,ACC_STATIC,ACC_VOLATILE,ACC_TRANSTENT並非修飾類的,這裏寫出來是讓你們知道還有這麼些標誌符),對於沒有使用到的標誌位要求一概爲0。Java不會窮舉上面全部標誌的組合,而是同|
運算來組合表示,至於這些標誌位是如何表示各類狀態,能夠看這篇文章,講的很清楚。
咱們繼續回到例子
例子中只是一個簡單的Main類,因此他的標誌是ACC_PUBLIC和ACC_SUPER,其餘標誌都不存在,因此它的訪問標誌爲0x0001|0x0020=0x0021。類索引和父類索引都是一個u2
類型的數據,接口索引是一組u2
類型的數據的集合,Class文件中由着三項數據來肯定這個類的繼承關係。這三者按順序排列在訪問標誌以後,本文例子中他們分別是:0005,0006,0000,也就是類索引爲5,父類索引爲6,接口索引集合大小爲0 ,查詢上面字節碼指令的常量池能夠一一對應(5對應com/verzqli/snake/Main
,6對應java/lang/Object
)。
類索引肯定這個類的全限定名,父類索引肯定這個類的父類全限定 名,由於Java不容許多重繼承,因此父類索引只有一個,除了Object
外,全部的類都有其父類,也就是其父類索引不爲0.接口索引便可用來描述這個類實現了哪些接口,這些被實現的接口按implements
(若是這個類自己就是一個接口,則應當是extends
語句)後的接口順序從左到右排列在接口索引集合中。
字段表用於描述類和接口中聲明的變量,包含類級別的變量以及實例變量。可是不包含方法內部聲明的局部變量。在Java中描述一個字段可能包含一下信息:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 標識是否爲private類型 |
ACC_PRIVATE | 0x0002 | 標識是否爲private類型 |
ACC_PROTECTED | 0x0004 | 標識是否爲protectes類型 |
ACC_STATIC | 0x0008 | 標識是否爲靜態類型 |
ACC_FINAL | 0x0010 | 標識是否被聲明爲final,只有類可設置 |
ACC_VOLATILE | 0x0040 | 標識是否被聲明volatile |
ACC_TRANSIENT | 0x0080 | 標識是否被聲明transient |
ACC_SYNTHETIC | 0x1000 | 標識這個類並不是由用戶代碼產生,而是由編譯器產生 |
ACC_ENUM | 0x4000 | 標識這是不是一個枚舉 |
字段表的結構分爲兩部分,第一部分爲兩個字節,描述字段個數(fields_count);第二部分是每一個字段的詳細信息(fields_info),按順序排列分別是訪問標誌(access_flags)、字段名稱索引(name_index)、字段的描述符索引(descriptor_index)、屬性表計數器(attribute_count)和屬性信息列表(attributes)。除了最後未知的屬性信息,其餘都是u2
的數據類型。
繼續看例子,這個例子選的有點尷尬,忘記往裏面放一個變量,因此在類索引後面的第一個u2
數據爲0000 表示字段個數爲0,因此後續的數據也沒有了。只能假設一組數據來看看字段表的結構
字節碼 | 00 01 | 00 02 | 00 03 | 00 07 | 00 00 |
---|---|---|---|---|---|
描述 | 字段表個數 | 訪問標誌 | 字段名稱索引 | 字段的描述符索引 | 屬性個數 |
內容 | 1 | ACC_PRIVATE | 3 | 7 | 0 |
字段表集合中不會列出從超類或者父類接口中繼承而來的字段,但有可能列出本來Java代碼之中不存在的字段,譬如在內部類中爲了保持對外部類的訪問性,會自動添加指向外部類實例的字段。另外,在Java中字段是沒法重載的,對於字節碼來說,只有兩個字段的描述符不一致,該字段纔是合法的。
爲了便於理解,這裏對上面提到的一些名詞進行一下解釋
com/verzqlisnake/Main
,僅僅把包名中的.
替換成/
便可爲了使連續的多個全限定名補償混淆,通常在使用時最後會假如一個;
,表示全限定名結束。public void fun()
和private int a
的簡單名稱就爲fun
和a
。void
,主要表示爲下表中形式。描述字符 | 含義 |
---|---|
描述 | 字段表個數 |
I | 基本類型int |
S | 基本類型short |
J | 基本類型long,這裏注意不是L,L是最後一個 |
F | 基本類型float |
D | 基本類型double |
B | 基本類型byte |
C | 基本類型char |
Z | 基本類型boolean |
V | 特殊類型void |
L | 對象類型,例如Ljava/lang/String |
對於數組類型,每一位度使用一個前置的[
來描述,例如String[]
數組將被記錄爲[Ljava/lang/String
,String[][]
數組被記錄爲[[Ljava/lang/String
;int[]
數組被記錄爲[I
。
用描述符來描述方法時,要先按照參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號()
之中。例如方法void fun()
的描述符爲()V
,String.toString()
的描述符爲()Ljava/lang/String
。public void multi(int i,String j,float[] c)
的描述符爲(ILjava/lang/String;[F)V
。
方法表的結構和字段表的結構幾乎徹底一致,存儲的格式和描述也很是類似。方法表的結構和字段表同樣,包含兩部分。第一部分爲方法計數器,第二部分爲每一個方法的詳細信息,依次包含了訪問標誌(access_flags)、方法名稱索引(name_index)、方法的描述符索引(descriptor_index)、屬性表計數器(attribute_count)和屬性信息列表(attributes)。這些數據的含義也和字段表很是類似,僅在訪問標誌和屬性表集合的可選項中有所區別。
類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attribute_info | attribute_count |
由於volatile
和transient
關鍵字不能修飾方法,因此方法標的訪問標誌中也就沒有這兩項標誌,與之對應的,synchronized、native、strictfp、abstract
能夠修飾方法,因此方發表的訪問標誌中增長了這幾類標誌,以下表
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 標識方法是否爲private |
ACC_PRIVATE | 0x0002 | 標識方法是否爲private |
ACC_PROTECTED | 0x0004 | 標識方法是否爲protectes |
ACC_STATIC | 0x0008 | 標識方法是否爲靜態 |
ACC_FINAL | 0x0010 | 標識方法是否被聲明爲final |
ACC_SYNCHRONIZED | 0x0020 | 標識方法是否被聲明synchronized |
ACC_BRIDGE | 0x0040 | 標識方法是否由編譯器產生的橋接方法 |
ACC_VARARGS | 0x0080 | 標識這個類是否接受不定參數 |
ACC_NATIVE | 0x0100 | 標識方法是否爲native |
ACC_ABSTRACT | 0x0400 | 標識方法是否爲abstract |
ACC_STRICTFP | 0x0800 | 標識方法是否爲strictfp |
ACC_SYNTHETIC | 0x1000 | 標識方法是否由編譯器自動產生的 |
繼續分析本文例子,方法表數據在字段表以後的數據 0002 0001 0007 0008 0001 0009
字節碼 | 00 02 | 00 01 | 00 07 | 00 08 | 00 01 | 0009 |
---|---|---|---|---|---|---|
描述 | 方法表個數 | 訪問標誌 | 方法名稱索引 | 方法的描述符索引 | 屬性表計數器 | 屬性名稱索引 |
內容 | 1 | ACC_PUBLIC | 7 | 8 | 1 | 9 |
從上表能夠看到方法表中有兩個方法,分別是編譯器添加的實例構造器<init>
和代碼中的main()
方法。第一個方法的訪問標誌爲ACC_PUBLIC
,方法名稱索引爲7(對應<init>
),方法描述符索引爲8(對應()V
),符合前面的常量池中的數據。
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
複製代碼
接着屬性表計數器的值爲1,表示此方法的屬性表集合有一箱屬性,屬性名稱索引爲9,對應常量池中爲Code
,說明此屬性是方法的字節碼描述。
方法重寫 : 若是父類方法在子類中沒有被重寫(Override),方法表集合中就不會出現來自父類的方法信息。但一樣的,有可能會出現由編譯器自動添加的方法,最典型的即是類構造器<clinit>
方法和實例構造器<init>
方法。 方法重載:在Java中藥重載(OverLoad)一個方法,除了要與原方法遇有相同的簡單名詞外,還須要要有一個與原方法徹底不一樣的特徵簽名。特徵簽名是一個方法中各個參數在常量池中的字段符號引用的集合,返回值並不會包含在前面中,所以沒法僅僅依靠返回值不一樣來重載一個方法。 可是在Class文件中,特徵簽名的範圍更大一些,只要描述符不是徹底一致的兩個方法也是能夠共存的。也就是說,若是兩個方法有相同的名稱和特徵簽名,但返回值不一樣,那麼也是能夠合法共存於同一個Class文件的,也就是說Java語法不支持,可是Class文件支持。
屬性表在前面的講解中已經出現過數次,在Class文件、字段表、方法表均可以攜帶本身的屬性表集合,已用於描述某些場景專有的信息 與Class文件中其餘的數據項目要求嚴格的順序、長度和內容不一樣,屬性表集合的限制稍微寬鬆了一些,不在要求各個屬性表具備嚴格的順序,只要不與已有的屬性名重複,任何人實現的編譯器均可以想屬性表中寫入本身定義的屬性信息:Java虛擬機運行時會忽略掉它不認識的屬性,具體的預約義屬性入下表。
屬性名稱 | 使用位置 | 含義 |
---|---|---|
Code | 方法表 | Java代碼編譯成的字節碼指令 |
ConstantValue | 字段表 | final關鍵字定義的常量池 |
Deprecated | 類,方法,字段表 | 被聲明爲deprecated的方法和字段 |
Exceptions | 方法表 | 方法拋出的異常 |
EnclosingMethod | 類文件 | 僅當一個類爲局部類或者匿名類是才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法 |
InnerClass | 類文件 | 內部類列表 |
LineNumberTable | Code屬性 | Java源碼的行號與字節碼指令的對應關係 |
LocalVariableTable | Code屬性 | 方法的局部變量描述 |
StackMapTable | Code屬性 | JDK1.6中新增的屬性,供新的類型檢查檢驗器檢查和處理目標方法的局部變量和操做數有所須要的類是否匹配 |
Signature | 類,方法表,字段表 | JDK1.5中新增的屬性,用於支持泛型狀況下的方法簽名。任何類,接口,初始化方法或成員的泛型前面若是包含了類型變量(Type Variables)或參數化類型(Parameterized Type),則signature屬性會爲它記錄泛型前面信息,因爲Java的泛型採用擦除法實現,在爲了便面類型信息被擦除後致使簽名混亂,須要這個屬性記錄泛型中的相關信息。 |
SourceFile | 類文件 | 記錄源文件名稱 |
SourceDebugExtension | 類文件 | JDK1.6中新增的屬性,用於存儲額外的調試信息 |
Synthetic | 類,方法表,字段表 | 標誌方法或字段爲編譯器自動生成的 |
LocalVariableTypeTable | 類 | JDK1.5中新增的屬性,使用特徵簽名代替描述符,是爲了引入泛型語法以後能描述泛型參數化類型而添加 |
RuntimeVisibleAnnotations | 類,方法表,字段表 | JDK1.5中新增的屬性,爲動態註解提供支持 ,用於指明那些註解是運行時(運行時就是進行反射調用)可見的 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | JDK1.5中新增的屬性,和上面恰好相反,用於指明哪些註解是運行時不可見的 |
RuntimeVisibleParameterAnnotation | 方法表 | JDK1.5中新增的屬性,做用與RuntimeVisibleAnnotations屬性相似,只不過做用對象爲方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | JDK1.5中新增的屬性,做用與RuntimeInvisibleAnnotations屬性相似,做用對象哪一個爲方法參數 |
AnnotationDefault | 方法表 | JDK1.5中新增的屬性,用於記錄註解類元素的默認值 |
BootstrapMethods | 類文件 | JDK1.7中新增的屬性,用於保存invokeddynamic指令引用的引導方式限定符 |
對於每一個屬性,它的名稱須要從常量池中應用一個CONSTANT_Utf8_info
類型的常量來標書,而屬性值的結構則是徹底子墩醫德,只須要經過一個u4
的長度屬性去說明屬性值作佔用的位數便可,其符合規則的結構以下圖。
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | infoattribute_length |
由於屬性表中的屬性包含二十多種,下面只對幾個屬性作一個簡要描述。
Java程序方法體中的代碼通過Javac編譯器處理後,最終變爲字節碼指令存儲在Code屬性內,Code屬性出如今方法表的屬性集合之中,但並未全部的方法表都必須存在這個屬性:接口或者抽象類中的方法就不存在Code屬性。若是方法表有Code屬性,那麼它的結構將以下表所示。
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info exception_table | exception_length | |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index
:一項指向CONSTANT_Utf8_info
型常量的索引,常量值固定爲「Code」,他表明了該屬性的名稱。 attribute_length
: 屬性值得長度,因爲屬性名稱索引和長度一共爲6字節,因此屬性值長度固定爲整個屬性表長度減去6個字節。 max_stack
:操做數棧深度的最大值,裝虛擬機運行的時候須要根據這個值來分配棧幀中的操做棧深度,沒有定義好迴歸的遞歸發生的棧溢出就是超過了這個值。 max_locals
:局部變量表所需的存儲空間。這裏的單位是Slot
,Slot
是虛擬機爲局部變量表分配內存所使用得最小單位。對於byte、char、float、int、short、boolean、returnAddress
這些長度不超過32位的整型數據,每一個局部變量佔用一個Slot
。像double和float
兩種64位的數據類型須要兩個Slot
來存放位置。**方法參數(實例方法中隱藏的this)、顯示異常處理器的參數(就是try-catch語句中catch鎖定義的異常)、放大提中定義的局部變量都須要使用局部變量表來存放。**由於Slot
能夠重用,因此這個最大值並非全部的Slot
之和,當代碼執行超過一個局部變量的做用於時,這個局部變量所佔用的Slot
能夠被其餘局部變量使用,因此該值主要根據變量的所用域來計算大小。 code_length
:字節碼長度。雖然是u4
長度,可是虛擬機規定了一個方法中的字節碼指令條數不超過u2(65535)
條,超過的話編譯器會拒絕編譯。 code
:存儲編譯後生成的字節碼指令。每一個字節碼指令是一個u1
類型的單字節。當虛擬機督導一個字節碼時,能夠找到這個字節碼代碼的指令,並能夠知道這個指令後面是否須要跟隨參數以及參數的意思。一個u1
數據的取值範圍爲0x00~0xff,也就是一共能夠表達256條指令,目前,Java虛擬機以及定義了其中200多條編碼值對應的指令含義,具體指令能夠看虛擬機字節碼指令表。 由於異常表對於Code屬性不是必須存在的,後面幾個類型也沒有太大的重要性,這裏就暫時略過。
throws
關鍵詞後面列舉的異常,其結構以下圖。類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_lrngth | 1 |
u2 | number_of_exception | 1 |
u2 | exception_index_table | number_of_exceptions |
number_of_exception
:表示方法可能拋出此項值數值的受查異常,每一種受查異常exception_index_table
表示。 exception_index_table
:表示一個指向常量池中CONSTANT_Class_indo
型常量的索引,因此,表明了該種受查異常的類型。
SourceFile屬性用於記錄生成這個Class文件的源碼文件名稱。可使用Javac的-g:none和-g:source
選項來關閉或者生成這項信息。對於大多數類來講,類名和文件名是一致的,可是例如內部類等一些特殊狀況就會不同。若是不生成這個屬性,當拋出異常時,堆棧中將不會顯示出錯代碼所屬的文件名,其結構入下表:
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
sourcefile_index
:指向常量池中的CONSTANT_Utf8_indo
型常量,常量值是源碼文件的文件名。
InnerClass屬性用於記錄內部類與宿主之間的關聯,若是一個類中定義了內部類,那編譯器將會爲他以及它所包含的內部類生成InnerClasses屬性,其表結構以下圖:
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_classes | number_of_classes |
number_of_classes
:表示內部類信息的個數。每個內部類的信息都由一inner_classes_info
表進行描述,改表結果以下:
類型 | 名稱 | 數量 |
---|---|---|
u2 | inner_class_info_index | 1 |
u2 | outer_class_info_index | 1 |
u2 | inner_name_index | 1 |
u2 | inner_class_access_flags | 1 |
inner_class_info_index
:指向常量池中的CONSTANT_Class_indo
型常量的索引,表示內部類的符號引用。 outer_class_info_index
:指向常量池中的CONSTANT_Class_indo
型常量的索引,表示宿主類的符號引用。 inner_class_access_flags
:內部類的訪問標誌,相似於類的access_flags
。
ConstantValue屬性的做用是通知虛擬機自動爲靜態變量賦值。只有被static
關鍵字修飾的變量(類變量)纔可使用這項屬性,例如int a=1
和static int a=1
,虛擬機對這兩種變量的賦值方式和時刻都有所不一樣。對於前者的賦值是在實例構造器方法中進行的,換而言之就是一個類的構造的方法沒有被執行前,該類的成員變量是還沒賦值的;而對於後者,則有兩種方式能夠選擇:在類構造器方法中或者使用ConstantValue屬性。目前Javac編譯器的選擇是若是同時使用final
和static
來修飾一個變量,而且這個變量的數據類型是基本類型或者字符串類型時,就生成ConstantValue屬性來初始化,若是這個變量沒有被final
修飾,或者並不是基本類型變量或字符串,則會選擇在<clinit>
方法中進行初始化。
<clinit>
:類構造器。在jvm第一次加載class文件時調用,由於是類級別的,因此只加載一次,是編譯器自動收集類中全部類變量(static修飾的變量)和靜態語句塊(static{}),中的語句合併產生的,編譯器收集的順序,是由程序員在寫在源文件中的代碼的順序決定的。 <init>
:實例構造器方法,在實例建立出來的時候調用,包括調用new操做符;調用Class或java.lang.reflect.Constructor對象的newInstance()方法;調用任何現有對象的clone()方法;經過java.io.ObjectInputStream類的getObject()方法反序列化。
<clinit>
方法和類的構造函數不一樣,它不須要顯示調用父類的構造方法,虛擬機會保證子類的<clinit>
方法執行以前,父類的此方法已經執行完畢,所以虛擬機中第一個被執行的方法的類確定是java.lang.Object。言而言之就是先須要<clinit>
完成類級別的變量和代碼塊的加載,再進行對象級別的加載信息,因此常常看的面試題子類和父類哪一個語句先被執行就是這些決定的。
public class Main {
static final int a=1;
}
字節碼:
static final int a;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 1
未添加final
public class Main {
static int a=1;
}
字節碼:
public com.verzqli.snake.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/verzqli/snake/Main;
//能夠看到 這裏的初始化放在了Main的類構造器中
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #2 // Field a:I
4: return
LineNumberTable:
line 13: 0
}
public class Main {
int a=1;
}
字節碼:
//能夠看到 這裏的初始化放在了Main的實例構造器中
public com.verzqli.snake.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
}
複製代碼
字節碼指令是一個字節長度的,表明着某種特色操做含義的數字,總數不超過256條(所有字節碼指令彙編),。對於大部分與數據類型相關的字節碼指令,它們的操做碼助記符中都有特殊字符來代表專門爲那種數據類型服務,以下表:
描述字符 | 含義 |
---|---|
i | 基本類型int |
s | 基本類型short |
l | 基本類型long,這裏注意不是L,L是最後一個 |
f | 基本類型float |
d | 基本類型double |
b | 基本類型byte |
c | 基本類型char |
b | 基本類型boolean |
a | 對象類型引用reference |
這裏有一個注意的點,這對於不是整數類型的byte、char、short、boolean。編譯器會在編譯器或運行期將byte和short類型的數據帶符號擴展(Sign-extend)爲相應的int類型數據,將boolean和char類型數據零位擴展(Zero-extend)爲相應的int數據。一樣在處理上訴類型的數組數據是,也會轉換爲使用int類型的字節碼指令來處理。
加載和存儲指令用於將數據在棧幀中的局部變量表和操做數棧之間來回傳輸。
<類型>load_<下標>
:將一個局部變量加載到操做數棧。例如iload_1,將一個int類型局部變量(下標爲1,0通常爲this)從局部變量表加載到操做棧,其餘的也都相似,例如:dload_2,fload_3。 <類型>store_<下標>
:將一個數值從操做數棧棧頂存儲到局部變量表。例如istore_3,將一個int類型的數值從操做數棧棧頂存儲到局部變量3中,後綴爲3,證實局部變量表中已經存在了兩個值。 <類型>const_<具體的值>
:將一個常量加載到操做數棧。例如iconst_3,將常量3加載到操做數棧。 wide擴展
:當上述的下標誌超過3時,就不用下劃線的方式了,而是使用istore 6
,load的寫法也是同樣。 bipush、sipush、ldc
:當上述的const指令後面的值變得很大時,該指令也會改變。
看例子:
public void save() {
int a = 1;
int b = 6;
int c = 128;
int d = 32768 ;
float f = 2.0f;
}
字節碼:
Code:
stack=1, locals=6, args_size=1
0: iconst_1 //將常量1入棧,
1: istore_1 //將棧頂的1存入局部變量表,下標爲1,由於0存儲了整個類的this
2: bipush 6 //將常量6入棧,同時也是以wide擴展的形式
4: istore_2 //將棧頂的6存入局部變量表,下標爲2
5: sipush 128 //將常量128入棧,
8: istore_3 //將棧頂的128存入局部變量表,下標爲3 ,後面同樣的意思
9: ldc #2 // int 32768
11: istore 4
13: fconst_2
14: fstore 5
16: return
複製代碼
運算主要分爲兩種:對徵信數據進行運算的指令和對浮點型數據運算的指令,和前面說的同樣,對於byte、char、short、和 boolean類型的算數質量都使用int類型的指令替代。整數和浮點數的運算指令在移除和被領出的時候也有各自不一樣的表現行爲。具體的指令也是在運算指令前加上對應的類型便可,例如加法指令:iadd,ladd,fadd,dadd。
上面的指令不必強記,須要的時候查找一下便可,看多了也天然就熟悉了。至於浮點數運算的精度損失之類的這裏就很少作贅述了。
類型轉換指令能夠將兩種不一樣的數值類型進行相互轉換,這些轉換通常用於實現用戶代碼中的顯示類型轉換操做。
Java虛擬機直接支持寬化數據類型轉換(小範圍數據轉換爲大數據類型),不須要顯示的轉換指令,例如int轉換long,float和double。舉例:int a=10;long b =a
Java虛擬機轉換窄化數據類型轉換時,必須顯示的調用轉化指令。舉例:long b=10;int a = (long)b
。
類型轉換的字節碼指令其實就比較簡單了,<前類型>2<後類型>
,例如i2l,l2i,i2f,i2d。固然這裏舉的都是基本數據類型,若是是對象,當相似寬化數據類型時就直接使用,當相似窄化數據類型時,須要checkcast
指令。
public class Main {
public static void main(String[] args) {
int a = 1;
long b = a;
Parent Parent = new Parent();
Son son = (Son) Parent;
}
}
字節碼:
Code:
stack=2, locals=6, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: i2l
4: lstore_2
5: new #2 // class com/verzqli/snake/Parent
8: dup
9: invokespecial #3 // Method com/verzqli/snake/Parent."<init>":()V
12: astore 4
14: aload 4
16: checkcast #4 // class com/verzqli/snake/Son
19: astore 5
21: return
複製代碼
注意上面這個轉換時錯誤的,父類是不能轉化爲子類的,編譯期正常,可是運行是會報錯的,這就是checkcast指令的緣由。
雖然累實例和數組都是對象,但Java蘇尼基對類實例和數組的建立與操做使用了不一樣的字節碼指令。對象建立後,就能夠經過對象訪問指令獲取對象實例或者數組實例中的字段或者數組元素,這些指令以下。
new
:建立類實例的指令newarray、anewarray、multianewarray
:建立數組的指令getfield、putfield、getstatic、putstatic
:訪問類字段(static字段,被稱爲類變量)和實例字段(非static字段,)。(b、c、s、i、l、f、d、a)aload
:很明顯,就是基礎數據類型加上aload,將一個數組元素加載到操做數棧。(b、c、s、i、l、f、d、a)astore
:同上面同樣的原理,將操做數棧棧頂的值存儲到數組元素中。arraylength
:取數組長度instanceof、checkcast
:檢查類實例類型的指令。如同操做一個普通數據結構中的堆棧那樣,Java虛擬機提供了一些直接操做操做數棧的指令。
pop、pop2
:將操做數棧棧頂的一個或兩個元素出棧。dup、dup二、dup_x一、dup2_x一、dup_x二、dup2_x2
:服戰棧頂一個或兩個數值並將期值複製一份或兩份後從新壓入棧頂。swap
:將棧頂兩個數互換。方法調用的指令只要包含下面這5條
invokespecial
:用於調用一些須要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法。invokestatic
:用於調用static方法。invokeinterface
:用於調用接口方法,他會在運行時搜索一個實現了這個接口方法的對象,找出合適的方法進行調用。invokevirtual
:用於調用對象的實例方法,根據對象的實際類型進行分派。invokedynamic
:用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法。前面4條指令的分派邏輯都固話在Java虛擬機內部,而此條指令的分派邏輯是由用戶設定的引導方法決定的。(i,l,f,d, 空)return
:根據前面的類型來肯定返回的數據類型,爲空時表示void在Java程序中顯示拋出異常的操做(throw語句)都由athrow
指令來實現。可是處理異常(catch語句)不是由字節碼指令來實現的,而是採用異常表來完成的,以下例子。
public class Main {
public static void main(String[] args) throws Exception{
try {
Main a=new Main();
}catch (Exception e){
e.printStackTrace();
}
}
}
字節碼:
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/verzqli/snake/Main
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: goto 16
11: astore_1
12: aload_1
13: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
16: return
複製代碼
Java虛擬機能夠支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用Monitor 實現的。 正常狀況下Java運行是同步的,無需使用字節碼控制。虛擬機能夠從方法常量池的方法表結構中的ACC_SYNCHRONIZE
訪問標誌得知一個方法是否聲明爲同步方法。當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZE
訪問表示是否被設置,若是設置了,執行線程就要求先持有Monitor
,而後才能執行方法,最後當方法完成時釋放Monitor
。在方法執行期間,執行線程持有了Monitor
,其餘任何一個線程都沒法在獲取到同一個Monitor
。若是一個同步方法執行期間拋出了異常,而且在方法內部沒法處理次異常,那麼這個同步方法所持有的Monitor
將在異常拋出到同步方法以外時自動釋放。 同步一段指令集序列一般是由synchronized
語句塊來表示的,Java虛擬機指令集中有monitorenter
和monitorexit
兩條指令來支持synchronized
關鍵字。以下例子
public class Main {
public void main() {
synchronized (Main.class) {
System.out.println("synchronized");
}
function();
}
private void function() {
System.out.printf("function");
}
}
字節碼:
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // class com/verzqli/snake/Main 將Main引用入棧
2: dup // 複製棧頂引用 Main
3: astore_1 // 將棧頂應用存入到局部變量astore1中
4: monitorenter // 將棧頂元素(Main)做爲鎖,開始同步
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String synchronized ldc指令在運行時建立這個字符串
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1 // 將局部變量表的astore1入棧(Main)
14: monitorexit //退出同步
15: goto 23 // 方法正常結束,跳轉到23
18: astore_2 //這裏是出現異常走的路徑,將棧頂元素存入局部變量表
19: aload_1 // 將局部變量表的astore1入棧(Main)
20: monitorexit //退出同步
21: aload_2 //將前面存入局部變量的異常astore2入棧
22: athrow // 把異常對象長線拋出給main方法的調用者
23: aload_0 // 將類this入棧,以便下面調用類的方法
24: invokespecial #6 // Method function:()V
27: return
複製代碼
編譯器必須確保不管方法經過何種方式完成,方法中調用過的每條monitorenter
指令都必須執行其對應的monitorexit
指令,不管這個方法是正常結束仍是異常結束。
前面說了一堆,空看理論既枯燥又難懂,理論就圖一樂,真懂還得看例子。
相信面試過的人基本地看過這個面試題,而後還扯過值傳遞仍是引用傳遞這個問題,下面從字節碼的角度來分析這個問題。
public class Main {
String str="newStr";
String[] array={"newArray1","newArray2"};
public static void main(String[] args) {
Main main=new Main();
main.change(main.str, main.array);
System.out.println(main.str);
System.out.println(Arrays.toString(main.array));
}
private void change(String str, String[] array) {
str="newStrEdit";
array[0]="newArray1Edit";
}
}
輸出結果:
newStr
[newArray1Edit, newArray2]
字節碼:
private void change(java.lang.String, java.lang.String[]);
descriptor: (Ljava/lang/String;[Ljava/lang/String;)V
flags: ACC_PRIVATE
Code:
stack=3, locals=3, args_size=3
0: ldc #14 // String newStrEdit
2: astore_1
3: aload_2
4: iconst_0
5: ldc #15 // String newArray1Edit
7: aastore
8: return
}
複製代碼
這裏main方法的字節碼內容能夠忽略,主要看這個change方法,下面用圖來表示。
這是剛進入這個方法的狀況,這時候尚未執行方法的內容,局部變量表存了三個值,第一個是this指代這個類,在普通方法內之因此能夠拿到外部的全局變量就是由於方法內部的局部變量表的第一個就是類的this,當獲取外部變量時,先將這個this入棧aload_0
,而後就能夠獲取到這個類全部的成員變量(也就是外部全局變量)了。 由於這個方法傳進來了兩個值,這裏局部變量表存儲的是這兩個對象的引用,也就是在堆上的內存地址。
str = "newStrEdit";
這條語句,先ldc指令建立了newStrEdit(0xaaa)字符串入棧,而後
astore_1
指令將棧頂的值保存再局部變量1中,覆蓋了原來的地址,因此這裏對局部變量表的修改徹底沒有影響外面的值。
上面執行
array[0] = "newArrar1Edit";
這條語句,將array的地址入棧,再將要修改的數組下標0入棧,最後建立newArray1Edit字符串入棧。最後調用
aastore
指令將棧頂的引用型數值(newArray1Edit)、數組下標(0)、數組引用(0xfff)依次出棧,最後將數值存入對應的數組元素中,這裏能夠看到對這個數組的操做一直都是這個0xfff地址,這個地址和外面的array指向的是同一個數組對象,因此這裏修改了,外界的那個array也就一樣修改了內容。
看過前面那個例子應該對局部變量表是什麼有所瞭解,下面這個例子就不繪製上面那個圖了。這個例子也是一個常見的面試題,判斷try-catch-finally-return
的執行順序。
finally是一個最終都會執行的代碼塊,finally裏面的return會覆蓋try和catch裏面的return,同時在finally裏面修改局部變量不會影響try和catch裏面的局部變量值,除非trycatch裏面返回的值是一個引用類型。
public static void main(String[] args) {
Main a=new Main();
System.out.println("args = [" + a.testFinally() + "]");;
}
public int testFinally(){
int i=0;
try{
i=2;
return i;
}catch(Exception e){
i=4;
return i;
}finally{
i=6;
}
字節碼:
public int testFinally();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_0 // 常量0入棧
1: istore_1 // 賦值給內存變量1(i) i=0
2: iconst_2 // 常量2入棧
3: istore_1 // 賦值給內存變量1(i) i=2
4: iload_1 // 內存變量1(i)入棧
5: istore_2 // 將數據存儲在內存變量2 這裏緣由下面說明
6: bipush 6 // 常量6入棧
8: istore_1 // 保存再內存變量1
9: iload_2 // 加載內存變量2
10: ireturn // 返回上一句加載的內存變量2(i) i=2
11: astore_2 // 看最下面的異常表,若是2-6發生異常,就從11開始,下面就是發生異常後進入catch的內容
12: iconst_4 // 常量4入棧
13: istore_1 // 保存在局部變量1
14: iload_1 // 加載局部變量1
15: istore_3 // 將局部變量1內容保存到局部變量3,緣由和上面5同樣
16: bipush 6 // 常量6入棧 (進入了catch最後也會執行finally,因此這裏會從新再執行一遍finally)
18: istore_1 // 保存在局部變量1
19: iload_3 // 加載局部變量3並返回
20: ireturn //上面相似的語句,不過是catch-finally的路徑
21: astore 4 // finally 生成的冗餘代碼,這裏發生的異常會拋出去
23: bipush 6
25: istore_1
26: aload 4
28: athrow
Exception table:
from to target type
2 6 11 Class java/lang/Exception //若是2-6發生指定的Exception異常(try),就從11開始
2 6 21 any //若是2-6發生任何其餘異常(finally),就從21開始
11 16 21 any //若是11-16發生任何其餘異常(catch),就從21開始
21 23 21 any //其實這裏有點不太能理解爲何會循環,若是有知道的大佬能夠解答一下
複製代碼
在Java1.4以後 Javac編譯器 已經再也不爲 finally 語句生成 jsr 和 ret 指令了, 當異常處理存在finally語句塊時,編譯器會自動在每一段可能的分支路徑以後都將finally語句塊的內容冗餘生成一遍來實現finally語義。(21~28)。但咱們Java代碼中,finally語句塊是在最後的,編譯器在生成字節碼時候,其實將finally語句塊的執行指令移到了ireturn指令以前,指令重排序了。因此,從字節碼層面,咱們解釋了,爲何finally語句總會執行!
若是try
中有return
,會在return
以前執行finally中的代碼,可是會保存一個副本變量(第五和第十五行)。finally
修改原來的變量,但try
中return
返回的是副本變量,因此若是是賦值操做,即便執行了finally
中的代碼,變量也不必定會改變,須要看變量是基本類型仍是引用類型。 可是若是在finally裏面添加一個return,那麼第9行和第19行加載的就是finally
塊裏修改的值(iload_1),再在最後添加一個iload_1
和ireturn
,感興趣的能夠本身去看一下字節碼。
仍是上面那個相似的例子,這裏作一下改變
public static void main(String[] args) {
Main a = new Main();
System.out.println("args = [" + a.testFinally1() + "]");
System.out.println("args = [" + a.testFinally2() + "]");
}
public StringBuilder testFinally1() {
StringBuilder a = new StringBuilder("start");
try {
a.append("try");
return a;
} catch (Exception e) {
a.append("catch");
return a;
} finally {
a.append("finally");
}
}
public String testFinally2() {
StringBuilder a = new StringBuilder("start");
try {
a.append("try");
return a.toString();
} catch (Exception e) {
a.append("catch");
return a.toString();
} finally {
a.append("finally");
}
}
輸出結果:
args = [starttryfinally]
args = [starttry]
複製代碼
這裏就不列舉全局字節碼了,兩個方法有點多,你們能夠本身嘗試去看一下。這裏作一下說明爲何第一個返回的結果沒有finally
。 首先這個方法的局部變量表1裏面存儲了一個StringBuilder地址,執行到try~finally這一部分沒什麼區別,都是複製了一份變量1的地址到變量3,注意,這兩個地址是同樣的。 那爲何第二個返回方法少了finally
呢,那是由於s.toString()
方法這個看起來是在return後面,但其實這個方法屬於這個try代碼塊,分爲兩步,先調用toString()
生成了一個新的字符串starttry
而後返回,因此這裏的字節碼邏輯就以下:
17: aload_1
18: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: astore_2
22: aload_1
23: ldc #18 // String finally
25: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: pop
29: aload_2
30: areturn
複製代碼
能夠很清楚的看到 調用append方法拼接「start」和「try」後,先調用了toString()
方法而後將值存入局部變量2。這時候finally沒有和上面那樣複製一份變量,而是繼續使用局部變量1的引用來繼續append,最後的結果也存入了局部變量1中,最後返回的是局部變量2中的值starttry
,可是要注意此時局部變量1中指向的StringBuilder的值倒是starttryfinally
,因此這也就是方法1中返回的值。
若是是ide的話,應該均可以,經過``Setting->Tools->External Tools進入 而後建立一個自定義的tools。
如上圖,新建一個External Tools
,第一行輸入你電腦的javap.exe
地址,第二行是你想要的命令符,第三行是顯示位置,設置好後要對着代碼右鍵便可一鍵查看字節碼指令,方便快捷。
new
指令建立後爲何會執行一個dup
(將棧頂的數據複製一份並壓入棧)?對象被new以後還須要調用invokespecial <init>
來初始化,這裏須要拿到一份new指令分配的內存地址,而後棧中還存在的一份地址是供這個對象給其餘地方調用的,不然棧中若是不存在這個引用以後,任何地方都訪問不到這個類了,因此就算這個類沒有被任何地方調用,棧中仍是會存在一份它的引用。
原本只是想寫點字節碼指令的筆記,結果越記越多,本文大部分理論知識來自於《深刻理解Java虛擬機--周志明》,寫得多了,錯誤在所不免,若是有發現的還望指出,謝謝。