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

夏至的稻穗發表於2019-04-12
(本文參考深入理解JAVA虛擬機器第二版第2章)
複製程式碼

一、認識Java環境

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

在這裡插入圖片描述
可以看到他們的包含關係是 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 都是可以編譯執行的;當然它還有一個

而這裡,我們就需要講解 JVM 這個 執行時資料區域的分佈了,如下圖(圖片來自百度圖片,稍微修改了一點):

在這裡插入圖片描述

上面解釋了一個java程式是怎麼執行的,其中 記憶體空間這裡,就是 JVM 了;

  • 執行緒共享區:即程式執行時,資料在各個執行緒之間是共享的,比如某個方法,某個類,還有一些執行時常量
  • 執行緒私有區:各個執行緒之間的資料是獨立的,比如多執行緒的資料

為了方便解釋,這裡的順序不會像上圖那裡的順序來;

二、執行緒私有區

2.1、程式計算器

首先先了解程式計算器,執行緒(UI執行緒)中程式語句的執行都離不開它,對它的解釋如下:

  1. 是一塊較小的記憶體存於,可以看做當前執行緒執行位元組碼時的行號指示器
  2. 程式的執行,比如跳轉、迴圈等指令,就是通過改變計算器的數值,來選取下一條需要執行的位元組碼指令
  3. 多執行緒時,每個執行緒的程式計算器都是獨立的,相互不干擾,獨立儲存;即記錄每次執行緒的位置,方便下次執行緒切換過來,知道上次執行緒的執行到哪了

2.2 虛擬機器棧

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

虛擬機器棧也是執行緒私有的,與執行緒的生命週期相同;它對應著執行緒的記憶體模式,每個方法在執行的時候,都有一個棧幀用於儲存區域性表,運算元棧、動態連結、方法出口等資訊;每個方法的執行,都對應著一個棧幀在虛擬機器棧中的入棧和出棧,如下圖(網上找的,當時保留的,具體哪位的有點忘了,看到可以聯絡我)

在這裡插入圖片描述
區域性變數表:

  • 儲存了編譯器存放著各種基本資料型別(boolean、byte、char等)
  • 物件引用型別,這裡的物件不是物件本身,可能是物件的定址指標,也可能是控制程式碼或者相關位置
  • returnAddress 型別,指向了一條位元組碼指令的地址

當進入一個方法時,這些變數在幀中分配的記憶體大小時固定的,在執行時不會改變區域性變數表的大小。針對這個區域,規定了兩種異常情況

  • 如果虛擬機器不支援動態擴充套件,當執行緒請求的棧大小大於虛擬機器規定的大小時,丟擲 StackOverflowError
  • 如果虛擬機器棧可以動態擴充套件,如果擴充套件時,無法申請到足夠的記憶體,丟擲 OutOfMemoryError

運算元棧:

運算元棧,也可以稱做操作棧,它可以是 Java 的任意型別,在資料提取時入棧和出棧,比如 int a = 1 + 2;在把1,2入到這個操作的棧的時候,也會把1,2提取出來,再分配給 a; 動態連線:

可以這樣理解,比如執行緒中的一個A方法,在類載入的時候,它只是一個符號引用,在執行期間,轉換為直接引用,這種稱為動態連線,關於符號引用,後面會說道。 方法出口: 其實就是返回地址,當方法執行完畢或者手動退出時,就出棧了,用來記錄一些資訊,比如恢復區域性變數等資訊

2.3 本地方法棧

本地方法棧與虛擬機器棧的作用非常相似;只不過虛擬機器棧執行的是 java 的位元組碼服務,而本地方法棧執行的是 Native 方法服務; 本地方法棧同樣會穿件棧幀,如區域性變數表、操作棧等資訊,同時也有 StackOverflowError 和 OutOfMemoryError 異常

三、執行緒共享區

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…)

在這裡插入圖片描述

四、直接記憶體

在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的

五、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"; (可參考常量池來解釋喲)

在這裡插入圖片描述

相關文章