第1章:認識Java

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png
JVM是Java Virtual Machine(Java虛擬機器)的縮寫,JVM是一種用於計算裝置的規範,它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。
Java語言的一個非常重要的特點就是與平臺的無關性。而使用Java虛擬機器是實現這一特點的關鍵。一般的高階語言如果要在不同的平臺上執行,至少需要編譯成不同的目的碼。而引入Java語言虛擬機器後,Java語言在不同平臺上執行時不需要重新編譯。Java語言使用Java虛擬機器遮蔽了與具體平臺相關的資訊,使得Java語言編譯程式只需生成在Java虛擬機器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。Java虛擬機器在執行位元組碼時,把位元組碼解釋成具體平臺上的機器指令執行。這就是Java的能夠“一次編譯,到處執行”的原因。
JVM基本資料型別
short://2位元組有符號整數的補碼
int://4位元組有符號整數的補碼
long://8位元組有符號整數的補碼
float://4位元組IEEE754單精度浮點數
double://8位元組IEEE754雙精度浮點數
char://2位元組無符號Unicode字元
boolean:boolean資料型別表示一位的資訊

image.png
幾乎所有的Java型別檢查都是在編譯時完成的。上面列出的原始資料型別的資料在Java執行時不需要用硬體標記。操作這些原始資料型別資料的位元組碼(指令)本身就已經指出了運算元的資料型別,例如iadd、ladd、fadd和dadd指令都是把兩個數相加,其運算元型別分別是int、long、float和double。虛擬機器沒有給boolean(布林)型別設定單獨的指令。boolean型的資料是由integer指令,包括integer返回來處理的。boolean型的陣列則是用byte陣列來處理的。虛擬機器使用IEEE754格式的浮點數。不支援IEEE格式的較舊的計算機,在執行Java數值計算程式時,可能會非常慢。
其它資料型別
object//對一個Javaobject(物件)的4位元組引用
returnAddress//4位元組,用於jsr/ret/jsr-w/ret-w指令
注:Java陣列被當做object處理。
JVM
虛擬機器的規範對於object內部的結構沒有任何特殊的要求。在Sun公司的實現中,對object的引用是一個控制代碼,其中包含一對指標:一個指標指向該object的方法表,另一個指向該object的資料。用Java虛擬機器的位元組碼錶示的程式應該遵守型別規定。Java虛擬機器的實現應拒絕執行違反了型別規定的位元組碼程式。Java虛擬機器由於位元組碼定義的限制似乎只能運行於32位地址空間的機器上。但是可以建立一個Java虛擬機器,它自動地把位元組碼轉換成64位的形式。從Java虛擬機器支援的資料型別可以看出,Java對資料型別的內部格式進行了嚴格規定,這樣使得各種Java虛擬機器的實現對資料的解釋是相同的,從而保證了Java的與平臺無關性和可移植性。
規格編輯
JVM的設計目標是提供一個基於抽象規格描述的計算機模型,為解釋程式開發人員提供很好的靈活性,同時也確保Java程式碼可在符合該規範的任何系統上執行。JVM對其實現的某些方面給出了具體的定義,特別是對Java可執行程式碼,即位元組碼(Bytecode)的格式給出了明確的規格。這一規格包括操作碼和運算元的語法和數值、識別符號的數值表示方式、以及Java類檔案中的Java物件、常量緩衝池在JVM的儲存映象。這些定義為JVM直譯器開發人員提供了所需的資訊和開發環境。Java的設計者希望給開發人員以隨心所欲使用Java的自由。
JVM定義了控制Java程式碼解釋執行和具體實現的五種規格,它們是:
JVM指令系統
JVM暫存器
JVM 棧結構
JVM 碎片回收堆
JVM 儲存區
原理編輯
JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種基於下層的作業系統和硬體平臺並利用軟體方法來實現的抽象的計算機,可以在上面執行java的位元組碼程式。
JVM執行原理
JVM執行原理 [1]
java編譯器只需面向JVM,生成JVM能理解的程式碼或位元組碼檔案。Java原始檔經編譯器,編譯成位元組碼程式,通過JVM將每一條指令翻譯成不同平臺機器碼,通過特定平臺執行。
JVM執行程式的過程 :
I.載入.class檔案
II.管理並分配記憶體
III.執行垃圾收集
JRE(java執行時環境)包含JVM的java程式的執行環境 [1]
JVM是Java程式執行的容器,但是他同時也是作業系統的一個程序,因此他也有他自己的執行的生命週期,也有自己的程式碼和資料空間。
JVM在整個jdk中處於最底層,負責與作業系統的互動,用來遮蔽作業系統環境,提供一個完整的Java執行環境,因此也叫虛擬計算機.作業系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境。
1.建立JVM裝載環境和配置
2.裝載JVM.dll
3.初始化JVM.dll並掛接到JNIENV(JNI呼叫介面)例項
4.呼叫JNIEnv例項裝載並處理class類。 [2]
指令系統編輯
JVM指令系統同其他計算機的指令系統極其相似。Java指令也是由操作碼和運算元兩部分組
JVM
操作碼為8位二進位制數,運算元緊隨在操作碼的後面,其長度根據需要而不同。操作碼用於指定一條指令操作的性質(在這裡我們採用彙編符號的形式進行說明),如iload表示從儲存器中裝入一個整數,anewarray表示為一個新陣列分配空間,iand表示兩個整數的"與",ret用於流程控制,表示從對某一方法的呼叫中返回。當長度大於8位時,運算元被分為兩個以上位元組存放。JVM採用了"big endian [3] "的編碼方式來處理這種情況,即高位bits存放在低位元組中。這同 Motorola及其他的RISC CPU採用的編碼方式是一致的,而與Intel採用的"little endian "的編碼方式即低位bits存放在低位位元組的方法不同。Java指令系統是以Java語言的實現為目的設計的,其中包含了用於呼叫方法和監視多執行緒系統的指令。Java的8位操作碼的長度使得JVM最多有256種指令,已使用了160多種操作碼。
暫存器編輯
所有的CPU均包含用於儲存系統狀態和處理器所需資訊的暫存器組。如果虛擬機器定義較多的暫存器,便可以從中得到更多的資訊而不必對棧或記憶體進行訪問,這有利於提高執行速度。然而,如果虛擬機器中的暫存器比實際CPU的暫存器多,在實現虛擬機器時就會佔用處理器大量的時間來用常規儲存器模擬暫存器,這反而會降低虛擬機器的效率。針對這種情況,JVM只設置了4個最為常用的暫存器。它們是:
pc程式計數器
optop運算元棧頂指標
frame當前執行環境指標
vars指向當前執行環境中第一個區域性變數的指標
所有暫存器均為32位。pc用於記錄程式的執行。optop,frame和vars用於記錄指向Java棧區的指標。
棧結構編輯
作為基於棧結構的計算機,Java棧是JVM儲存資訊的主要方法。當JVM得到一個Java位元組碼應用程式後,便為該程式碼中一個類的每一個方法建立一個棧框架,以儲存該方法的狀態資訊。每個棧框架包括以下三類資訊:
區域性變數
執行環境
運算元棧
區域性變數用於儲存一個類的方法中所用到的區域性變數。vars暫存器指向該變量表中的第一個區域性變數。
執行環境用於儲存直譯器對Java位元組碼進行解釋過程中所需的資訊。它們是:上次呼叫的方法、區域性變數指標和運算元棧的棧頂和棧底指標。執行環境是一個執行一個方法的控制中心。例如:如果直譯器要執行iadd(整數加法),首先要從frame暫存器中找到當前執行環境,而後便從執行環境中找到運算元棧,從棧頂彈出兩個整數進行加法運算,最後將結果壓入棧頂。
運算元棧用於儲存運算所需運算元及運算的結果。
碎片回收編輯
Java類的例項所需的儲存空間是在堆上分配的。直譯器具體承擔為類例項分配空間的工作。直譯器在為一個例項分配完儲存空間後,便開始記錄對該例項所佔用的記憶體區域的使用。一旦物件使用完畢,便將其回收到堆中。在Java語言中,除了new語句外沒有其他方法為一物件申請和釋放記憶體。對記憶體進行釋放和回收的工作是由Java執行系統承擔的。這允許Java執行系統的設計者自己決定碎片回收的方法。在SUN公司開發的Java直譯器和Hot Java環境中,碎片回收用後臺執行緒的方式來執行。這不但為執行系統提供了良好的效能,而且使程式設計人員擺脫了自己控制記憶體使用的風險。

image.png
儲存區編輯
JVM有兩類儲存區:常量緩衝池和方法區。常量緩衝池用於儲存類名稱、方法和欄位名稱以及串常量。方法區則用於儲存Java方法的位元組碼。對於這兩種儲存區域具體實現方式在JVM規格中沒有明確規定。這使得Java應用程式的儲存佈局必須在執行過程中確定,依賴於具體平臺的實現方式。JVM是為Java位元組碼定義的一種獨立於具體平臺的規格描述,是Java平臺獨立性的基礎。JVM還存在一些限制和不足,有待於進一步的完善,但無論如何,JVM的思想是成功的。
對比分析:如果把Java原程式想象成我們的C++原程式,Java原程式編譯後生成的位元組碼就相當於C++原程式編譯後的80x86的機器碼(二進位制程式檔案),JVM虛擬機器相當於80x86計算機系統,Java直譯器相當於80x86CPU。在80x86CPU上執行的是機器碼,在Java直譯器上執行的是Java位元組碼。Java直譯器相當於執行Java位元組碼的“CPU”,但該“CPU”不是通過硬體實現的,而是用軟體實現的。Java直譯器實際上就是特定的平臺下的一個應用程式。只要實現了特定平臺下的直譯器程式,Java位元組碼就能通過直譯器程式在該平臺下執行,這是Java跨平臺的根本。當前,並不是在所有的平臺下都有相應Java直譯器程式,這也是Java並不能在所有的平臺下都能執行的原因,它只能在已實現了Java直譯器程式的平臺下執行。
執行資料編輯
JVM定義了若干個程式執行期間使用的資料區域。這個區域裡的一些資料在JVM啟動的時候建立,在JVM退出的時候銷燬。而其他的資料依賴於每一個執行緒,線上程建立時建立,線上程退出時銷燬。分別有程式計數器,堆,棧,方法區,執行時常量池。

image.png
體系結構編輯
JVM可以由不同的廠商來實現。由於廠商的不同必然導致JVM在實現上的一些不同,然而JVM還是可以實現跨平臺的特性,這就要歸功於設計JVM時的體系結構了。我們知道,一個JVM例項的行為不光是它自己的事,還涉及到它的子系統、儲存區域、資料型別和指令這些部分,它們描述了JVM的一個抽象的內部體系結構,其目的不光規定實現JVM時它內部的體系結構,更重要的是提供了一種方式,用於嚴格定義實現時的外部行為。每個JVM都有兩種機制,一個是裝載具有合適名稱的類(類或是介面),叫做類裝載子系統;另外的一個負責執行包含在已裝載的類或介面中的指令,叫做執行引擎。每個JVM又包括方法區、堆、Java棧、程式計數器和本地方法棧這五個部分,這幾個部分和類裝載機制與執行引擎機制一起組成的體系結構圖為:
JVM體系結構

image.png
JVM體系結構
JVM的每個例項都有一個它自己的方法域和一個堆,運行於JVM內的所有的執行緒都共享這些區域;當虛擬機器裝載類檔案的時候,它解析其中的二進位制資料所包含的類資訊,並把它們放到方法域中;當程式執行的時候,JVM把程式初始化的所有物件置於堆上;而每個執行緒建立的時候,都會擁有自己的程式計數器和Java棧,其中程式計數器中的值指向下一條即將被執行的指令,執行緒的Java棧則儲存為該執行緒呼叫Java方法的狀態;本地方法呼叫的狀態被儲存在本地方法棧,該方法棧依賴於具體的實現。
下面分別對這幾個部分進行說明。
執行引擎處於JVM的核心位置,在Java虛擬機器規範中,它的行為是由指令集所決定的。儘管對於每條指令,規範很詳細地說明了當JVM執行位元組碼遇到指令時,它的實現應該做什麼,但對於怎麼做卻言之甚少。Java虛擬機器支援大約248個位元組碼。每個位元組碼執行一種基本的CPU運算,例如,把一個整數加到暫存器,子程式轉移等。Java指令集相當於Java程式的組合語言。Java指令集中的指令包含一個單位元組的操作符,用於指定要執行的操作,還有0個或多個運算元,提供操作所需的引數或資料。許多指令沒有運算元,僅由一個單位元組的操作符構成。
虛擬機器的內層迴圈的執行過程如下:
do{
取一個操作符位元組;
根據操作符的值執行一個動作;
}while(程式未結束)
由於指令系統的簡單性,使得虛擬機器執行的過程十分簡單,從而有利於提高執行的效率。指令中運算元的數量和大小是由操作符決定的。如果運算元比一個位元組大,那麼它儲存的順序是高位位元組優先。例如,一個16位的引數存放時佔用兩個位元組,其值為:
第一個位元組*256+第二個位元組位元組碼。
指令流一般只是位元組對齊的。指令tableswitch和lookup是例外,在這兩條指令內部要求強制的4位元組邊界對齊。對於本地方法介面,實現JVM並不要求一定要有它的支援,甚至可以完全沒有。Sun公司實現Java本地介面(JNI [3] )是出於可移植性的考慮,當然我們也可以設計出其它的本地介面來代替Sun公司的JNI [4] 。但是這些設計與實現是比較複雜的事情,需要確保垃圾回收器不會將那些正在被本地方法呼叫的物件釋放掉。
Java的堆是一個執行時資料區,類的例項(物件)從中分配空間,它的管理是由垃圾回收來負責的:不給程式設計師顯式釋放物件的能力。Java不規定具體使用的垃圾回收演算法,可以根據系統的需求使用各種各樣的演算法。
Java方法區與傳統語言中的編譯後代碼或是Unix程序中的正文段類似。它儲存方法程式碼(編譯後的java程式碼)和符號表。在當前的Java實現中,方法程式碼不包括在垃圾回收堆中,但計劃在將來的版本中實現。每個類檔案包含了一個Java類或一個Java介面的編譯後的程式碼。可以說類檔案是Java語言的執行程式碼檔案。為了保證類檔案的平臺無關性,Java虛擬機器規範中對類檔案的格式也作了詳細的說明。其具體細節請參考Sun公司的Java虛擬機器規範。
Java虛擬機器的暫存器用於儲存機器的執行狀態,與微處理器中的某些專用暫存器類似。Java虛擬機器的暫存器有四種:
pc: Java程式計數器;
optop: 指向運算元棧頂端的指標;
frame: 指向當前執行方法的執行環境的指標;。
vars: 指向當前執行方法的區域性變數區第一個變數的指標。
在上述體系結構圖中,我們所說的是第一種,即程式計數器,每個執行緒一旦被建立就擁有了自己的程式計數器。當執行緒執行Java方法的時候,它包含該執行緒正在被執行的指令的地址。但是若執行緒執行的是一個本地的方法,那麼程式計數器的值就不會被定義。
Java虛擬機器的棧有三個區域:區域性變數區、執行環境區、運算元區。
區域性變數區
每個Java方法使用一個固定大小的區域性變數集。它們按照與vars暫存器的字偏移量來定址。區域性變數都是32位的。長整數和雙精度浮點數佔據了兩個區域性變數的空間,卻按照第一個區域性變數的索引來定址。(例如,一個具有索引n的區域性變數,如果是一個雙精度浮點數,那麼它實際佔據了索引n和n+1所代表的儲存空間)虛擬機器規範並不要求在區域性變數中的64位的值是64位對齊的。虛擬機器提供了把區域性變數中的值裝載到運算元棧的指令,也提供了把運算元棧中的值寫入區域性變數的指令。
JRE和JVM的區別
JRE(JavaRuntimeEnvironment,Java執行環境),也就是Java平臺。所有的Java程式都要在JRE下才能執行。JDK的工具也是Java程式,也需要JRE才能執行。為了保持JDK的獨立性和完整性,在JDK的安裝過程中,JRE也是安裝的一部分。所以,在JDK的安裝目錄下有一個名為jre的目錄,用於存放JRE檔案。
JVM(JavaVirtualMachine,Java虛擬機器)是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。JVM有自己完善的硬體架構,如處理器、堆疊、暫存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平臺執行。使用JVM就是為了支援與作業系統無關,實現跨平臺。 [3]
執行環境區
在執行環境中包含的資訊用於動態連結,正常的方法返回以及異常捕捉。
動態連結
執行環境包括對指向當前類和當前方法的直譯器符號表的指標,用於支援方法程式碼的動態連結。方法的class檔案程式碼在引用要呼叫的方法和要訪問的變數時使用符號。動態連結把符號形式的方法呼叫翻譯成實際方法呼叫,裝載必要的類以解釋還沒有定義的符號,並把變數訪問翻譯成與這些變數執行時的儲存結構相應的偏移地址。動態連結方法和變數使得方法中使用的其它類的變化不會影響到本程式的程式碼。
正常的方法返回
如果當前方法正常地結束了,在執行了一條具有正確型別的返回指令時,呼叫的方法會得到一個返回值。執行環境在正常返回的情況下用於恢復呼叫者的暫存器,並把呼叫者的程式計數器增加一個恰當的數值,以跳過已執行過的方法呼叫指令,然後在呼叫者的執行環境中繼續執行下去。
異常捕捉
異常情況在Java中被稱作Error(錯誤)或Exception(異常),是Throwable類的子類,在程式中的原因是:①動態連結錯,如無法找到所需的class檔案。②執行時錯,如對一個空指標的引用。程式使用了throw語句。
當異常發生時,Java虛擬機器採取如下措施:
§ 檢查與當前方法相聯絡的catch子句表。每個catch子句包含其有效指令範圍,能夠處理的異常型別,以及處理異常的程式碼塊地址。
§ 與異常相匹配的catch子句應該符合下面的條件:造成異常的指令在其指令範圍之內,發生的異常型別是其能處理的異常型別的子型別。如果找到了匹配的catch子句,那麼系統轉移到指定的異常處理塊處執行;如果沒有找到異常處理塊,重複尋找匹配的catch子句的過程,直到當前方法的所有巢狀的catch子句都被檢查過。
§ 由於虛擬機器從第一個匹配的catch子句處繼續執行,所以catch子句表中的順序是很重要的。因為Java程式碼是結構化的,因此總可以把某個方法的所有的異常處理器都按序排列到一個表中,對任意可能的程式計數器的值,都可以用線性的順序找到合適的異常處理塊,以處理在該程式計數器值下發生的異常情況。
§ 如果找不到匹配的catch子句,那麼當前方法得到一個"未截獲異常"的結果並返回到當前方法的呼叫者,好像異常剛剛在其呼叫者中發生一樣。如果在呼叫者中仍然沒有找到相應的異常處理塊,那麼這種錯誤將被傳播下去。如果錯誤被傳播到最頂層,那麼系統將呼叫一個預設的異常處理塊。
運算元棧區
機器指令只從運算元棧中取運算元,對它們進行操作,並把結果返回到棧中。選擇棧結構的原因是:在只有少量暫存器或非通用暫存器的機器(如Intel486)上,也能夠高效地模擬虛擬機器的行為。運算元棧是32位的。它用於給方法傳遞引數,並從方法接收結果,也用於支援操作的引數,並儲存操作的結果。例如,iadd指令將兩個整數相加。相加的兩個整數應該是運算元棧頂的兩個字。這兩個字是由先前的指令壓進堆疊的。這兩個整數將從堆疊彈出、相加,並把結果壓回到運算元棧中。
每個原始資料型別都有專門的指令對它們進行必須的操作。每個運算元在棧中需要一個儲存位置,除了long和double型,它們需要兩個位置。運算元只能被適用於其型別的操作符所操作。例如,壓入兩個int型別的數,如果把它們當作是一個long型別的數則是非法的。在Sun的虛擬機器實現中,這個限制由位元組碼驗證器強制實行。但是,有少數操作(操作符dupe和swap),用於對執行時資料區進行操作時是不考慮型別的。
本地方法棧,當一個執行緒呼叫本地方法時,它就不再受到虛擬機器關於結構和安全限制方面的約束,它既可以訪問虛擬機器的執行期資料區,也可以使用本地處理器以及任何型別的棧。例如,本地棧是一個C語言的棧,那麼當C程式呼叫C函式時,函式的引數以某種順序被壓入棧,結果則返回給呼叫函式。在實現Java虛擬機器時,本地方法介面使用的是C語言的模型棧,那麼它的本地方法棧的排程與使用則完全與C語言的棧相同。
執行過程編輯
上面對虛擬機器的各個部分進行了比較詳細的說明,下面通過一個具體的例子來分析它的執行過程。
虛擬機器通過呼叫某個指定類的方法main啟動,傳遞給main一個字串陣列引數,使指定的類被裝載,同時連結該類所使用的其它的型別,並且初始化它們。新建一java原始檔並取名HelloApp.java,內容如下:
class HelloApp {
public static void main(String[] args) {
System.out.println("Hello World!");
for (int i = 0; i < args.length; i++ ) {
System.out.println(args);
}
}
}
在命令模式下輸入:javac HelloApp.java 進行編譯,這時同目錄下會產生一個編譯後的檔案:HelloApp.class
然後在命令列模式下鍵入:java HelloApp run virtual machine
將通過呼叫HelloApp的方法main來啟動java虛擬機器,傳遞給main一個包含三個字串"run"、"virtual"、"machine"的陣列。我們略述虛擬機器在執行HelloApp時可能採取的步驟。
JVM虛擬機器執行過程
JVM虛擬機器執行過程
開始試圖執行類HelloApp的main方法,發現該類並沒有被裝載,也就是說虛擬機器當前不包含該類的二進位制代表,於是虛擬機器使用ClassLoader試圖尋找這樣的二進位制代表。如果這個程序失敗,則丟擲一個異常。類被裝載後同時在main方法被呼叫之前,必須對類HelloApp與其它型別進行連結然後初始化。連結包含三個階段:檢驗,準備和解析。檢驗檢查被裝載的主類的符號和語義,準備則建立類或介面的靜態域以及把這些域初始化為標準的預設值,解析負責檢查主類對其它類或介面的符號引用,在這一步它是可選的。類的初始化是對類中宣告的靜態初始化函式和靜態域的初始化構造方法的執行。一個類在初始化之前它的父類必須被初始化。
總結:
JVM是java的虛擬機器,是一個解釋位元組碼檔案的直譯器;假如你成功啟動了一個java程式,那麼它的第一步就是由JVM將程式的class檔案解釋為機器能夠識別的指令。換言之,JVM無需自行啟動

image.png

image.png
半編譯半解釋的特點
先編譯成位元組 -----然後在對位元組解析

image.png

image.png

image.png

image.png
對應程式碼:
package com.neusoft.demo01; /** * 這是我們學到的第一個JAVA類 * public 公有的關鍵字(有特殊意義的單詞)都是小寫 * class類關鍵字 * HelloTest 類名 * 第一個類都可以建立一個入口的main方法,這個方法的作用可以輸出一些結果 * @author ttc * 文件註釋 */ public class HelloTest { //當前所有的程式都要寫在main方法中 public static void main(String[] args){ //輸出一句話(快捷鍵)syso alt+/ System.out.println("這是我們的第一個程式"); System.out.println("這是我們的第一個程式"); System.out.println("這是我們的第一個程式"); System.out.println("這是我們的第一個程式"); System.out.println("這是我們的第一個程式"); System.out.println("adfadsfadsf"); System.out.println("adfasdfadsf"); System.out.println("fffffffrfff"); } }