jvm(一)虛擬機概述
時間:2023-07-02 03:30:01 | 來源:網(wǎng)站運營
時間:2023-07-02 03:30:01 來源:網(wǎng)站運營
jvm(一)虛擬機概述:
一、基礎(chǔ)知識
1、Java 程序的執(zhí)行過程
一個 Java 程序, 首先經(jīng)過 javac 編譯成 .class 文件, 然后 JVM 將其加載到方法區(qū), 執(zhí)行引擎將會執(zhí)行這些字節(jié)碼。 執(zhí)行時, 會翻譯成操作系統(tǒng)相關(guān)的函數(shù)。 JVM 作為 .class 文件的翻譯存在, 輸入字節(jié)碼, 調(diào)用操作系統(tǒng)函數(shù)。
過程如下: Java 文件->編譯器>字節(jié)碼->JVM->機器碼。
JVM 全稱 Java Virtual Machine, 也就是我們耳熟能詳?shù)?Java 虛擬機。 它能識別 .class 后綴的文件, 并且能夠解析它的指令, 最終調(diào)用操作系統(tǒng)上的函數(shù), 完成我們想要的操作。
2、JVM、 JRE、 JDK 的關(guān)系
JVM 只是一個翻譯, 把 Class 翻譯成機器識別的代碼, 但是需要注意, JVM 不會自己生成代碼, 需要大家編寫代碼, 同時需要很多依賴類庫, 這個時候就需要用到 JRE。
JRE 是什么, 它除了包含 JVM 之外, 提供了很多的類庫(就是我們說的 jar 包, 它可以提供一些即插即用的功能, 比如讀取或者操作文件, 連接網(wǎng)絡(luò),
使用 I/O 等等之類的) 這些東西就是 JRE 提供的基礎(chǔ)類庫。 JVM 標準加上實現(xiàn)的一大堆基礎(chǔ)類庫, 就組成了 Java 的運行時環(huán)境, 也就是我們常說的 JRE(Java Runtime Environment) 。
但對于程序員來說, JRE 還不夠。 我寫完要編譯代碼, 還需要調(diào)試代碼, 還需要打包代碼、 有時候還需要反編譯代碼。 所以我們會使用 JDK, 因為 JDK還提供了一些非常好用的小工具, 比如 javac(編譯代碼) 、 java、 jar (打包代碼) 、 javap(反編譯<反匯編>) 等。 這個就是 JDK。
具體可以文檔可以通過官網(wǎng)去下載:
https://www.oracle.com/java/technologies/javase-jdk8-doc-downloads.htmlJVM 的作用是: 從軟件層面屏蔽不同操作系統(tǒng)在底層硬件和指令的不同。 這個就是我們在宏觀方面對 JVM 的一個認識。
3、跨平臺
我們寫的一個類, 在不同的操作系統(tǒng)上(Linux、 Windows、 MacOS 等平臺) 執(zhí)行, 效果是一樣, 這個就是 JVM 的跨平臺性。
跨語言( 語言無關(guān)性) : JVM 只識別字節(jié)碼, 所以 JVM 其實跟語言是解耦的, 也就是沒有直接關(guān)聯(lián), JVM 運行不是翻譯 Java 文件, 而是識別 class文件, 這個一般稱之為字節(jié)碼。 還有像 Groovy 、 Kotlin、 Scala 等等語言, 它們其實也是編譯成字節(jié)碼, 所以它們也可以在 JVM 上面跑, 這個就是 JVM 的跨語言特征。 Java 的跨語言性一定程度上奠定了非常強大的 java 語言生態(tài)圈。
4、常見jvm實現(xiàn)
二、jvm內(nèi)存區(qū)域
運行時數(shù)據(jù)區(qū)
運行時數(shù)據(jù)區(qū)的定義: Java 虛擬機在執(zhí)行 Java 程序的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域
所以要深入理解 JVM 必須理解內(nèi)存虛擬化的概念。
在 JVM 中, JVM 內(nèi)存主要分為堆、 程序計數(shù)器、 方法區(qū)、 虛擬機棧和本地方法棧等。
同時按照與線程的關(guān)系也可以這么劃分區(qū)域:
線程私有區(qū)域: 一個線程擁有單獨的一份內(nèi)存區(qū)域。
線程共享區(qū)域: 被所有線程共享, 且只有一份。
這里還有一個直接內(nèi)存, 這個雖然不是運行時數(shù)據(jù)區(qū)的一部分, 但是會被頻繁使用。 你可以理解成沒有被虛擬機化的操作系統(tǒng)上的其他內(nèi)存(比如操作系統(tǒng)上有 8G 內(nèi)存, 被 JVM 虛擬化了 3G, 那么還剩余 5G, JVM 是借助一些工具使用這 5G 內(nèi)存的, 這個內(nèi)存部分稱之為直接內(nèi)存)
- 程序計數(shù)器(Program Counter)
程序計數(shù)器是一塊很小的內(nèi)存空間, 主要用來記錄各個線程執(zhí)行的字節(jié)碼的地址, 例如, 分支、 循環(huán)、 跳轉(zhuǎn)、 異常、 線程恢復(fù)等都依賴于計數(shù)器。
由于 Java 是多線程語言, 當執(zhí)行的線程數(shù)量超過 CPU 核數(shù)時, 線程之間會根據(jù)時間片輪詢爭奪 CPU 資源。 如果一個線程的時間片用完了, 或者是其它原因?qū)е逻@個線程的 CPU 資源被提前搶奪, 那么這個退出的線程就需要單獨的一個程序計數(shù)器, 來記錄下一條運行的指令。
因為 JVM 是虛擬機, 內(nèi)部有完整的指令與執(zhí)行的一套流程, 所以在運行 Java 方法的時候需要使用程序計數(shù)器(記錄字節(jié)碼執(zhí)行的地址或行號) , 如果是遇到本地方法(native 方法) , 這個方法不是 JVM 來具體執(zhí)行, 所以程序計數(shù)器不需要記錄了, 這個是因為在操作系統(tǒng)層面也有一個程序計數(shù)器,這個會記錄本地代碼的執(zhí)行的地址, 所以在執(zhí)行 native 方法時, JVM 中程序計數(shù)器的值為空(Undefined)。
另外程序計數(shù)器也是 JVM 中唯一不會 OOM(OutOfMemory)的內(nèi)存區(qū)域。每個線程私有的, 線程在運行時, 在執(zhí)行每個方法的時候都會打包成一個棧幀, 存儲了局部變量表, 操作數(shù)棧, 動態(tài)鏈接, 方法出口等信息, 然后放入棧。 每個時刻正在執(zhí)行的當前方法就是虛擬機棧頂?shù)臈E。 方法的執(zhí)行就對應(yīng)著棧幀在虛擬機棧中入棧和出棧的過程。
虛擬機棧的作用: 在 JVM 運行過程中存儲當前線程運行方法所需的數(shù)據(jù), 指令、 返回地址。 其實在我們實際的代碼中, 一個線程是可以運行多個方法的。
這段代碼, 就是起一個 main 方法, 在 main 方法運行中調(diào)用 A 方法, A 方法中調(diào)用 B 方法, B 方法中運行 C 方法。
我們把代碼跑起來, 線程 1 來運行這段代碼, 線程 1 跑起來, 就會有一個對應(yīng) 的虛擬機棧, 同時在執(zhí)行每個方法的時候都會打包成一個棧幀。
比如 main 開始運行, 打包一個棧幀送入到虛擬機棧。
棧的數(shù)據(jù)結(jié)構(gòu): 先進后出(FILO)的數(shù)據(jù)結(jié)構(gòu),
虛擬機棧是基于線程的: 哪怕你只有一個 main() 方法, 也是以線程的方式運行的。 在線程的生命周期中, 參與計算的數(shù)據(jù)會頻繁地入棧和出棧, 棧的生命周期是和線程一樣的。
虛擬機棧的大小缺省為 1M, 可用參數(shù) –Xss 調(diào)整大小, 例如-Xss256k。
棧幀: 在每個 Java 方法被調(diào)用的時候, 都會創(chuàng)建一個棧幀, 并入棧。 一旦方法完成相應(yīng)的調(diào)用, 則出棧。
棧幀組成:
1、局部變量表
用于存放我們的局部變量的(方法中的變量) 。 首先它是一個 32 位的長度, 主要存放Java 的八大基礎(chǔ)數(shù)據(jù)類型, 一般 32 位就可以存放下, 如果是 64 位的就使用高低位占用兩個也可以存放下, 如果是局部的一些對象, 比如我們的 Object 對象, 我們只需要存放它的一個引用地址即可。(基本數(shù)據(jù)類型、 對象引用、 returnAddress 類型)
2、 操作數(shù)據(jù)棧:
操作數(shù)棧是執(zhí)行引擎的一個工作區(qū),類似于緩存
存放 java 方法執(zhí)行的操作數(shù)的, 它就是一個棧, 先進后出的棧結(jié)構(gòu), 操作數(shù)棧, 就是用來操作的, 操作的的元素可以是任意的 java 數(shù)據(jù)類型, 一個方法剛剛開始的時候, 這個方法的操作數(shù)棧就是空的。
3、 動態(tài)連接:
Java 語言特性多態(tài)(后續(xù)章節(jié)細講, 需要結(jié)合 class 與執(zhí)行引擎一起來講) 。
4、 返回地址:
正常返回(調(diào)用程序計數(shù)器中的地址作為返回) 、 異常的話(通過異常處理器表<非棧幀中的>來確定
正常返回: (調(diào)用程序計數(shù)器中的地址作為返回)
三步曲:
恢復(fù)上層方法的局部變量表和操作數(shù)棧、
把返回值(如果有的話) 壓入調(diào)用者棧幀的操作數(shù)棧中、
調(diào)整程序計數(shù)器的值以指向方法調(diào)用指令后面的一條指令、
異常的話: (通過異常處理表<非棧幀中的>來確定)
棧幀執(zhí)行對內(nèi)存區(qū)域的影響
public class Person { public int work()throws Exception{ int x =1; int y =2; int z =(x+y)*10; return z; } public static void main(String[] args) throws Exception{ Person person = new Person();//person 棧中--、 new Person 對象是在堆 person.work(); person.hashCode(); }}
work方法對應(yīng)指令
0 iconst_1 1 istore_1 2 iconst_2 3 istore_2 4 iload_1 5 iload_2 6 iadd 7 bipush 10 9 imul10 istore_311 iload_312 ireturn
具體指令含義查看:[三] java虛擬機 JVM字節(jié)碼 指令集 bytecode 操作碼 指令分類用法 助記符
大概執(zhí)行過程:先把數(shù)據(jù)壓入到操作數(shù)棧中,然后存儲到局部變量表中或者通知操作引擎進行指令計算(運算后的結(jié)果自動入棧),最終出棧
本地方法棧跟 Java 虛擬機棧的功能類似, Java 虛擬機棧用于管理 Java 函數(shù)的調(diào)用, 而本地方法棧則用于管理本地方法的調(diào)用。 但本地方法并不是用 Java 實現(xiàn)的, 而是由 C 語言實現(xiàn)的(比如 Object.hashcode 方法)。
本地方法棧是和虛擬機棧非常相似的一個區(qū)域, 它服務(wù)的對象是 native 方法。 你甚至可以認為虛擬機棧和本地方法棧是同一個區(qū)域。
虛擬機規(guī)范無強制規(guī)定, 各版本虛擬機自由實現(xiàn) , HotSpot 直接把本地方法棧和虛擬機棧合二為一 。
方法區(qū)(Method Area) 是可供各條線程共享的運行時內(nèi)存區(qū)域。 它存儲了每一個類的結(jié)構(gòu)信息, 例如運行時常量池(Runtime Constant Pool)字段和方法數(shù)據(jù)、 構(gòu)造函數(shù)和普通方法的字節(jié)碼內(nèi)容、 還包括一些在類、 實例、 接口初始化時用到的特殊方法。
方法區(qū)是 JVM 對內(nèi)存的“邏輯劃分” , 在 JDK1.7 及之前很多開發(fā)者都習(xí)慣將方法區(qū)稱為“永久代”, 是因為在 HotSpot 虛擬機中, 設(shè)計人員使用了永久代來實現(xiàn)了 JVM 規(guī)范的方法區(qū)。 在 JDK1.8 及以后使用了元空間來實現(xiàn)方法區(qū)。
1、Class 常量池(靜態(tài)常量池)
在 class 文件中除了有類的版本、 字段、 方法和接口等描述信息外, 還有一項信息是常量池 (Constant Pool Table), 用于存放編譯期間生成的各種字面量和符號引用。
字面量: 給基本類型變量賦值的方式就叫做字面量或者字面值。
比如: String a=“b” , 這里“b”就是字符串字面量, 同樣類推還有整數(shù)字面值、 浮點類型字面量、 字符字面量。
符號引用 : 符號引用以一組符號來描述所引用的目標。 符號引用可以是任何形式的字面量, JAVA 在編譯的時候一個每個 java 類都會被編譯成一個 class文件, 但在編譯的時候虛擬機并不知道所引用類的地址(實際地址), 就用符號引用來代替, 而在類的解析階段(后續(xù) JVM 類加載會具體講到) 就是為了把這個符號引用轉(zhuǎn)化成為真正的地址的階段。
一個 java 類(假設(shè)為 People 類) 被編譯成一個 class 文件時, 如果 People 類引用了 Tool 類, 但是在編譯時 People 類并不知道引用類的實際內(nèi)存地址, 因此只能使用符號引用(org.simple.Tool) 來代替。 而在類裝載器裝載 People 類時, 此時可以通過虛擬機獲取 Tool 類的實際內(nèi)存地址, 因此便可以既將符號org.simple.Tool 替換為 Tool 類的實際內(nèi)存地址。
符號引用主要包括:
- 被模塊導(dǎo)出或者開放的包(package)
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
- 方法句柄和方法類型
- 動態(tài)調(diào)用點和動態(tài)常量
常量表中的數(shù)據(jù)結(jié)構(gòu):
2、運行時常量池
運行時常量池( Runtime Constant Pool) 是每一個類或接口的常量池( Constant_Pool) 的運行時表示形式, 它包括了若干種不同的常量: 從編譯期可知的數(shù)值字面量到必須運行期解析后才能獲得的方法或字段引用。
編譯期生成的各種字面量和符號引用,這部分內(nèi)容在類加載后存放到方法區(qū)的運行時常量池。
運行時常量池是方法區(qū)的一部分。 運行時常量池相對于 Class 常量池的另外一個重要特征是具備動態(tài)性 。
在 JDK1.8 中, 使用元空間代替永久代來實現(xiàn)方法區(qū), 但是方法區(qū)并沒有改變, 變動的只是方法區(qū)中內(nèi)容的物理存放位置, 但是運行時常量池和字符串常量池被移動到了堆中。 但是不論它們物理上如何存放, 邏輯上還是屬于方法區(qū)的。
3、字符串常量池
以 JDK1.8 為例, 字符串常量池是存放在堆中, 并且與 java.lang.String 類有很大關(guān)系。 設(shè)計這塊內(nèi)存區(qū)域的原因在于: String 對象作為 Java 語言中重要的數(shù)據(jù)類型, 是內(nèi)存中占據(jù)空間最大的一個對象。 高效地使用字符串, 可以提升系統(tǒng)的整體性能。
所以要徹底弄懂, 我們的重心其實在于深入理解 String。
堆是JVM 上最大的內(nèi)存區(qū)域, 我們申請的幾乎所有的對象, 都是在這里存儲的。 我們常說的垃圾回收, 操作的對象就是堆。
堆空間一般是程序啟動時, 就申請了, 但是并不一定會全部使用。 堆一般設(shè)置成可伸縮的。
隨著對象的頻繁創(chuàng)建, 堆空間占用的越來越多, 就需要不定期的對不再使用的對象進行回收。 這個在 Java 中, 就叫作 GC( Garbage Collection) 。那一個對象創(chuàng)建的時候, 到底是在堆上分配, 還是在棧上分配呢? 這和兩個方面有關(guān): 對象的類型和在 Java 類中存在的位置。
Java 的對象可以分為基本數(shù)據(jù)類型和普通對象。對于普通對象來說, JVM 會首先在堆上創(chuàng)建對象, 然后在其他地方使用的其實是它的引用。 比如, 把這個引用保存在虛擬機棧的局部變量表中。對于基本數(shù)據(jù)類型來說( byte、 short、 int、 long、 float、 double、 char), 有兩種情況。當你在方法體內(nèi)聲明了基本數(shù)據(jù)類型的對象, 它就會在棧上直接分配。 其他情況, 都是在堆上分配。
直接內(nèi)存有一種更加科學(xué)的叫法, 堆外內(nèi)存。
JVM 在運行時, 會從操作系統(tǒng)申請大塊的堆內(nèi)存, 進行數(shù)據(jù)的存儲; 同時還有虛擬機棧、 本地方法棧和程序計數(shù)器, 這塊稱之為棧區(qū)。 操作系統(tǒng)剩余的內(nèi)存也就是堆外內(nèi)存。
它不是虛擬機運行時數(shù)據(jù)區(qū)的一部分, 也不是 java 虛擬機規(guī)范中定義的內(nèi)存區(qū)域; 如果使用了 NIO,這塊區(qū)域會被頻繁使用, 在 java 堆內(nèi)可以用directByteBuffer 對象直接引用并操作;這塊內(nèi)存不受 java 堆大小限制, 但受本機總內(nèi)存的限制, 可以通過
-XX:MaxDirectMemorySize 來設(shè)置(默認與堆內(nèi)存最大值一樣) , 所以也會出現(xiàn) OOM 異
常。
1、 直接內(nèi)存主要是通過 DirectByteBuffer 申請的內(nèi)存, 可以使用參數(shù)“MaxDirectMemorySize” 來限制它的大小。
2、 其他堆外內(nèi)存, 主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申請的內(nèi)存。
堆外內(nèi)存的泄漏是非常嚴重的, 它的排查難度高、 影響大, 甚至?xí)斐芍鳈C的死亡。 同時, 要注意 Oracle 之前計劃在 Java 9 中去掉 sun.misc.Unsafe API。 這里刪除 sun.misc.Unsafe 的原因之一是使 Java 更加安全, 并且有替代方案。
目前我們主要針對的 JDK1.8, JDK1.9 暫時不放入討論范圍中, 我們大致知道 java 的發(fā)展即可。
三、棧和堆區(qū)別
1、功能
以棧幀的方式存儲方法調(diào)用的過程, 并存儲方法調(diào)用過程中基本數(shù)據(jù)類型的變量(int、 short、 long、 byte、 float、 double、 boolean、 char 等) 以及對象的引用變量, 其內(nèi)存分配在棧上, 變量出了作用域就會自動釋放;
而堆內(nèi)存用來存儲 Java 中的對象。 無論是成員變量, 局部變量, 還是類變量, 它們指向的對象都存儲在堆內(nèi)存中;
2、 線程獨享還是共享
棧內(nèi)存歸屬于單個線程, 每個線程都會有一個棧內(nèi)存, 其存儲的變量只能在其所屬線程中可見, 即棧內(nèi)存可以理解成線程的私有內(nèi)存。
堆內(nèi)存中的對象對所有線程可見。 堆內(nèi)存中的對象可以被所有線程訪問。
3、空間大小
棧的內(nèi)存要遠遠小于堆內(nèi)存