Java虛擬機會將內存分爲幾個不一樣的管理區,這些區域各自有各自的用途,根據不一樣的特色,承擔不一樣的任務以及在垃圾回收時運用不一樣的算法。整體分爲下面幾個部分:html
程序計數器(Program Counter Register)、JVM虛擬機棧(JVM Stacks)、本地方法棧(Native Method Stacks)、堆(Heap)、方法區(Method Area)java
jvm 管理的內存大體包括三種不一樣類型的內存區域:程序員
Permanent Generation space(永久保存區域)、Heap space(堆區域)、Java Stacks(Java棧)。算法
其中永久保存區域主要存放Class(類)和Meta的信息,Class第一次被 Load 的時候被放入 PermGen space區域,Class須要存儲的內容主要包括方法和靜態屬性。堆區域用來存放Class的實例(即對象),對象須要存儲的內容主要是非靜態屬性。每次用new建立一個對象實例後,對象實例存儲在堆區域中,這部分空間也被jvm的垃圾回收機制管理。而Java棧跟大多數編程語言包括彙編語言的棧功能類似,主要基本類型變量以及方法的輸入輸出參數。Java程序的每一個線程中都有一個獨立的堆棧。容易發生內存溢出問題的內存空間包括:Permanent Generation space和Heap space。編程
這是一塊比較小的內存,不在Ram上,而是直接劃分在CPU上的,程序員沒法直接操做它,它的做用是:JVM在解釋字節碼文件(.class)時(每次執行class裏面的方法其實就是調用JVM方法區裏面的字節碼指令),存儲當前線程所執行的字節碼的行號,只是一種概念模型,各類JVM所採用的方式不一樣,字節碼解釋器工做時,就是經過改變程序計數器的值來選取下一條要執行的指令,分支、循環、跳轉、等基礎功能都是依賴此技術區完成的。數組
還有一種狀況,就是咱們常說的Java多線程方面的,多線程就是經過現程輪流切換而達到的,同一時刻,一個內核只能執行一個指令,因此,對於每個程序來講,必須有一個計數器來記錄程序的執行進度,這樣,當現程恢復執行的時候,才能從正確的地方開始,因此,每一個線程都必須有一個獨立的程序計數器,這類計數器爲線程私有的內存。若是一個線程正在執行一個Java方法,則計數器記錄的是字節碼的指令的地址,若是執行的一個Native方法,則計數器的記錄爲空,此內存區是惟一一個在Java規範中沒有任何OutOfMemoryError狀況的區域。網絡
JVM虛擬機棧就是咱們常說的堆棧的棧(咱們經常把內存粗略分爲堆和棧),和程序計數器同樣,也是線程私有的,生命週期和線程同樣,每一個方法被執行的時候會產生一個棧幀,用於存儲局部變量表、動態連接、操做數、方法出口等信息。方法的執行過程就是棧幀在JVM中出棧和入棧的過程。局部變量表中存放的是各類基本數據類型,如boolean、byte、char、等8種,及引用類型(存放的是指向各個對象的內存地址),所以,它有一個特色:內存空間能夠在編譯期間就肯定,運行期不在改變。這個內存區域會有兩種可能的Java異常:StackOverFlowError和OutOfMemoryError。多線程
注意:Java 棧堆的區別與聯繫架構
當在一段代碼塊定義一個變量時,Java就在棧中爲這個變量分配內存空間,當超過變量的做用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間能夠當即被另做他用。 jvm
堆內存用來存放由new建立的對象和數組。在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。在堆中產生了一個數組或對象後,還能夠在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。引用變量就至關因而爲數組或對象起的一個名稱,之後就能夠在程序中使用棧中的引用變量來訪問堆中的數組或對象。
從名字便可看出,本地方法棧就是用來處理Java中的本地方法的,Java類的祖先類Object中有衆多Native方法,如hashCode()、wait()等,他們的執行不少時候是藉助於操做系統,可是JVM須要對他們作一些規範,來處理他們的執行過程。此區域,能夠有不一樣的實現方法,向咱們經常使用的Sun的JVM就是本地方法棧和JVM虛擬機棧是同一個。
堆內存是內存中最重要的一塊,也是最有必要進行深究的一部分。由於Java性能的優化,主要就是針對這部份內存的。全部的對象實例及數組都是在堆上面分配的(隨着JIT技術的逐漸成熟,這句話視乎有些絕對,不過至少目前還基本是這樣的),可經過-Xmx和-Xms來控制堆的大小。JIT技術的發展產生了新的技術,如棧上分配和標量替換,也許在不久的幾年裏,即時編譯會誕生及成熟,那個時候,「全部的對象實例及數組都是在堆上面分配的」這句話就應該稍微改改了。堆內存是垃圾回收的主要區域,因此在下文垃圾回收板塊會重點介紹,此處只作概念方面的解釋。在32位系統上最大爲2G,64位系統上無限制。可經過-Xms和-Xmx控制,-Xms爲JVM啓動時申請的最小Heap內存,-Xmx爲JVM可申請的最大Heap內存。
方法區是全部線程共享的內存區域,用於存儲已經被 JVM 加載的類信息、常量、靜態變量等數據,通常來講,方法區屬於持久代(關於持久代,會在GC部分詳細介紹,除了持久代,還有新生代和舊生代),也難怪Java規範將方法區描述爲堆的一個邏輯部分,可是它不是堆。方法區的垃圾回收比較棘手,就算是Sun的HotSpot VM在這方面也沒有作得多麼完美。此處引入方法區中一個重要的概念:運行時常量池。主要用於存放在編譯過程當中產生的字面量(字面量簡單理解就是常量)和引用。通常狀況,常量的內存分配在編譯期間就能肯定,但不必定全是,有一些可能就是運行時也可將常量放入常量池中,如String類中有個Native方法intern()<關於intern()的詳細說明,請看另外一篇文章:http://blog.csdn.net/zhangerqing/article/details/8093919>
此處補充一個在JVM內存管理以外的一個內存區:直接內存。在JDK1.4中新加入類NIO類,引入了一種基於通道與緩衝區的I/O方式,它可使用Native函數庫直接分配堆外內存,即咱們所說的直接內存,這樣在某些場景中會提升程序的性能。
在咱們上面介紹的五大區中,有三個是不須要進行垃圾回收的:程序計數器、JVM棧、本地方法棧。由於它們的生命週期是和線程同步的,隨着線程的銷燬,它們佔用的內存會自動釋放,因此只有方法區和堆須要進行GC。具體到哪些對象的話,簡單概況一句話:若是某個對象已經不存在任何引用,那麼它能夠被回收。通俗解釋一下就是說,若是一個對象,已經沒有什麼做用了,就能夠被當廢棄物被回收了。
垃圾回收算法:
根據一個經典的引用計數算法,每一個對象添加一個引用計數器,每被引用一次,計數器加1,失去引用,計數器減1,當計數器在一段時間內保持爲0時,該對象就認爲是能夠被回收得了。可是,這個算法有明顯的缺陷:當兩個對象相互引用,可是兩者已經沒有做用時,按照常規,應該對其進行垃圾回收,可是其相互引用,又不符合垃圾回收的條件,所以沒法完美處理這塊內存清理,所以Sun的JVM並無採用引用計數算法來進行垃圾回收。而是採用一個叫:根搜索算法,以下圖:
基本思想就是:從一個叫GC Roots的對象開始,向下搜索,若是一個對象不能到達GC Roots對象的時候,說明它已經再也不被引用,便可被進行垃圾回收(此處 暫且這樣理解,其實事實還有一些不一樣,當一個對象再也不被引用時,並無徹底「死亡」,若是類重寫了finalize()方法,且沒有被系統調用過,那麼系統會調用一次finalize()方法,以完成最後的工做,在這期間,若是能夠將對象從新與任何一個和GC Roots有引用的對象相關聯,則該對象能夠「重生」,若是不能夠,那麼就說明完全能夠被回收了),如上圖中的Object五、Object六、Object7,雖然它們3個依然可能相互引用,可是整體來講,它們已經沒有做用了,這樣就解決了引用計數算法沒法解決的問題。
歡迎來到「Under The Hood「第三期。前兩期咱們分別介紹了JVM的基本結構和功能和Java類文件的基本結構,本期的主要內容有:字節碼所操做的原始類型、類型轉換的字節碼,以及操做JVM棧的字節碼。
字節碼格式
字節碼是JVM的機器語言。JVM加載類文件時,對類中的每一個方法,它都會獲得一個字節碼流。這些字節碼流保存在JVM的方法區中。在程序運行過程當中,當一個方法被調用時,它的字節碼流就會被執行。根據特定JVM設計者的選擇,它們能夠經過解釋的方式,即時編譯(Just-in-time compilation)的方式或其餘技術的方式被執行。
方法的字節碼流就是JVM的指令(instruction)序列。每條指令包含一個單字節的操做碼(opcode)和0個或多個操做數(operand)。操做碼指明要執行的操做。若是JVM在執行操做前,須要更多的信息,這些信息會以0個或多個操做數的方式,緊跟在操做碼的後面。
每種類型的操做碼都有一個助記符(mnemonic)。相似典型的彙編語言風格,Java字節碼流能夠用它們的助記符和緊跟在後面的操做數來表示。例如,下面的字節碼流能夠分解成多個助記符的形式。
// 字節碼流: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // 分解後: iconst_0 // 03 istore_0 // 3b iinc 0, 1 // 84 00 01 iload_0 // 1a iconst_2 // 05 imul // 68 istore_0 // 3b goto -7 // a7 ff f9
字節碼指令集被設計的很緊湊。除了處理跳錶的2條指令之外,全部的指令都以字節邊界對齊。操做碼的總數不多,一個字節就能搞定。這最小化了JVM加載前,經過網絡傳輸的類文件的大小;也使得JVM能夠維持很小的實現。
JVM中,全部的計算都是圍繞棧(stack)而展開的。由於JVM沒有存儲任意數值的寄存器(register),全部的操做數在計算開始以前,都必須先壓入棧中。所以,字節碼指令主要是用來操做棧的。例如,在上面的字節碼序列中,經過iload_0先把本地變量(local variable)入棧,而後用iconst_2把數字2入棧的方式,來計算本地變量乘以2。兩個整數都入棧以後,imul指令有效的從棧中彈出它們,而後作乘法,最後把運算結果壓入棧中。istore_0指令把結果從棧頂彈出,保存回本地變量。JVM被設計成基於棧,而不是寄存器的機器,這使得它在如80486寄存器架構不佳的處理器上,也能被高效的實現。