JVM系統學習之路系列演示代碼地址:https://github.com/mtcarpenter/JavaTutorial
本篇將 運行時數據區概述及線程 和 程序計數器 的知識點由於不是很多所以就一起梳理,也是為後續學習的知識點做一個鋪墊。
運行時數據區概述運行時數據區,它是在類加載完成後的階段,如果對類加載不是很熟悉的小夥伴,可以看我上一篇文章。
當我們通過前面的:類的加載-> 驗證 -> 準備 -> 解析 -> 初始化 這幾個階段完成後,就會用到執行引擎對我們的類進行使用,同時執行引擎將會使用到我們運行時數據區,如下內存是非常重要的系統資源,是硬碟和 CPU 的中間倉庫及橋梁,承載著作業系統和應用程式的實時運行 JVM 內存布局規定了 Java 在運行過程中內存申請、分配、管理的策略,保證了 JVM 的高效穩定運行。不同的 JVM 對於內存的劃分方式和管理機制存在著部分差異。結合 JVM 虛擬機規範,來探討一下經典的 JVM 內存布局。
我們通過磁碟或者網絡 IO 得到的數據,都需要先加載到內存中,然後 CPU 從內存中獲取數據進行讀取,也就是說內存充當了 CPU 和磁碟之間的橋梁
首先還是一張運行時數據區的完整圖:Java 虛擬機定義了若干種程序運行期間會使用到的運行時數據區,其中有一些會隨著虛擬機啟動而創建,隨著虛擬機退出而銷毀。另外一些則是與線程一一對應的,這些與線程對應的數據區域會隨著線程開始和結束而創建和銷毀。
灰色的為單獨線程私有的,紅色的為多個線程共享的。即:
線程間共享:堆、堆外內存(永久代或元空間、代碼緩存)image.png線程線程是一個程序裡的運行單元。JVM 允許一個應用有多個線程並行的執行。在 Hotspot JVM 裡,每個線程都與作業系統的本地線程直接映射。
當一個 Java 線程準備好執行以後,此時一個作業系統的本地線程也同時創建。Java 線程執行終止後,本地線程也會回收。作業系統負責所有線程的安排調度到任何一個可用的 CPU 上。一旦本地線程初始化成功,它就會調用 Java 線程中的run() 方法。
JVM系統線程如果你使用 console 或者是任何一個調試工具,都能看到在後臺有許多線程在運行。這些後臺線程不包括調用 public static void main(String[]) 的 main 線程以及所有這個main線程自己創建的線程。這些主要的後臺系統線程在 Hotspot JVM 裡主要是以下幾個:
虛擬機線程:這種線程的操作是需要 JVM 達到安全點才會出現。這些操作必須在不同的線程中發生的原因是他們都需要JVM達到安全點,這樣堆才不會變化。這種線程的執行類型包括 "stop-the-world" 的垃圾收集,線程棧收集,線程掛起以及偏向鎖撤銷。周期任務線程:這種線程是時間周期事件的體現(比如中斷),他們一般用於周期性操作的調度執行。GC 線程:這種線程對在 JVM 裡不同種類的垃圾收集行為提供了支持。編譯線程:這種線程在運行時會將字節碼編譯成到本地代碼。信號調度線程:這種線程接收信號並發送給 JVM,在它內部通過調用適當的方法進行處理。程序計數器(PC Register) 介紹JVM 中的 程序計數寄存器 ( Program Counter Register )中,Register 的命名源於 CPU 的寄存器,寄存器存儲指令相關的現場信息。CPU 只有把數據裝載到寄存器才能夠運行。這裡,並非是廣義上所指的物理寄存器,或許將其翻譯為 PC計數器(或指令計數器)會更加貼切(也稱為程序鉤子),並且也不容易引起一些不必要的誤會。JVM 中的 PC 寄存器是對物理PC寄存器的一種抽象模擬。程序計數器是一塊很小的內存空間,幾乎可以忽略不記。也是運行速度最快的存儲區域。在 JVM 規範中,每個線程都有它自己的程序計數器,是線程私有的,生命周期與線程的生命周期保持一致。任何時間一個線程都只有一個方法在執行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執行的 Java 方法的 JVM 指令地址;或者,如果是在執行 native 方法,則是未指定值(undefned)。它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。它是唯一一個在 Java 虛擬機規範中沒有規定任何 outotMemoryError 情況的區域。
作用PC寄存器 用來存儲指向下一條指令的地址,也即將要執行的指令代碼。由執行引擎讀取下一條指令。
舉例說明通過寫一個簡單的代碼演示:
/**程序計數器
*/
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}將上面的 java 文件編譯成字節碼文件,然後執行 ` javap -c PCRegisterTest.class`` ,通過控制臺查看 ,發現在字節碼的左邊有一個行號標識,它其實就是指令地址,用於指向當前執行到哪裡。
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
}通過PC寄存器,我們就可以知道當前程序執行到哪一步了
兩個常見問題使用PC寄存器存儲字節碼指令地址有什麼用呢?因為 CPU 需要不停的切換各個線程,這時候切換回來以後,就得知道接著從哪開始繼續執行。JVM的字節碼解釋器就需要通過改變 PC 寄存器 的值來明確下一條應該執行什麼樣的字節碼指令。
PC寄存器為什麼被設定為私有的?我們都知道所謂的多線程在一個特定的時間段內只會執行其中某一個線程的方法,CPU 會不停地做任務切換,這樣必然導致經常中斷或恢復,如何保證分毫無差呢?為了能夠準確地記錄各個線程正在執行的當前字節碼指令地址,最好的辦法自然是為每一個線程都分配一個PC寄存器 ,這樣一來各個線程之間便可以進行獨立計算,從而不會出現相互幹擾的情況。由於 CPU 時間片輪限制,眾多線程在並發執行過程中,任何一個確定的時刻,一個處理器或者多核處理器中的一個內核,只會執行某個線程中的一條指令。這樣必然導致經常中斷或恢復,如何保證分毫無差呢?每個線程在創建後,都會產生自己的程序計數器和棧幀,程序計數器在各個線程之間互不影響。
CPU時間片CPU 時間片即 CPU 分配給各個程序的時間,每個線程被分配一個時間段,稱作它的時間片。在宏觀上:我們可以同時打開多個應用程式,每個程序並行不悖,同時運行。但在微觀上:由於只有一個 CPU,一次只能處理程序要求的一部分,如何處理公平,一種方法就是引入時間片,每個程序輪流執行。
總結本篇回顧,第一部分為運行時數據區,第二部分就是程序計數器。JVM 能高效穩定運行 ,主要是 JVM 內存布局規定了 Java 在運行過程中內存申請、分配、管理的策略。接下來會通過很多的章節講述運行時數據區下的各個區。還需要了解PC寄存器 用來存儲指向下一條指令的地址,也即將要執行的指令代碼。由執行引擎讀取下一條指令。
歡迎關注公眾號 山間木匠 , 我是小春哥,從事 Java 後端開發,會一點前端、通過持續輸出系列技術文章以文會友,如果本文能為您提供幫助,歡迎大家關注、在看、 點讚、分享支持,我們下期再見!