(本文參考深刻理解JAVA虛擬機第二版第2章)
複製代碼
在講 JVM 以前,先講講 JDK、JRE和 JVM 的關係,以下面這張圖(圖片來自百度圖片): html
JVM:熟稱Java虛擬機,也叫運行時數據區域,是保證跨平臺的基本,由於 jvm 只認識字節碼,只要linux、window、mac 有jvm 都是能夠編譯執行的;固然它還有一個java
而這裏,咱們就須要講解 JVM 這個 運行時數據區域的分佈了,以下圖(圖片來自百度圖片,稍微修改了一點):linux
上面解釋了一個java程序是怎麼運行的,其中 內存空間這裏,就是 JVM 了;web
爲了方便解釋,這裏的順序不會像上圖那裏的順序來;算法
首先先了解程序計算器,線程(UI線程)中程序語句的執行都離不開它,對它的解釋以下:安全
結合方法去中的一些變量和常量去理解會比較好
複製代碼
虛擬機棧也是線程私有的,與線程的生命週期相同;它對應着線程的內存模式,每一個方法在執行的時候,都有一個棧幀用於存儲局部表,操做數棧、動態連接、方法出口等信息;每一個方法的執行,都對應着一個棧幀在虛擬機棧中的入棧和出棧,以下圖(網上找的,當時保留的,具體哪位的有點忘了,看到能夠聯繫我) websocket
當進入一個方法時,這些變量在幀中分配的內存大小時固定的,在運行時不會改變局部變量表的大小。針對這個區域,規定了兩種異常狀況多線程
操做數棧:併發
操做數棧,也能夠稱作操做棧,它能夠是 Java 的任意類型,在數據提取時入棧和出棧,好比 int a = 1 + 2;在把1,2入到這個操做的棧的時候,也會把1,2提取出來,再分配給 a; 動態鏈接:jvm
能夠這樣理解,好比線程中的一個A方法,在類加載的時候,它只是一個符號引用,在運行期間,轉換爲直接引用,這種稱爲動態鏈接,關於符號引用,後面會說道。 方法出口: 其實就是返回地址,當方法執行完畢或者手動退出時,就出棧了,用來記錄一些信息,好比恢復局部變量等信息
本地方法棧與虛擬機棧的做用很是類似;只不過虛擬機棧執行的是 java 的字節碼服務,而本地方法棧執行的是 Native 方法服務; 本地方法棧一樣會穿件棧幀,如局部變量表、操做棧等信息,同時也有 StackOverflowError 和 OutOfMemoryError 異常
是Java虛擬機鎖管理的內存中最大的一塊,在虛擬機啓動建立時,此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都是在這分配內存的; Java 堆是內存回收的主要區域,也叫 GC 堆;根據規定,Java堆的物理地址能夠是不連續的,只要保證邏輯上是連續的便可。因爲Java 堆基本採用分代手機算法,因此也能夠分爲:新生代和老年代;再細緻分,也能夠分爲 Eden空間,From Survivor 空間、To Surivivor 空間等涉及到的GC回收算法,後面再開章節介紹。
方法堆也是線程共享的一個區域塊,它用於存儲虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區規定爲 Java 堆的一個邏輯模塊,但它還有一個方法,叫 Non-Heap (非堆) ,目的就是爲了和 Java堆區分開來
運行時常量池,其實算方法區的一部分。Class文件中除了有 類的版本、字段、方法、接口等信息外;還有一項信息就是常量池,用於存放編譯期生成的字面量和字符引用,以下圖: (圖片來源 blog.csdn.net/wangbiao007…)
在JDK1.4中,新增長了一個 NIO(New Inout/Outinput)類,引入了一種基於通道(channel)與緩衝區(buffer)的I/O方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣在一些場景中可以顯著提高技能,避免了數據再 Java 堆和 Native 堆中來回複製數據,常見的通道類型有:
具體案例能夠查找NIO的具體案例 直接內存,不是虛擬機運行時內存區的一部分,也不是Java規範中定義的內存區域。但既然是內存,若是 超過了 RAM 和 SWAP 尋址空間限制,仍是會報OutOfMemoryError的。
上面瞭解了 JVM 的一些知識以後,那麼一個對象的建立是怎麼樣的呢?對象的建立,能夠分爲如下幾個步驟
當虛擬機遇到一個 new 指令的時候,會先去檢測這個指令的參數是否能定位到這個類的符號引用,並檢查這個類是否被加載、解釋或初始化過。若是沒有,則執行類加載 (後面新開一章解釋)
內存分配
在類加載經過以後,虛擬機將爲新生對象分配內存,對象所需內存的大小在類加載完成後即可徹底肯定,至關於從Java堆中抽取一塊內存出來;而根據內存的是否絕對規整,分爲 指針碰撞 和 空閒列表 兩種分配方式:
從上面的解釋看,用哪一種分配方式,是經過Java堆的內存塊是否絕對規整決定的。
內存分配
但對象的建立是頻繁的,在併發的狀況,多線程不必定是安全的,即存在A對象在分配內存,指針還將來得及修改,B對象也同時使用了原來的指針來分配對象。因此又衍生了兩種解決辦法,CAS+失敗重試 和 TLAB兩種方式
初始值爲零
在內存分配完成以後,虛擬機須要將分配到的內存空間初始化爲零值 (除對象頭外),這一步操做也保證了對象的實例字段在java代碼中能夠不賦初始值就可使用,由於程序能訪問這些字段的數據類型所對應的零值。
設置對象頭
初始值設置以後,怎麼知道對象是哪一個類的實例,如何才能找到類的元數據信息、哈希碼、GC分代年齡等信息呢?這就須要對對象頭進行一些必要的設置,才能定位到,詳細在5.2節介紹。
入棧,執行init指令:
從虛擬機來看,對象已經分配產生完成了,且入棧了;但 Java 程序來看,這纔剛開始,因此,new 以後,則執行 init 方法,進行初始化。
上面講解了對象在 虛擬機的分配以後,再擴展一下,對象在內存中是怎麼分配的呢,對象在內存中的存儲佈局可分爲 3個部分:
其中,對象頭能夠再細分爲兩部分:
實例數據
是對象真正儲存的有效信息,好比程序中定義的各類類型的字段內容,不管父類和子類都會記錄下來;在分配時,相同寬度的字段會被分配到一塊兒,這也是父類定義的變量會出如今子類以前的緣由。
對齊填充
沒啥實際意義,只是爲了保證對象是8字節的整數倍,沒對齊時,用來補全而已。
創建對象是爲了使用對象,Java 程序須要經過棧上的 reference 數據來操做堆上的具體對象;但這些訪問方式取決於虛擬機實現而定,目前主流有句柄和直接指針兩種:
優勢介紹: 句柄:使用句柄好處是,reference中存放的是文檔的句柄地址,對象被移動時,只改變句柄的實例數據指針,而reference 自己不須要修改 直接指針:使用直接指針的最大好處就是速度更快,節省了指針定位的開銷;
爲何字符串拼接的時候,不適合用 String ,而應該使用 StringBuilder 或者 StringBuffer ? 好比 String = "abc"; (可參考常量池來解釋喲)