計算機只認識0和1。這意味着任何語言編寫的程序最終都須要通過編譯器編譯成機器碼才能被計算機執行。因此,咱們所編寫的程序在不一樣的平臺上運行前都要通過從新編譯才能被執行。 而Java剛誕生的時候曾經提過一個很是著名的宣傳口號: "一次編寫,處處運行"。java
Write Once, Run Anywhere.編程
爲了實現該目的,Sun公司以及其餘虛擬機提供商發佈了許多能夠運行在不一樣平臺上的JVM虛擬機,而這些虛擬機都擁有一個共同的功能,那就是能夠載入和執行同一種與平臺無關的字節碼(ByteCode)。 因而,咱們的源代碼再也不必須根據不一樣平臺翻譯成0和1,而是間接翻譯成字節碼,儲存字節碼的文件再交由運行於不一樣平臺上的JVM虛擬機去讀取執行,從而實現一次編寫,處處運行的目的。 現在,JVM也再也不只支持Java,由此衍生出了許多基於JVM的編程語言,如Groovy, Scala, Koltin等等。數組
源代碼中的各類變量,關鍵字和運算符號的語義最終都會編譯成多條字節碼命令。而字節碼命令所能提供的語義描述能力是要明顯強於Java自己的,因此有其餘一些一樣基於JVM的語言能提供許多Java所不支持的語言特性。bash
下面以一個簡單的例子來逐步講解字節碼。jvm
//Main.java
public class Main {
private int m;
public int inc() {
return m + 1;
}
}
複製代碼
經過如下命令, 能夠在當前所在路徑下生成一個Main.class
文件。編程語言
javac Main.java
複製代碼
以文本的形式打開生成的class文件,內容以下:函數
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4d61 696e 2e6a 6176 610c
0007 0008 0c00 0500 0601 0010 636f 6d2f
7268 7974 686d 372f 4d61 696e 0100 106a
6176 612f 6c61 6e67 2f4f 626a 6563 7400
2100 0300 0400 0000 0100 0200 0500 0600
0000 0200 0100 0700 0800 0100 0900 0000
1d00 0100 0100 0000 052a b700 01b1 0000
0001 000a 0000 0006 0001 0000 0003 0001
000b 000c 0001 0009 0000 001f 0002 0001
0000 0007 2ab4 0002 0460 ac00 0000 0100
0a00 0000 0600 0100 0000 0800 0100 0d00
0000 0200 0e
複製代碼
對於文件中的16進制代碼,除了開頭的cafe babe
,剩下的內容大體能夠翻譯成: 啥玩意啊這是......工具
英雄莫慌,咱們就從咱們所能認識的"cafe babe"講起吧。 文件開頭的4個字節稱之爲 魔數,惟有以"cafe babe"開頭的class文件方可被虛擬機所接受,這4個字節就是字節碼文件的身份識別。 目光右移,0000是編譯器jdk版本的次版本號0,0034轉化爲十進制是52,是主版本號,java的版本號從45開始,除1.0和1.1都是使用45.x外,之後每升一個大版本,版本號加一。也就是說,編譯生成該class文件的jdk版本爲1.8.0。 經過java -version
命令稍加驗證, 可得結果。ui
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
複製代碼
結果驗證成立。this
繼續往下是常量池。但我並不打算繼續直接分析這個十六進制文件。
使用到java內置的一個反編譯工具javap
能夠反編譯字節碼文件。 經過javap -help
可瞭解javap的基本用法
用法: javap <options> <classes>
其中, 可能的選項包括:
-help --help -? 輸出此用法消息
-version 版本信息
-v -verbose 輸出附加信息
-l 輸出行號和本地變量表
-public 僅顯示公共類和成員
-protected 顯示受保護的/公共類和成員
-package 顯示程序包/受保護的/公共類
和成員 (默認)
-p -private 顯示全部類和成員
-c 對代碼進行反彙編
-s 輸出內部類型簽名
-sysinfo 顯示正在處理的類的
系統信息 (路徑, 大小, 日期, MD5 散列)
-constants 顯示最終常量
-classpath <path> 指定查找用戶類文件的位置
-cp <path> 指定查找用戶類文件的位置
-bootclasspath <path> 覆蓋引導類文件的位置
複製代碼
輸入命令javap -verbose -p Main.class
查看輸出內容:
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
Last modified 2018-4-7; size 362 bytes
MD5 checksum 4aed8540b098992663b7ba08c65312de
Compiled from "Main.java"
public class com.rhythm7.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/rhythm7/Main.m:I
#3 = Class #20 // com/rhythm7/Main
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/rhythm7/Main;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Main.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/rhythm7/Main
#21 = Utf8 java/lang/Object
{
private int m;
descriptor: I
flags: ACC_PRIVATE
public com.rhythm7.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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rhythm7/Main;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/rhythm7/Main;
}
SourceFile: "Main.java"
複製代碼
開頭的7行信息包括:Class文件當前所在位置,最後修改時間,文件大小,MD5值,編譯自哪一個文件,類的全限定名,jdk次版本號,主版本號。 而後緊接着的是該類的訪問標誌:ACC_PUBLIC, ACC_SUPER,訪問標誌的含義以下:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否爲Public類型 |
ACC_FINAL | 0x0010 | 是否被聲明爲final,只有類能夠設置 |
ACC_SUPER | 0x0020 | 是否容許使用invokespecial字節碼指令的新語義. |
ACC_INTERFACE | 0x0200 | 標誌這是一個接口 |
ACC_ABSTRACT | 0x0400 | 是否爲abstract類型,對於接口或者抽象類來講, 次標誌值爲真,其餘類型爲假 |
ACC_SYNTHETIC | 0x1000 | 標誌這個類並不是由用戶代碼產生 |
ACC_ANNOTATION | 0x2000 | 標誌這是一個註解 |
**ACC_ENUM ** | 0x4000 | 標誌這是一個枚舉 |
Constant pool
意爲常量池。 常量池能夠理解成Class文件中的資源倉庫。主要存放的是兩大類常量:字面量(Literal)和符號引用(Symbolic References)。字面量相似於java中的常量概念,如文本字符串,final常量等,而符號引用則屬於編譯原理方面的概念,包括如下三種:
不一樣於C/C++, JVM是在加載Class文件的時候才進行的動態連接,也就是說這些字段和方法符號引用只有在運行期轉換後才能得到真正的內存入口地址。當虛擬機運行時,須要從常量池得到對應的符號引用,再在類建立或運行時解析並翻譯到具體的內存地址中。
直接經過反編譯文件來查看字節碼內容:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#4 = Class #21 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#18 = NameAndType #7:#8 // "<init>":()V
#21 = Utf8 java/lang/Object
複製代碼
第一個常量是一個方法定義,指向了第4和第18個常量。以此類推查看第4和第18個常量。最後能夠拼接成第一個常量右側的註釋內容:
java/lang/Object."<init>":()V
複製代碼
這段能夠理解爲該類的實例構造器的聲明,因爲Main類沒有重寫構造方法,因此調用的是父類的構造方法。此處也說明了Main類的直接父類是Object。 該方法默認返回值是V, 也就是void,無返回值。
同理可分析第二個常量:
#2 = Fieldref #3.#19 // com/rhythm7/Main.m:I
#3 = Class #20 // com/rhythm7/Main
#5 = Utf8 m
#6 = Utf8 I
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/rhythm7/Main
複製代碼
此處聲明瞭一個字段m,類型爲I, I便是int類型。關於字節碼的類型對應以下:
標識字符 | 含義 |
---|---|
B | 基本類型byte |
C | 基本類型char |
D | 基本類型double |
F | 基本類型float |
I | 基本類型int |
J | 基本類型long |
S | 基本類型short |
Z | 基本類型boolean |
V | 特殊類型void |
L | 對象類型,以分號結尾,如Ljava/lang/Object; |
對於數組類型,每一位使用一個前置的"["字符來描述,如定義一個java.lang.String[][]類型的維數組,將被記錄爲"[[Ljava/lang/String;"
在常量池以後的是對類內部的方法描述,在字節碼中以表的集合形式表現,暫且無論字節碼文件的16進制文件內容如何,咱們直接看反編譯後的內容。
private int m;
descriptor: I
flags: ACC_PRIVATE
複製代碼
此處聲明瞭一個私有變量m,類型爲int,返回值爲int
public com.rhythm7.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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rhythm7/Main;
複製代碼
這裏是構造方法:Main(),返回值爲void, 公開方法。 code內的主要屬性爲:
stack 最大操做數棧,JVM運行時會根據這個值來分配棧幀(Frame)中的操做棧深度,此處爲1
locals: 局部變量所需的存儲空間,單位爲Slot, Slot是虛擬機爲局部變量分配內存時所使用的最小單位,爲4個字節大小。方法參數(包括實例方法中的隱藏參數this),顯示異常處理器的參數(try catch中的catch塊所定義的異常),方法體中定義的局部變量都須要使用局部變量表來存放。值得一提的是,locals的大小並不必定等於全部局部變量所佔的Slot之和,由於局部變量中的Slot是能夠重用的。
args_size: 方法參數的個數,這裏是1,由於每一個實例方法都會有一個隱藏參數this
attribute_info 方法體內容,0,1,4爲字節碼"行號",該段代碼的意思是將第一個引用類型本地變量推送至棧頂,而後執行該類型的實例方法,也就是常量池存放的第一個變量,也就是註釋裏的"java/lang/Object."":()V", 而後執行返回語句,結束方法。
LineNumberTable 該屬性的做用是描述源碼行號與字節碼行號(字節碼偏移量)之間的對應關係。可使用 -g:none 或-g:lines選項來取消或要求生成這項信息,若是選擇不生成LineNumberTable,當程序運行異常時將沒法獲取到發生異常的源碼行號,也沒法按照源碼的行數來調試程序。
LocalVariableTable 該屬性的做用是描述幀棧中局部變量與源碼中定義的變量之間的關係。可使用 -g:none 或 -g:vars來取消或生成這項信息,若是沒有生成這項信息,那麼當別人引用這個方法時,將沒法獲取到參數名稱,取而代之的是arg0, arg1這樣的佔位符。 start 表示該局部變量在哪一行開始可見,length表示可見行數,Slot表明所在幀棧位置,Name是變量名稱,而後是類型簽名。
同理能夠分析Main類中的另外一個方法"inc()": 方法體內的內容是:將this入棧,獲取字段#2並置於棧頂, 將int類型的1入棧,將棧內頂部的兩個數值相加,返回一個int類型的值。
源碼文件名稱
經過以上一個最簡單的例子,能夠大體瞭解源碼被編譯成字節碼後是什麼樣子的。 下面利用所學的知識點來分析一些Java問題:
public class TestCode {
public int foo() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
複製代碼
試問當不發生異常和發生異常的狀況下,foo()的返回值分別是多少。 使出老手段
javac TestCode.java
javap -verbose TestCode.class
複製代碼
查看字節碼的foo方法內容:
public int foo();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1 //int型1入棧 ->棧頂=1
1: istore_1 //將棧頂的int型數值存入第二個局部變量 ->局部2=1
2: iload_1 //將第二個int型局部變量推送至棧頂 ->棧頂=1
3: istore_2 //!!將棧頂int型數值存入第三個局部變量 ->局部3=1
4: iconst_3 //int型3入棧 ->棧頂=3
5: istore_1 //將棧頂的int型數值存入第二個局部變量 ->局部2=3
6: iload_2 //!!將第三個int型局部變量推送至棧頂 ->棧頂=1
7: ireturn //從當前方法返回棧頂int數值 ->1
8: astore_2 // ->局部3=Exception
9: iconst_2 // ->棧頂=2
10: istore_1 // ->局部2=2
11: iload_1 //->棧頂=2
12: istore_3 //!! ->局部4=2
13: iconst_3 // ->棧頂=3
14: istore_1 // ->局部1=3
15: iload_3 //!! ->棧頂=2
16: ireturn // -> 2
17: astore 4 //將棧頂引用型數值存入第五個局部變量=any
19: iconst_3 //將int型數值3入棧 -> 棧頂3
20: istore_1 //將棧頂第一個int數值存入第二個局部變量 -> 局部2=3
21: aload 4 //將局部第五個局部變量(引用型)推送至棧頂
23: athrow //將棧頂的異常拋出
Exception table:
from to target type
0 4 8 Class java/lang/Exception //0到4行對應的異常,對應#8中儲存的異常
0 4 17 any //Exeption以外的其餘異常
8 13 17 any
17 19 17 any
複製代碼
在字節碼的4,5,以及13,14中執行的是同一個操做,就是將int型的3入操做數棧頂,並存入第二個局部變量。這正是咱們源碼在finally語句塊中內容。也就是說,JVM在處理異常時,會在每一個可能的分支都將finally語句重複執行一遍。 經過一步步分析字節碼,能夠得出最後的運行結果是:
以上例子來自於《深刻理解Java虛擬機 JVM高級特性與最佳實踐》 關於虛擬機字節碼指令表,也能夠在《深刻理解Java虛擬機 JVM高級特性與最佳實踐-附錄B》中獲取。
kotlin提供了擴展函數的語言特性,藉助這個特性,咱們能夠給任意對象添加自定義方法。 如下示例爲Object添加"sayHello"方法
//SayHello.kt
package com.rhythm7
fun Any.sayHello() {
println("Hello")
}
複製代碼
編譯後,使用javap查看生成SayHelloKt.class文件的字節碼。
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/SayHelloKt.class
Last modified 2018-4-8; size 958 bytes
MD5 checksum 780a04b75a91be7605cac4655b499f19
Compiled from "SayHello.kt"
public final class com.rhythm7.SayHelloKt
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
//省略常量池部分字節碼
{
public static final void sayHello(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: ldc #9 // String $receiver
3: invokestatic #15 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: ldc #17 // String Hello
8: astore_1
9: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_1
13: invokevirtual #28 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
16: return
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 $receiver Ljava/lang/Object;
LineNumberTable:
line 4: 6
line 5: 16
RuntimeInvisibleParameterAnnotations:
0:
0: #7()
}
SourceFile: "SayHello.kt"
複製代碼
觀察頭部發現,koltin爲文件SayHello生成了一個類,類名"com.rhythm7.SayHelloKt". 因爲咱們一開始編寫SayHello.kt時並不但願SayHello是一個可實例化的對象類,因此,SayHelloKt是沒法被實例化的,SayHelloKt並無任何一個構造器。 再觀察惟一的一個方法:發現Any.sayHello()
的具體實現是靜態不可變方法的形式:
public static final void sayHello(java.lang.Object);
複製代碼
因此當咱們在其餘地方使用Any.sayHello()
時,事實上等同於調用java的SayHelloKt.sayHello(Object)
方法。 順便一提的是,當擴展的方法爲Any時,意味着Any是non-null的,這時,編譯器會在方法體的開頭檢查參數的非空,即調用 kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(Object value, String paramName)
方法來檢查傳入的Any類型對象是否爲空。若是咱們擴展的函數爲Any?.sayHello()
,那麼在編譯後的文件中則不會有這段字節碼的出現。