天天看點

JVM學習筆記(1)

Java虛拟機(Java Virtual Machine) 簡稱JVM,Java虛拟機是一個想象的虛拟機器,通過在實際計算機上的軟體模拟來實作,一個Java虛拟機包括一套位元組碼指令集、一組寄存器、一個棧、一個垃圾回收堆和一個存儲方法域。Java程式通過這個虛拟的機器,加載代碼,管理代碼存放位置,執行代碼。換句話說,JVM= 類加載器 (classloader)+ 執行引擎(execution engine )+ 運作時資料區域 (runtime data areaclassloader)。我個人的了解,JVM的核心部分就是代碼存儲在哪裡以及代碼是如何運作的,為了更好的了解JVM的運作機制,首先看看JVM的記憶體管理。

JVM虛拟機記憶體管理

虛拟機的記憶體分為程式計數器(Program CounterRegister)、堆區(Heap)、虛拟機棧(VMStack)、本地方法棧(Native Method Stack)、方法區(Method Area)、運作時常量池(Runtime Constant Pool)等,如下圖

JVM學習筆記(1)

程式計數器

程式計數器作用可以看作目前線程所執行的位元組碼行号的訓示器,位元組碼解釋器工作時就是通過改變計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都依賴這個計數器來完成。

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

除了程式計數器,JVM還設定了另外3個常用的寄存器。它們是:optop操作數棧頂指針 ,frame目前執行環境指針, vars指向目前執行環境中第一個局部變量的指針, 所有寄存器均為32位。pc用于記錄程式的執行。optop,frame和vars用于記錄指向Java棧區的指針。

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虛拟機)直接就把本地方法棧和虛拟機棧合二為一。

Java堆

Java堆即JVM碎片回收堆,線程共享的,在虛拟機啟動時建立。此區域唯一的目的就是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體。Java堆是立即收集器管理的主要區域,是以也就“GC堆”

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

堆記憶體沒有可用的空間存儲生成對象,JVM會抛出Java.lang.OutOfMemoryError

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

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

方法區

方法區與Java堆一樣是各個線程共享的區域,用于存儲已經被虛拟機加載的類資訊(即加載類時需要加載的資訊,包括版本、field、方法、接口等資訊)、final常量、靜态變量、編譯器即時編譯的代碼等。Java虛拟機規範把方法區描述為堆的一個邏輯部分,但它卻有一個别名叫做Non-Heap(非堆)。同樣,根據Java虛拟機規範,當此區域無法滿足記憶體配置設定時,抛出OutOfMemoryError

運作時常量緩沖池

運作時常量池是方法區的一部分,Class檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有一項是常量池,用于存放編譯期生産的各種字面量和符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中。

運作時常量池(Runtime ConstantPool)是方法區的一部分,用于存儲編譯期就生成的字面常量、符号引用、翻譯出來的直接引用(符号引用就是編碼是用字元串表示某個變量、接口的位置,直接引用就是根據符号引用翻譯出來的位址,将在類連結階段完成翻譯);運作時常量池除了存儲編譯期常量外,也可以存儲在運作時間産生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果調用的字元“abc”已經在常量池中,則傳回池中的字元串位址,否則,建立一個常量加入池中,并傳回位址)。

直接記憶體

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

類和對象的生命周期

1.類生命周期

JVM學習筆記(1)

2.加載

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

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

JVM學習筆記(1)

 連接配接

(1)驗證

驗證是連接配接階段的第一步,其目的是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機的安全,如果驗證失敗,會抛出Java.lang.VerifyError異常。

JVM學習筆記(1)

(2)準備

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

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

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

(3)解析

解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程。

符号引用(SymbolicReference):以一組符号來描述所引用的目标,與虛拟機記憶體布局無關,引用的目标不一定已經被加載到虛拟機記憶體中。

直接引用(DirectReference):可以直接指向目标的指針、相對偏移量或者是一個能間接定位到目标的句柄。直接引用和虛拟機實作的記憶體布局相關,同一個符号引用在不同虛拟機上翻譯處理的直接引用不一定相同,如果有了直接引用,則引用的目标對象必須已經被加載到虛拟機記憶體中。

解析的動作主要針對類或接口、字段、類方法、接口方法四類符号引用進行解析。

 初始化

初始化是類使用前的最後一個階段,在初始化階段Java虛拟機真正開始執行類中定義的Java程式代碼。初始化:隻會初始化與類相關的靜态指派語句和靜态語句,而沒有static修飾的指派語句和執行語句在執行個體化對象的時候才會運作。如果一個類被直接引用,就會觸發類的初始化。

JVM學習筆記(1)

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

初始化的過程其實就是一個執行類構造器<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在類初始化完成後,根據類的資訊在堆區執行個體化類對象,初始化非靜态變量和預設構造方法。

說到這裡,上一節的問題應該可以解決了吧,父類的非靜态成員變量在對象執行個體化的時候進行指派。

使用

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

解除安裝

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

Java虛拟機規定隻有當加載該類型的類加載器執行個體為unreachable狀态時,目前被加載的類型才被解除安裝.啟動類加載器執行個體永遠為reachable狀态,由啟動類加載器加載的類型可能永遠不會被解除安裝,類型解除安裝僅僅是作為一種減少記憶體使用的性能優化措施存在的,具體和虛拟機實作有關,對開發者來說是透明的.

如果下面的所有情況都成立,類将會被解除安裝:

(1)類所有的執行個體都已經被回收。(即堆中不存在該類的任何執行個體)

(2)加載該類的ClassLoader被回收。

(3)該類對應的Java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射通路該類的方法。

JVM在方法區垃圾回收的時候對類進行解除安裝,在方法區中清空類資訊。

至此,一個Java類的生命周期結束

 對象的生命周期

對象的生命周期隻是類的生命周期中使用階段主動引用的一種情況(執行個體化對象),Java對象是在JVM的堆區建立的,在建立對象之前,可能會觸發類的加載、連接配接和初始化。

由于Java在堆上建立對象,是以編譯器對對象的生命周期一無所知。Java提供了垃圾回收器機制,JVM會在空閑時間以不定時的方式動态回收無任何引用的對象占據的記憶體空間。

JVM學習筆記(1)

  執行引擎

執行引擎是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解釋器程式的平台下運作。