這是一篇你能看懂 Java JVM 文章

(本文參考深刻理解JAVA虛擬機第二版第2章)
複製代碼

1、認識Java環境

在講 JVM 以前,先講講 JDK、JRE和 JVM 的關係,以下面這張圖(圖片來自百度圖片): html

在這裏插入圖片描述
能夠看到他們的包含關係是 JDK>JRE>JVM

  • JDK:jdk是支持 JAVA程序開發的最小環境,集成了JRE和一些工具包,如 javac,jar等;好比一個可運行jar,你就須要安裝了jdk,才能運行起來
  • JRE:是Java運行時的標準環境,除了JVM的環境還有一些基本的JAVA庫,好比界面的 swing、I/O等

JVM:熟稱Java虛擬機,也叫運行時數據區域,是保證跨平臺的基本,由於 jvm 只認識字節碼,只要linux、window、mac 有jvm 都是能夠編譯執行的;固然它還有一個java

而這裏,咱們就須要講解 JVM 這個 運行時數據區域的分佈了,以下圖(圖片來自百度圖片,稍微修改了一點):linux

在這裏插入圖片描述

上面解釋了一個java程序是怎麼運行的,其中 內存空間這裏,就是 JVM 了;web

  • 線程共享區:即程序運行時,數據在各個線程之間是共享的,好比某個方法,某個類,還有一些運行時常量
  • 線程私有區:各個線程之間的數據是獨立的,好比多線程的數據

爲了方便解釋,這裏的順序不會像上圖那裏的順序來;算法

2、線程私有區

2.一、程序計算器

首先先了解程序計算器,線程(UI線程)中程序語句的執行都離不開它,對它的解釋以下:安全

  1. 是一塊較小的內存存於,能夠看作當前線程執行字節碼時的行號指示器
  2. 程序的運行,好比跳轉、循環等指令,就是經過改變計算器的數值,來選取下一條須要執行的字節碼指令
  3. 多線程時,每一個線程的程序計算器都是獨立的,相互不干擾,獨立儲存;即記錄每次線程的位置,方便下次線程切換過來,知道上次線程的運行到哪了

2.2 虛擬機棧

結合方法去中的一些變量和常量去理解會比較好
複製代碼

虛擬機棧也是線程私有的,與線程的生命週期相同;它對應着線程的內存模式,每一個方法在執行的時候,都有一個棧幀用於存儲局部表,操做數棧、動態連接、方法出口等信息;每一個方法的執行,都對應着一個棧幀在虛擬機棧中的入棧和出棧,以下圖(網上找的,當時保留的,具體哪位的有點忘了,看到能夠聯繫我) websocket

在這裏插入圖片描述
局部變量表:

  • 存儲了編譯器存放着各類基本數據類型(boolean、byte、char等)
  • 對象引用類型,這裏的對象不是對象自己,多是對象的尋址指針,也多是句柄或者相關位置
  • returnAddress 類型,指向了一條字節碼指令的地址

當進入一個方法時,這些變量在幀中分配的內存大小時固定的,在運行時不會改變局部變量表的大小。針對這個區域,規定了兩種異常狀況多線程

  • 若是虛擬機不支持動態擴展,當線程請求的棧大小大於虛擬機規定的大小時,拋出 StackOverflowError
  • 若是虛擬機棧能夠動態擴展,若是擴展時,沒法申請到足夠的內存,拋出 OutOfMemoryError

操做數棧:併發

操做數棧,也能夠稱作操做棧,它能夠是 Java 的任意類型,在數據提取時入棧和出棧,好比 int a = 1 + 2;在把1,2入到這個操做的棧的時候,也會把1,2提取出來,再分配給 a; 動態鏈接:jvm

能夠這樣理解,好比線程中的一個A方法,在類加載的時候,它只是一個符號引用,在運行期間,轉換爲直接引用,這種稱爲動態鏈接,關於符號引用,後面會說道。 方法出口: 其實就是返回地址,當方法執行完畢或者手動退出時,就出棧了,用來記錄一些信息,好比恢復局部變量等信息

2.3 本地方法棧

本地方法棧與虛擬機棧的做用很是類似;只不過虛擬機棧執行的是 java 的字節碼服務,而本地方法棧執行的是 Native 方法服務; 本地方法棧一樣會穿件棧幀,如局部變量表、操做棧等信息,同時也有 StackOverflowError 和 OutOfMemoryError 異常

3、線程共享區

3.1 Java 堆:

是Java虛擬機鎖管理的內存中最大的一塊,在虛擬機啓動建立時,此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都是在這分配內存的; Java 堆是內存回收的主要區域,也叫 GC 堆;根據規定,Java堆的物理地址能夠是不連續的,只要保證邏輯上是連續的便可。因爲Java 堆基本採用分代手機算法,因此也能夠分爲:新生代和老年代;再細緻分,也能夠分爲 Eden空間,From Survivor 空間、To Surivivor 空間等涉及到的GC回收算法,後面再開章節介紹。

3.2 方法堆

方法堆也是線程共享的一個區域塊,它用於存儲虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區規定爲 Java 堆的一個邏輯模塊,但它還有一個方法,叫 Non-Heap (非堆) ,目的就是爲了和 Java堆區分開來

3.2.1 運行時常量池

運行時常量池,其實算方法區的一部分。Class文件中除了有 類的版本、字段、方法、接口等信息外;還有一項信息就是常量池,用於存放編譯期生成的字面量和字符引用,以下圖: (圖片來源 blog.csdn.net/wangbiao007…)

在這裏插入圖片描述

4、直接內存

在JDK1.4中,新增長了一個 NIO(New Inout/Outinput)類,引入了一種基於通道(channel)與緩衝區(buffer)的I/O方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣在一些場景中可以顯著提高技能,避免了數據再 Java 堆和 Native 堆中來回複製數據,常見的通道類型有:

  • FileChannel:從文件中讀寫數據
  • DatagramChannel:從UDP中讀寫數據
  • SocketChannel:從TCP中讀寫數據
  • ServerSocketChannel:用來監聽 websocket 的鏈接

具體案例能夠查找NIO的具體案例 直接內存,不是虛擬機運行時內存區的一部分,也不是Java規範中定義的內存區域。但既然是內存,若是 超過了 RAM 和 SWAP 尋址空間限制,仍是會報OutOfMemoryError的

5、HotSpot 虛擬機對象探祕

上面瞭解了 JVM 的一些知識以後,那麼一個對象的建立是怎麼樣的呢?對象的建立,能夠分爲如下幾個步驟

在這裏插入圖片描述
類加載

當虛擬機遇到一個 new 指令的時候,會先去檢測這個指令的參數是否能定位到這個類的符號引用,並檢查這個類是否被加載、解釋或初始化過。若是沒有,則執行類加載 (後面新開一章解釋)

內存分配

在類加載經過以後,虛擬機將爲新生對象分配內存,對象所需內存的大小在類加載完成後即可徹底肯定,至關於從Java堆中抽取一塊內存出來;而根據內存的是否絕對規整,分爲 指針碰撞空閒列表 兩種分配方式:

  • 指針碰撞:假設Java堆中的內存只絕對規整的,分爲空閒和非空閒兩種,中間用一個指針當作劃分界限的指示器;當一個新對象須要分配對象時,至關於把指針向空閒區域移動一段與對象大小相等的距離
  • 空閒列表:假設Java堆的內存不是絕對規整的,空閒和非空閒是相互交錯的,那就須要一個列表,用來記錄哪些內存塊是能夠用的,在對象分配內存時,劃分一塊大小相等的區域給對象,並更新這個列表

從上面的解釋看,用哪一種分配方式,是經過Java堆的內存塊是否絕對規整決定的。

內存分配

但對象的建立是頻繁的,在併發的狀況,多線程不必定是安全的,即存在A對象在分配內存,指針還將來得及修改,B對象也同時使用了原來的指針來分配對象。因此又衍生了兩種解決辦法,CAS+失敗重試TLAB兩種方式

  • CAS+失敗重試:虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性 (關於CAS鎖,是樂觀鎖的一種實現,解釋起來也比較麻煩,能夠參考這裏:www.cnblogs.com/javalyy/p/8…)
  • TLAB:本地線程分配緩衝,把內存分配的動做按照線程分配劃分在不一樣的空間中進行,即每一個線程在Java堆中預先分配一小塊內存,哪一個線程須要須要分配,先在 TLAB 中分配,用完了並從新分配新的TLAB時,才須要同步鎖定。

初始值爲零

在內存分配完成以後,虛擬機須要將分配到的內存空間初始化爲零值 (除對象頭外),這一步操做也保證了對象的實例字段在java代碼中能夠不賦初始值就可使用,由於程序能訪問這些字段的數據類型所對應的零值

設置對象頭

初始值設置以後,怎麼知道對象是哪一個類的實例,如何才能找到類的元數據信息、哈希碼、GC分代年齡等信息呢?這就須要對對象頭進行一些必要的設置,才能定位到,詳細在5.2節介紹。

入棧,執行init指令

從虛擬機來看,對象已經分配產生完成了,且入棧了;但 Java 程序來看,這纔剛開始,因此,new 以後,則執行 init 方法,進行初始化。

5.2 對象的內存分佈

上面講解了對象在 虛擬機的分配以後,再擴展一下,對象在內存中是怎麼分配的呢,對象在內存中的存儲佈局可分爲 3個部分:

在這裏插入圖片描述
對象頭

其中,對象頭能夠再細分爲兩部分:

  • 存儲對象自身的運行時數據:如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的、偏向線程ID等信息
  • 類型指針:即對象指向它的類元數據的指針,虛擬機經過這個來肯定這個對象是哪一個類的實例

實例數據

是對象真正儲存的有效信息,好比程序中定義的各類類型的字段內容,不管父類和子類都會記錄下來;在分配時,相同寬度的字段會被分配到一塊兒,這也是父類定義的變量會出如今子類以前的緣由。

對齊填充

沒啥實際意義,只是爲了保證對象是8字節的整數倍,沒對齊時,用來補全而已。

5.3 對象的訪問定位

創建對象是爲了使用對象,Java 程序須要經過棧上的 reference 數據來操做堆上的具體對象;但這些訪問方式取決於虛擬機實現而定,目前主流有句柄和直接指針兩種:

  • 句柄:從Java 堆中劃分出一塊內存用來做爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄包含了對象實例數據與類型數據各自的具體地址信息,以下圖(圖片來自Java虛擬機第三版)
    在這裏插入圖片描述
  • 直接指針在直接指針中,reference 儲存的就是對象地址,因此,須要考慮的是如何防止訪問類型數據的相關信息(圖片來自Java虛擬機第三版)
    在這裏插入圖片描述

優勢介紹: 句柄:使用句柄好處是,reference中存放的是文檔的句柄地址,對象被移動時,只改變句柄的實例數據指針,而reference 自己不須要修改 直接指針:使用直接指針的最大好處就是速度更快,節省了指針定位的開銷;

擴展

爲何字符串拼接的時候,不適合用 String ,而應該使用 StringBuilder 或者 StringBuffer ? 好比 String = "abc"; (可參考常量池來解釋喲)

在這裏插入圖片描述
相關文章
相關標籤/搜索