1. 程式人生 > >Java虛擬機器學習入門

Java虛擬機器學習入門

1      前言

想深入瞭解Java,虛擬機器是必須掌握的技能,任何一個Java程式都離不開虛擬機器,對於初學者瞭解JVM也可以更好的理解Java的初始化、記憶體使用等知識點。總結了一下自己在學習虛擬機器過程中的一些知識點,整理了一下筆記,有不妥和不當之處歡迎Java愛好者批評指正,也歡迎各位來自五湖四海的朋友交流任何問題。

Java虛擬機器(Java Virtual Machine) 簡稱JVM,Java虛擬機器是一個想象的虛擬機器器,通過在實際計算機上的軟體模擬來實現,一個Java虛擬機器包括一套位元組碼指令集、一組暫存器、一個棧、一個垃圾回收堆和一個儲存方法域。Java程式通過這個虛擬的機器,載入程式碼,管理程式碼存放位置,執行程式碼。換句話說,JVM= 類載入器

(classloader)+ 執行引擎(execution engine )+ 執行時資料區域 (runtime data areaclassloader)。我個人的理解,JVM的核心部分就是程式碼儲存在哪裡以及程式碼是如何執行的,為了更好的理解JVM的執行機制,首先看看JVM的記憶體管理

2      Java虛擬機器記憶體管理

大多數的Java虛擬機器把虛擬機器的記憶體分為程式計數器(Program CounterRegister)、堆區(Heap)、虛擬機器棧(VMStack)、本地方法棧(Native Method Stack)、方法區(Method Area)、執行時常量池(Runtime Constant Pool)等,如下表。

表 1JVM記憶體分配


2.1      程式計數器

程式計數器作用可以看作當前執行緒所執行的位元組碼行號的指示器,位元組碼直譯器工作時就是通過改變計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都依賴這個計數器來完成。

程式計數器是一種暫存器,在計算機世界裡暫存器作為cpu的重要組成部分,用來暫存指令、資料和地址等資訊。是記憶體層次結構中的最頂端,也是系統操作資料的最快途徑,基本單元為觸發器。

除了程式計數器,JVM還設定了另外3個常用的暫存器。它們是:optop運算元棧頂指標 ,frame當前執行環境指標, vars指向當前執行環境中第一個區域性變數的指標, 所有暫存器均為32位。pc用於記錄程式的執行。optop,frame和vars用於記錄指向Java棧區的指標。

該區域也是虛擬機器記憶體中唯一一塊沒有規定任何異常的區域。

2.2     Java虛擬機器棧

棧記憶體屬於單個執行緒,每個執行緒都會有一個棧記憶體,即棧記憶體可以理解為執行緒的私有記憶體。

虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀用於儲存區域性變量表、操作棧、動態連結和方法出口等資訊。每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器中從入棧到出棧的過程。具體來講就是當JVM得到一個Java位元組碼應用程式後,便為該程式碼中一個類的每一個方法建立一個棧框架,以儲存該方法的狀態資訊。每個棧框架包括以下三類資訊:區域性變數執行環境運算元棧,區域性變數用於儲存一個類的方法中所用到的區域性變數。vars暫存器指向該變量表中的第一個區域性變數。執行環境用於儲存直譯器對Java位元組碼進行解釋過程中所需的資訊。它們是:上次呼叫的方法、區域性變數指標和運算元棧的棧頂和棧底指標。執行環境是一個執行一個方法的控制中心。例如:如果直譯器要執行iadd(整數加法),首先要從frame暫存器中找到當前執行環境,而後便從執行環境中找到運算元棧,從棧頂彈出兩個整數進行加法運算,最後將結果壓入棧頂。運算元棧用於儲存運算所需運算元及運算的結果。

棧記憶體沒有可用的空間儲存方法呼叫和區域性變數,JVM丟擲Java.lang.StackOverFlowError

-Xss設定棧記憶體大小,棧記憶體遠遠小於堆記憶體,如果使用遞迴的話,不及時跳出很容易發生StackOverFlowError問題。

棧記憶體存放基本型別的變數資料和物件的引用、區域性變數,存取方式僅次於暫存器,String a=”abc”,是個例外,存放在棧中如果沒有,則開闢一個存放字面值為"abc"的地址,接著建立一個新的String類的物件o,並將o 的字串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的物件o。如果已經有了值為"abc"的地址,則查詢物件o,並返回o的地址。 

2.3     本地方法棧

本地方法棧與虛擬機器棧所發揮的作用非常相似,虛擬機器棧為虛擬機器執行的Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。有的虛擬機器(如sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。

本地方法棧與虛擬機器棧一樣,也會丟擲StackOverFlowError和OutOfMemoryError

2.4     Java

Java堆即JVM碎片回收堆,執行緒共享的,在虛擬機器啟動時建立。此區域唯一的目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。Java堆是立即收集器管理的主要區域,因此也就“GC堆”

Java類的例項所需的儲存空間是在堆上分配的。直譯器具體承擔為類例項分配空間的工作。直譯器在為一個例項分配完儲存空間後,便開始記錄對該例項所佔用的記憶體區域的使用。一旦物件使用完畢,便將其回收到堆中。在Java語言中,除了new語句外沒有其他方法為一物件申請和釋放記憶體。對記憶體進行釋放和回收的工作是由Java執行系統承擔的。這允許Java執行系統的設計者自己決定碎片回收的方法。在SUN公司開發的Java直譯器和Hot Java環境中,碎片回收用後臺執行緒的方式來執行。這不但為執行系統提供了良好的效能,而且使程式設計人員擺脫了自己控制記憶體使用的風險。

堆記憶體沒有可用的空間儲存生成物件,JVM會丟擲Java.lang.OutOfMemoryError

—Xms設定堆的初始大小 —設定堆的最大值

如果再進一步細分堆分為新生代和老年代,更多詳細內容這裡不再贅述。

2.5     方法區

方法區與Java堆一樣是各個執行緒共享的區域,用於儲存已經被虛擬機器載入的類資訊(即載入類時需要載入的資訊,包括版本、field、方法、介面等資訊)、final常量、靜態變數、編譯器即時編譯的程式碼等。Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但它卻有一個別名叫做Non-Heap(非堆)。同樣,根據Java虛擬機器規範,當此區域無法滿足記憶體分配時,丟擲OutOfMemoryError

2.6     執行時常量緩衝池(runtime constant pool

執行時常量池是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項是常量池,用於存放編譯期生產的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。

執行時常量池(Runtime ConstantPool)是方法區的一部分,用於儲存編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字串表示某個變數、介面的位置,直接引用就是根據符號引用翻譯出來的地址,將在類連結階段完成翻譯);執行時常量池除了儲存編譯期常量外,也可以儲存在執行時間產生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果呼叫的字元“abc”已經在常量池中,則返回池中的字串地址,否則,新建一個常量加入池中,並返回地址)

2.7     直接記憶體

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但是這一部分也被頻繁使用,也可能導致OutOfMemoryError異常出現。例如,JDK1.4中新加入的NIO類,引入了一種基於通道與緩衝區的I/O方式,使用的就是堆外記憶體,但既然是記憶體,肯定會受到本機總記憶體的大小及處理器定址空間的限制。

3      類和物件的生命週期

3.1     類的生命週期

 

圖1 類的生命週期

3.1.1     載入

類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個Java.lang.Class物件,用來封裝類在方法區內的資料結構 。這裡的class物件其實就像一面鏡子一樣,外面是類的源程式,裡面是class物件,它實時的反應了類的資料結構和資訊。把硬碟上的class 檔案載入到JVM中的執行時資料區域, 但是它不負責這個類檔案能否執行,而這個是 執行引擎負責的

不同的JVM對於類的裝載時機並不相同,有些在遇到這個類時就裝載這個類(雖然並不知道這個類是否會被用到),另一些則在真正用到一個類的時候才對它進行裝載。

表 2類的載入


3.1.2     連線

(1)驗證

驗證是連線階段的第一步,其目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器的安全,如果驗證失敗,會丟擲Java.lang.VerifyError異常。

表 3驗證主要工作


(2)準備

準備階段:為類的靜態變數分配記憶體並設為JVM預設的初值,對於非靜態變數則不會分配記憶體。基本型別預設值為0,引用型別預設值為null,常量型別預設值為程式中設定值,這些記憶體都將在方法區中進行分配。

對於普通非final的類變數,如public static int value = 123;在準備階段過後的初始值是0(資料型別的零值),而不是123,而把123賦值給value是在初始化階段才進行的動作。

對於final的類變數,即常量,如public staticfinal int value =123;在準備階段過程的初始值直接就是123了,不需要準備為零值。

(3)解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

符號引用(SymbolicReference):以一組符號來描述所引用的目標,與虛擬機器記憶體佈局無關,引用的目標不一定已經被載入到虛擬機器記憶體中。

直接引用(DirectReference):可以直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的控制代碼。直接引用和虛擬機器實現的記憶體佈局相關,同一個符號引用在不同虛擬機器上翻譯處理的直接引用不一定相同,如果有了直接引用,則引用的目標物件必須已經被載入到虛擬機器記憶體中。

解析的動作主要針對類或介面、欄位、類方法、介面方法四類符號引用進行解析。

3.1.3     初始化

初始化是類使用前的最後一個階段,在初始化階段Java虛擬機器真正開始執行類中定義的Java程式程式碼。初始化:只會初始化與類相關的靜態賦值語句和靜態語句,而沒有static修飾的賦值語句和執行語句在例項化物件的時候才會執行。如果一個類被直接引用,就會觸發類的初始化。

表 4主動引用和被動引用


如果一個類被直接引用,而物件沒有初始化時,就會觸發類的初始化

初始化的過程其實就是一個執行類構造器<clint>方法的過程,類構造器執行的特點和注意事項:

(1)類構造器<clint>方法是由編譯器自動收集類中所有類變數(靜態非final變數)賦值動作和靜態初始化塊(static{……})中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序決定。靜態初始化塊中只能訪問到定義在它之前的類變數,定義在它之後的類變數,在前面的靜態初始化中可以賦值,但是不能訪問。

(2)類構造器<clint>方法與例項構造器<init>方法不同,它不需要顯式地呼叫父類構造器方法,虛擬機器會保證在呼叫子類構造器方法之前,父類的構造器<clinit>方法已經執行完畢。

(3)由於父類構造器<clint>方法先與子類構造器執行,因此父類中定義的靜態初始化塊要先於子類的類變數賦值操作。

(4) 類構造器<clint>方法對於類和介面並不是必須的,如果一個類中沒有靜態初始化塊,也沒有類變數賦值操作,則編譯器可以不為該類生成類構造器<clint>方法。

(5)介面中不能使用靜態初始化塊,但可以有類變數賦值操作,因此介面與類一樣都可以生成類構造器<clint>方法。介面與類不同的是:

首先,執行介面的類構造器<clint>方法時不需要先執行父介面的類構造器<clint>方法,只有當父介面中定義的靜態變數被使用時,父接口才會被初始化。

其次,介面的實現類在初始化時同樣不會執行介面的類構造器<clint>方法。

(6)Java虛擬機器會保證一個類的<clint>方法在多執行緒環境中被正確地加鎖和同步,如果多個執行緒同時去初始化一個類,只會有一個執行緒去執行這個類的<clint>方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clint>方法完畢。

初始化階段,當執行完類構造器<clint>方法之後,才會執行例項構造器的<init>方法,例項構造方法同樣是按照先父類,後子類,先成員變數,後例項構造方法的順序執行。

JVM在類初始化完成後,根據類的資訊在堆區例項化類物件,初始化非靜態變數和預設構造方法。

說到這裡,上一節的問題應該可以解決了吧,父類的非靜態成員變數在物件例項化的時候進行賦值。

3.1.4     使用

當初始化完成之後,Java虛擬機器就可以執行Class的業務邏輯指令,通過堆Java.lang.Class物件的入口地址,呼叫方法區的方法邏輯,最後將方法的運算結果通過方法返回地址存放到方法區或堆中。使用階段包括主動引用和被動引用,主動飲用會引起類的初始化,而被動引用不會引起類的初始化。

3.1.5     解除安裝

當物件不再被使用時,Java虛擬機器的垃圾收集器將會回收堆中的物件,方法區中不再被使用的Class也要被解除安裝,否則方法區(Sun HotSpot永久代)會記憶體溢位。

Java虛擬機器規定只有當載入該型別的類載入器例項為unreachable狀態時,當前被載入的型別才被解除安裝.啟動類載入器例項永遠為reachable狀態,由啟動類載入器載入的型別可能永遠不會被解除安裝,型別解除安裝僅僅是作為一種減少記憶體使用的效能優化措施存在的,具體和虛擬機器實現有關,對開發者來說是透明的.
如果下面的所有情況都成立,類將會被解除安裝:
(1)類所有的例項都已經被回收。(即堆中不存在該類的任何例項)
(2)載入該類的ClassLoader被回收。
(3)該類對應的Java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
JVM在方法區垃圾回收的時候對類進行解除安裝,在方法區中清空類資訊。
至此,一個Java類的生命週期結束。

3.2     物件的生命週期

物件的生命週期只是類的生命週期中使用階段主動引用的一種情況(例項化物件),Java物件是在JVM的堆區建立的,在建立物件之前,可能會觸發類的載入、連線和初始化。
由於Java在堆上建立物件,因此編譯器對物件的生命週期一無所知。Java提供了垃圾回收器機制,JVM會在空閒時間以不定時的方式動態回收無任何引用的物件佔據的記憶體空間。 

表 5物件的生命週期


3.3     執行引擎

執行引擎是Java虛擬機器最核心的組成部分之一,“虛擬機器”是一個相對於“物理機”的概念,這兩種機器都有程式碼執行能力,區別是物理機的執行引擎是直接建立在處理器、硬體、指令集和作業系統層面的,而虛擬機器的執行引擎是自己實現的。在不同的虛擬機器實現裡面,執行引擎在執行Java程式碼的時候可能有解釋執行(通過直譯器執行)和編譯執行(通過即時編譯器產生的原生代碼執行)兩種選擇,但從外觀看起來,所有的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直譯器程式的平臺下執行。

小結:

1.類的成員變數在不同物件中,都有自己的儲存空間,在堆記憶體中的類的例項中;類的方法卻是該類的所有物件共享的,存在與方法區的位元組碼,不使用則不佔記憶體;方法的呼叫是通過堆記憶體中的方法入口訪問方法區的方法位元組碼。

2.執行緒私有的記憶體區域隨著線性的終結記憶體釋放,沒有自動垃圾回收,而執行緒共享的區域則是垃圾回收的區域。

3.常量在編譯期放入方法區的類的常量池中,靜態成員變數在初始化的時候賦值,非靜態成員變數在物件例項化的時候賦值。