成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

17張圖帶你了解,JVM 運行時數據區

原創 精選
開發 前端
JVM 會把Java的字節碼加載到運行時數據區內,這個內存區域分為:方法區、堆、虛擬機棧、本地方法棧以及程序計數器。

開篇

眾所周知,Java程序的執行需要依賴于JVM(Java 虛擬機)。JVM 會將Java源代碼編譯成字節碼文件,然后使用類加載器將其加載到運行時數據區中執行,垃圾收集器也會針對運行時數據區進行對象回收的工作。今天就來說說JVM的運行時數據區。

運行時數據區概述

在計算機世界中,內存是十分重要的系統資源,它承載著操作系統和應用程序實時運行的責任。JVM內存布局規定了Java在運行過程中內存申請、分配、管理的策略,從而保證了JVM的高效穩定運行。

Java虛擬機在執行Java程序的過程中,會將涉及到的數據劃分到不同的內存區域去管理,在這些數據區域,有些是隨著虛擬機啟動而創建,虛擬機關閉而銷毀。還有一部分是隨著線程生命周期創建銷毀的。這部分區域就是接下來要講的Java虛擬機的運行時數據區。

圖1 運行時數據區

如圖1所示,紅色的部分就是運行時數據區,它包括:方法區、堆、虛擬機棧、本地方法棧以及程序計數器五個部分。

圖1中標注為黃色的方法區和堆是線程間共享的,也就是說它們會隨著虛擬機啟動而創建,隨著虛擬機退出而銷毀。橙色部分為每個線程單獨享有的,即它們與線程是一一對應的,會隨著線程開始和結束而創建和銷毀。在HotSpot JVM中,每個線程都與操作系統的本地線程直接映射,例如:有一個Java線程準備好執行時,就有一個操作系統的本地線程被創建并且與Java 線程對應,當Java線程執行終止后,本地線程也會被回收。同時操作系統負責線程調度,及分配對應的CPU執行線程,一旦操作系統的本地線程初始化成功,它就會調用Java線程中的的run()方法去執行Java線程。

褐色部分的執行引擎就負責讀取指令并且交由CPU執行,它包括解釋器、JIT(即時編譯器),GC(垃圾回收器)。而另外一個褐色的本地庫接口會提供Java程序調用的native方法。

另外,運行時數據區的劃分也隨著JDK的發展不斷變遷,如圖2 所示, JDK 1.6、JDK 1.7、JDK 1.8 的內存劃分都會有所不同。

圖2 運行時數據區的變遷

如圖2 所示,在JDK 1.8 中加入了元數據區的概念,將原來保存在方法區中的運行時常量池和類常量池都包括其中。

虛擬機棧

上面介紹了JVM 運行時數據區的概念和組成,接下來一次介紹每個組成部分,首先從虛擬機棧開始。

每個Java線程都會對應一個虛擬機棧,換句話說多個線程就對應多個虛擬機棧。上面講過了虛擬機棧是線程私有,虛擬機棧中包含多個棧幀(Stack Frame),每一個棧幀是為方法執行而創建的,棧幀中描述的是Java方法執行的內存模型。每個方法從調用開始直到完成的全過程都對應著一個棧幀。棧幀是用來管理Java程序的運行,并保存方法的局部變量、部分結果、并參與方法的調用與返回。在活動線程中,只有一個棧幀是處于活躍狀態的,也就是說只有位于棧頂的棧幀才是有效的,稱為當前棧幀,與這個棧幀相關聯的方法稱為當前方法。執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作。

如圖3 所示,每個Java 方法都會對應一個棧幀,左邊的四個方法就對應了四個棧幀,從下往上依次是方法調用的順序,最終方法1 會調用方法4, 此時正在執行方法4 ,它對應的棧幀4 就是“當前棧幀”,就是出于活躍狀態的,其包含了局部變量表、操作數棧、動態鏈接以及返回地址等信息。

圖3 棧幀結構

局部變量表

它定義為數字數組,主要用于存儲方法參數和定義在方體內的局部變量,包含基本數據類型,對象引用,以及returnAddress類型。它建立在線程的棧上,是線程的私有數據,因此不存在數據的安全問題。

局部變量表所需的容量在編譯期間確定,在運行期間是不改變其容量。方法嵌套調用的次數由棧的容量來決定,例如圖3就進行了4個方法的嵌套,也就是說棧越大,方法嵌套調用次數越多。對一個函數而言,它的參數和局部變量越多,對應的棧幀就越大。因此,函數調用就會占用更多的棧空間。局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞。當方法調用結束后,隨著方法棧幀的銷毀,局部變量表也會隨之銷毀。

操作數棧

它是一個后進先出的棧,在方法執行的過程中,根據字節碼指令、往棧中寫入或取出數據,即入棧/出棧。字節碼指令將值壓入操作棧,其余的字節碼指令將操作數取出棧,進行操作之后再將結果壓入棧。操作包括:復制、交換、求和等。

這樣講比較抽象,來看一個具體的例子。

如圖4 所示,生成一個testAdd 方法,給變量i和j 分別賦值為1 和2 ,然后讓其相加并且把結果賦值給k。

圖4 操作數棧代碼

使用jclasslib反編譯上面的代碼得到圖5 的結果。

圖5 jclasslib反編譯結果

如圖6 所示,當執行地址 0 的時候操作指令為bipush,此時程序寄存器的地址顯示為0 ,bipush 命令將 1 壓入到操作數棧的頂部。

圖6

如圖7 所示,當指令地址到2 的時候,程序寄存器顯示為2, 此時執行istore_1 的指令,將棧頂的數字1 保存到局部變量表中。

圖7

如圖8所示,指令地址執行到3 的時候,程序寄存器為3 , bipush指令把2 壓入到操作數棧的頂部。

圖8

在指令地址為5 的時候,程序寄存器的值為5, istore_2指令將操作數棧中的2 保存到局部變量表中的2 的位置。

圖9

如圖10所示,指令地址為6 的時候,執行iload_1 指令獲取局部變量表中 位置為1 的值,也就是1 并且把它放到操作數棧的頂部。

圖10

如圖11所示,指令地址為7 的時候,執行iload_2 指令,從局部變量表2 的位置取出值2 放到操作數棧的頂部。

圖11

如圖12 所示,在指令地址為8 時,執行iadd 指令,將操作數棧的兩個數字1和2 相加結果為3,并且將其放到操作數棧的頂部。

圖12

如圖13 所示,接著執行指令地址 9 , istore_3 執行之后將操作數棧頂的3 保存到局部變量表3 的位置,完成相加的操作,最后通過指令地址10 中的return指令返回方法。

圖13

動態鏈接

在介紹動態鏈接之前先說說靜態鏈接,即字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,且運行期間保持不變時。這種情況下將調用方法的符號引用轉換為直接引用的過程稱之為靜態鏈接。但是,如果被調用方法在編譯期間無法被確定下來,只能在程序運行時將調用方法的符號引用轉換為直接引用,由于這種引用轉換的過程具備動態性,被稱為動態鏈接。

如圖14所示,上面是反編譯的字節碼部分,對應的#3、#6、#5等等就是符號引用,下面的Constant pool就是常量池。在Java源文件被編譯成字節碼文件時,所有的變量和方法引用都作為符號引用保存在class文件的常量池中。例如在指令第9行會執行invokevirtual的指令,對應的符號引用就是#7,所對應常量池中的#7 就是Methodref,也就是方法引用,這里對應的方法是com.itcast.java.DynamicLinkTest中的methodA方法。

圖14 從字節碼到常量池中的方法引用

如圖15所示,當字節碼文件被加載后,字節碼文件中的一些數據,如類型信息、域信息、方法信息等,就會被放置到方法區中。而棧幀中的當前類常量池引用(Current Class Constant Pool Reference)保存的是方法符號引用,真正的方法引用放在了方法區(Method Area)中的方法引用(method reference)中了,這個方法引用是為了支持代碼的動態鏈接。動態鏈接就是將符號引用轉化為直接引用。

圖15 棧幀中的當前類常量池引用對應方法區中的方法引用

JVM之所以這么設計是因為字節碼文件需要數據支持的量會很大,因此不能直接將這些數據存放到字節碼中。針對方法的引用創建符號引用,這個符號引用放在棧幀的常量池引用中,而實際的方法和符號引用的對照表卻放在方法區的常量池中,這樣字節碼就可以通過常量池中的對照關系找到引用的方法,并且也不會增加棧幀的容量。

方法返回地址

當一個方法開始執行后,可以通過兩種方式退出該方法。第一種是執行引擎遇到方法返回的字節碼指令,此時返回值會傳遞到上層調用者,這種方式稱為正常完成出口。另外一種退出方式是在方法執行中遇到異常,這個異常在方法體內沒有得到處理,就會導致方法退出,這種方式稱為異常完成出口。由于是異常退出,就不會給上層調用者任何返回值。無論采取上面那種退出方式,方法都會到處調用它的位置,程序才能繼續執行。方法在返回的時候需要在棧幀中保存一些信息,用來恢復調用該方法的上層方法的執行狀態。這里可以通過方法調用者的程序計數器存放返回地址,如果是正常退出方法,上層方法會從程序計數器中保存的地址繼續執行接下來的步驟。如果是異常退出的情況,返回地址就需要異常處理器來確定了。

程序計數器

有了上面虛擬機棧的講解,對于程序計數器的理解會相對簡單點。記得在虛擬機棧中的操作數棧的例子中,提到了使用程序計數器記錄操作指令的地址。程序計數器就是一塊較小的內存空間,它是當前線程執行的字節碼的行號(操作指令的地址)指示器。在棧幀中字節碼解釋器就是通過改變計數器的值來選去下一條要執行的字節碼指令的,例如:分支、循環、跳轉、異常處理、線程恢復等。

上面講虛擬機棧的時候提到過,多個執行的Java線程就是多個虛擬機棧,每個棧中存在多個棧幀,在一個時刻只有一個棧幀執行,也就是當前棧幀。也就是說在一個時刻一個處理只會對一個線程中的一個幀棧執行一條指令,而每個棧幀都會維護一個屬于自己的程序計數器,這個計數器就是來記錄指令執行的地址的。每個線程的計數器不會相互影響,這也保證了在Java 多線程進行切換的時候,每個線程都能夠保證正確的指令地址被讀取。

如圖 16所示,在invokevirtual的框圖中存在多個線程,每個線程就是一個虛擬機棧,每個線程中包含多個Frame 也就是棧幀,針對每個線程都會維護一個PC Registers也就是程序寄存器,它會記錄指令地址信息,從而讓方法實現:跳轉、分支、循環、異常處理和線程恢復的功能。

圖16 程序計數器

本地方法棧

本地方法棧與虛擬機棧所發揮的作用是非常相似的,它們之間的區別是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧為虛擬機所使用到的Native方法服務。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。

說白了,本地方法(Native Method)就是一個Java調用非Java代碼的接口。 當Java應用需要與Java之外的環境交互時就需要使用本地方法,特別與底層系統、操作系統以及硬件打交道時就會用到本地方法。大家可以把本地方法理解為一種交流機制:它提供了一個對外的簡潔的接口,讓我們無需去了解Java應用之外的細節。

那么JVM是如何使用Native Method的呢?當一個類第一次被使用時,類的字節碼會被加載到內存,在字節碼的入口維持著該類所有方法描述符的list,包括:方法代碼來源,參數,方法描述符(例如:public)等等。

如果方法描述符是native,同時描述符塊將有一個指向該方法實現的指針,而具體實現在DLL文件內,此時DLL文件會被操作系統加載到Java程序的地址空間里。當一個帶有本地方法的類被加載時,其相關的DLL并未被加載,因此指向方法實現的指針并不會被設置。當本地方法被調用之前, DLL才會被加載,即通過調用java.system.loadLibrary()實現的。

堆和方法區

上面說的虛擬機棧、程序計數器和本地方法棧都是線程私有的,而接下來說的方法區和堆是線程共享的。這里把堆和方法區合起來說。

Java堆是Java虛擬機所管理內存中最大的一塊,在虛擬機啟動時創建,被所有線程共享。Java對象實例以及數組都在堆上分配。堆的大小可以是固定的,也可以根據計算的需要進行擴展,如果不需要更大的堆,則可以收縮。堆的內存不需要是連續的。Java虛擬機實現可以為程序員或用戶提供對堆初始大小的控制,如果可以動態擴展或收縮堆,還可以控制堆的最大和最小大小。

Java堆是垃圾收集器管理的主要區域,所以也被稱為GC堆。從內存回收的角度來看,由于現在收集器基本都采用分代收集算法,所以Java堆中還可以細分為:新生代和老年代;新生代再細分就是:Eden空間、From Survivor空間、ToSurvivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)。不論如何劃分,都與存放內容無關,無論哪個區域,存放的都仍然是對象實例;進一步劃分的目的是為了更好的回收內存,或者更快地分配內存。

對于堆中垃圾回收的部分這里不展開說明,后面會有文章去介紹。

方法區

方法區和堆一樣是線程共享的內存區域,它用來存放被虛擬機加載的類型信息、運行時常量池、靜態變量、JIT代碼緩存、域信息、方法信息等。方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,有如下特點:

  • 方法區在JVM啟動的時候被創建,并且它的實際的物理內存空間和Java堆區一樣都可以是不連續的。
  • 方法區的大小,和堆空間一樣,可以選擇固定大小和可擴展。
  • 方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機就會拋出內存溢出錯誤:

java.lang.OutOfMemoryError:PermGenspace或者 java.lang.OutOfMemoryError: Metaspace。

  • 關閉JVM就會釋放這個區域的內存。

這里把堆、方法區和虛擬機棧的關系整理一下。如圖17 所示,在右邊創建了AppMain 類,在運行時JVM 會把AppMain的信息放入到方法區,因為方法區會存放類型信息。同時main 的方法本身也會放入到方法區。接下來的new Sample(“測試1”)的語句中Sample的自定義對象會放到堆里面,而對應的test1 應用會放入到虛擬機棧中,對應的test1.printName()方法的執行會在虛擬機棧中的棧幀中通過指令執行完成。另外下面的class Sample也是放到方法區中的,聲明的private name,其中name的引用放在虛擬機棧中,name對應的對象放在堆中。對應的printName方法是放在方法區中的。

圖17 棧、堆、方法區關系

總結

JVM 會把Java的字節碼加載到運行時數據區內,這個內存區域分為:方法區、堆、虛擬機棧、本地方法棧以及程序計數器。堆里面放對象,也是垃圾回收器要處理的對象;方法區放類型、方法描述、方法本體;程序計數器負責記錄虛擬機棧中指令執行的地址;虛擬機棧對應Java執行的線程,對象的引用都保存在棧幀中,通過指令地址和指令執行方法中的內容;本地方法棧用來調用Java 之外的系統級別的接口。

譯者介紹

崔皓,51CTO社區編輯,資深架構師,擁有18年的軟件開發和架構經驗,10年分布式架構經驗。曾任惠普技術專家。樂于分享,撰寫了很多熱門技術文章,閱讀量超過60萬。《分布式架構原理與實踐》作者。

責任編輯:武曉燕 來源: 51CTO技術棧
相關推薦

2021-07-14 07:21:57

JVM運行數據

2018-11-22 12:07:37

Java虛擬機結構

2024-11-26 08:31:36

2022-01-17 22:09:50

JVM方法區數據

2020-06-28 07:39:44

Kafka分布式消息

2021-08-13 08:15:23

JVM 虛擬機Java

2017-12-07 18:02:01

Python新手運行時錯誤

2021-05-07 17:11:19

負載均衡運維服務

2015-07-20 15:44:46

Swift框架MJExtension反射

2021-09-18 08:02:49

Go程序工具

2024-03-21 09:15:58

JS運行的JavaScrip

2018-10-25 09:04:56

Java虛擬機JVM

2018-12-13 09:27:31

后臺服務架構

2021-03-08 09:52:55

架構運維技術

2021-07-04 22:27:42

存儲BookKeeper系統

2019-07-12 09:30:12

DashboardDockerDNS

2021-09-11 15:38:23

容器運行鏡像開放

2009-07-09 13:52:41

Inside JVM

2021-11-12 08:38:26

一致性哈希算法數據結構

2014-10-09 09:43:05

虛擬機遷移
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 一区二区三区高清 | 九九热在线观看视频 | 欧美极品在线观看 | 久久免费视频观看 | 久久久久久成人 | 少妇性l交大片免费一 | 日韩最新网站 | 成人久久18免费网站麻豆 | 久久一区二区av | 欧州一区二区三区 | 国产午夜久久 | av网站推荐 | 国产99久久久国产精品 | 国产视频h | www312aⅴ欧美在线看 | 久久亚洲国产精品日日av夜夜 | 欧美日韩精品久久久免费观看 | 正在播放国产精品 | 亚洲a在线视频 | 伦理午夜电影免费观看 | 日韩精品一区二区三区高清免费 | 91精品久久| japanhdxxxx裸体 | 亚洲一区在线观看视频 | 99精品视频在线 | 色播99 | 中文字幕精品一区久久久久 | 久久国产日韩 | 欧美视频区 | 日本精品在线一区 | 国产精品亚洲一区二区三区在线观看 | 国产伦精品一区二区三区精品视频 | 亚洲一区欧美 | 国产在线视频99 | 精品国产乱码久久久久久蜜柚 | 亚洲高清视频在线观看 | 精品国产免费一区二区三区演员表 | 99视频 | 国产露脸国语对白在线 | 日本一区二区三区在线观看 | 午夜不卡一区二区 |