jvm性能调优 - 02JVM中内存区域
文章目錄
- Pre
- 什么是JVM的內存區域劃分?
- 存放類的方法區
- 執行代碼指令用的程序計數器
- Java虛擬機棧
- Java堆內存
- 示例演示核心內存區域的全流程
- 其他內存區域
- 思考題
Pre
上一篇文章我們聊了一下JVM類加載這塊的機制,先簡單回顧一下。
大家需要搞明白的是,在什么情況下會觸發類的加載?加載之后的驗證、準備和解析分別是干什么的?
尤為重要的是準備階段和初始化階段,是如何為類分配內存空間的?然后類加載器的規則是什么?
現在互聯網大廠面試一般都必定會考核JVM相關的知識積累, 所以在了解完了JVM的類加載機制之后,先一起來看看JVM的內存區域劃分,這個基本上是互聯網公司面試必問。
什么是JVM的內存區域劃分?
其實這個問題非常簡單,JVM在運行我們寫好的代碼時,他是必須使用多塊內存空間的,不同的內存空間用來放不同的數據,然后配合我們寫的代碼流程,才能讓我們的系統運行起來。
舉個最簡單的例子,比如咱們現在知道了JVM會加載類到內存里來供后續運行, 這些類加載到內存以后,放到哪兒去了呢?
所以JVM里就必須有一塊內存區域,用來存放我們寫的那些類。
繼續來看,我們的代碼運行起來時,是不是需要執行我們寫的一個一個的方法?
那么運行方法的時候,方法里面有很多變量之類的東西,是不是需要放在某個內存區域里?
接著如果我們寫的代碼里創建一些對象,這些對象是不是也需要內存空間來存放?
這就是為什么JVM中必須劃分出來不同的內存區域,它是為了我們寫好的代碼在運行過程中根據需要來使用的。
接下來,我們就依次看看JVM中有哪些內存區域。
存放類的方法區
這個方法區是在JDK 1.8以前的版本里,代表JVM中的一塊區域。
主要是放從“.class”文件里加載進來的類,還會有一些類似常量池的東西放在這個區域里。
但是在JDK 1.8以后,這塊區域的名字改了,叫做“Metaspace”,可以認為是“元數據空間”這樣的意思。當然這里主要還是存放我們自己寫的各種類相關的信息。
舉個例子,還是跟我們之前說的那樣,假設我們有一個“Kafka.class”類和“ReplicaManager.class”類,類似下面的代碼。
這兩個類加載到JVM后,就會放在這個方法區中,大家看下圖:
執行代碼指令用的程序計數器
繼續假設我們的代碼是如下所示:
上面那段代碼首先會存在于“.java”后綴的文件里,這個文件就是java源代碼文件。
但是這個文件是面向我們程序員的,計算機他是看不懂你寫的這段代碼的。
所以此時就得通過編譯器,把“.java”后綴的源代碼文件編譯為“.class”后綴的字節碼文件。
這個“.class”后綴的字節碼文件里,存放的就是對你寫出來的代碼編譯好的字節碼了。
字節碼才是計算器可以理解的一種語言,而不是我們寫出來的那一堆代碼。
字節碼看起來大概是下面這樣的,跟上面的代碼無關,就是一個示例而已,給大家感受一下。
這段字節碼就是讓大家知道“.java”翻譯成的“.class”是大概什么樣子的。
比如“0: aload_0”這樣的,就是“字節碼指令”,他對應了一條一條的機器指令,計算機只有讀到這種機器碼指令,才知道具體應該要干什么。
比如字節碼指令可能會讓計算機從內存里讀取某個數據,或者把某個數據寫入到內存里去,都有可能,各種各樣的指令就會指示計算機去干各種各樣的事情。
所以現在大家首先明白一點:我們寫好的Java代碼會被翻譯成字節碼,對應各種字節碼指令
現在Java代碼通過JVM跑起來的第一件事情就明確了, 首先Java代碼被編譯成字節碼指令,然后字節碼指令一定會被一條一條執行,這樣才能實現我們寫好的代碼執行的效果。
所以當JVM加載類信息到內存之后,實際就會使用自己的字節碼執行引擎,去執行我們寫的代碼編譯出來的代碼指令,如下圖。
那么在執行字節碼指令的時候,JVM里就需要一個特殊的內存區域了,那就是“程序計數器”
這個程序計數器就是用來記錄當前執行的字節碼指令的位置的,也就是記錄目前執行到了哪一條字節碼指令。
我們通過一張圖來說明:
大家都知道JVM是支持多個線程的,所以其實你寫好的代碼可能會開啟多個線程并發執行不同的代碼,所以就會有多個線程來并發的執行不同的代碼指令
因此每個線程都會有自己的一個程序計數器,專門記錄當前這個線程目前執行到了哪一條字節碼指令了
下圖更加清晰的展示出了他們之間的關系。
Java虛擬機棧
Java代碼在執行的時候,一定是線程來執行某個方法中的代碼
哪怕就是下面的代碼,也會有一個main線程來執行main()方法里的代碼
在main線程執行main()方法的代碼指令的時候,就會通過main線程對應的程序計數器記錄自己執行的指令位置。
但是在方法里,我們經常會定義一些方法內的局部變量
比如在上面的main()方法里,其實就有一個“replicaManager”局部變量,他是引用一個ReplicaManager實例對象的,關于這個對象我們先別去管他,先來看方法和局部變量。
因此,JVM必須有一塊區域是來保存每個方法內的局部變量等數據的,這個區域就是Java虛擬機棧
每個線程都有自己的Java虛擬機棧,比如這里的main線程就會有自己的一個Java虛擬機棧,用來存放自己執行的那些方法的局部變量。
如果線程執行了一個方法,就會對這個方法調用創建對應的一個棧幀
棧幀里就有這個方法的局部變量表 、操作數棧、動態鏈接、方法出口等東西
這里大家先不用全都理解,我們先關注局部變量。
比如main線程執行了main()方法,那么就會給這個main()方法創建一個棧幀,壓入main線程的Java虛擬機棧
同時在main()方法的棧幀里,會存放對應的“replicaManager”局部變量
然后假設main線程繼續執行ReplicaManager對象里的方法,比如下面這樣,就在“loadReplicasFromDisk”方法里定義了一個局部變量:“hasFinishedLoad”
那么main線程在執行上面的“loadReplicasFromDisk”方法時,就會為“loadReplicasFromDisk”方法創建一個棧幀壓入線程自己的Java虛擬機棧里面去。
然后在棧幀的局部變量表里就會有“hasFinishedLoad”這個局部變量。
接著如果“loadReplicasFromDisk”方法調用了另外一個“isLocalDataCorrupt()”方法 ,這個方法里也有自己的局部變量
比如下面這樣的代碼:
那么這個時候會給“isLocalDataCorrupt”方法又創建一個棧幀,壓入線程的Java虛擬機棧里。
而且“isLocalDataCorrupt”方法的棧幀的局部變量表里會有一個“isCorrupt”變量,這是“isLocalDataCorrupt”方法的局部變量
整個過程,如下圖所示:
接著如果“isLocalDataCorrupt”方法執行完畢了,就會把“isLocalDataCorrupt”方法對應的棧幀從Java虛擬機棧里給出棧
然后如果“loadReplicasFromDisk”方法也執行完畢了,就會把“loadReplicasFromDisk”方法也從Java虛擬機棧里出棧。
上述就是JVM中的“Java虛擬機棧”這個組件的作用:調用執行任何方法時,都會給方法創建棧幀然后入棧
在棧幀里存放了這個方法對應的局部變量之類的數據,包括這個方法執行的其他相關的信息,方法執行完畢之后就出棧。
咱們再來看一個圖,了解一下每個線程在執行代碼時,除了程序計數器以外,還搭配了一個Java虛擬機棧內存區域來存放每個方法中的局部變量表。
Java堆內存
現在大家都知道了,main線程執行main()方法的時候,會有自己的程序計數器。
此外,還會依次把main()方法,loadReplicasFromDisk()方法,isLocalDataCorrupt()方法的棧幀壓入Java虛擬機棧,存放每個方法的局部變量。
那么接著我們就得來看JVM中的另外一個非常關鍵的區域,就是Java堆內存,這里就是存放我們在代碼中創建的各種對象的
比如下面的代碼:
上面的“new ReplicaManager()”這個代碼就是創建了一個ReplicaManager類的對象實例,這個對象實例里面會包含一些數據,如下面的代碼所示。
這個“ReplicaManager”類里的“replicaCount”就是屬于這個對象實例的一個數據。
類似ReplicaManager這樣的對象實例,就會存放在Java堆內存里。
Java堆內存區域里會放入類似ReplicaManager的對象,然后我們因為在main方法里創建了ReplicaManager對象的,那么在線程執行main方法代碼的時候,就會在main方法對應的棧幀的局部變量表里,讓一個引用類型的“replicaManager”局部變量來存放ReplicaManager對象的地址
相當于你可以認為局部變量表里的“replicaManager”指向了Java堆內存里的ReplicaManager對象
還是給大家來一張圖,更加清晰一些:
示例演示核心內存區域的全流程
其實我們把上面的那個圖和下面的這個總的大圖一起串起來看看,還有配合整體的代碼,我們來捋一下整體的流程,大家就會覺得很清晰。
首先,你的JVM進程會啟動,就會先加載你的Kafka類到內存里。然后有一個main線程,開始執行你的Kafka中的main()方法。 main線程是關聯了一個程序計數器的,那么他執行到哪一行指令,就會記錄在這里
其次,就是main線程在執行main()方法的時候,會在main線程關聯的Java虛擬機棧里,壓入一個main()方法的棧幀。接著會發現需要創建一個ReplicaManager類的實例對象,此時會加載ReplicaManager類到內存里來。
然后會創建一個ReplicaManager的對象實例分配在Java堆內存里,并且在main()方法的棧幀里的局部變量表引入一個“replicaManager”變量,讓他引用ReplicaManager對象在Java堆內存中的地址。
看到這里,大家結合上面的兩個圖理解一下。
接著,main線程開始執行ReplicaManager對象中的方法,會依次把自己執行到的方法對應的棧幀壓入自己的Java虛擬機棧 . 執行完方法之后再把方法對應的棧幀從Java虛擬機棧里出棧。
其實大家理解了這個過程,那么JVM中的各個核心內存區域的功能和對應的我們的Java代碼之間的關系,就徹底理解了
其他內存區域
其實在JDK很多底層API里,比如IO相關的,NIO相關的,網絡Socket相關的
如果大家去看他內部的源碼,會發現很多地方都不是Java代碼了,而是走的native方法去調用本地操作系統里面的一些方法,可能調用的都是c語言寫的方法,或者一些底層類庫
比如下面這樣的:public native int hashCode();
在調用這種native方法的時候,就會有線程對應的本地方法棧,這個里面也是跟Java虛擬機棧類似的,也是存放各種native方法的局部變量表之類的信息。
還有一個區域,是不屬于JVM的,通過NIO中的allocateDirect這種API,可以在Java堆外分配內存空間。然后,通過Java虛擬機里的DirectByteBuffer來引用和操作堆外內存空間。
其實很多技術都會用這種方式,因為有一些場景下,堆外內存分配可以提升性能。
思考題
們學習了JVM中的各個內存區域,那我們在Java堆內存中分配的那些對象,到底會占用多少內存?一般怎么來計算和估算我們的系統創建的對象對內存占用的一個壓力呢?
這個其實很簡單,一個對象對內存空間的占用,大致分為兩塊:
-
一個是對象自己本身的一些信息
-
一個是對象的實例變量作為數據占用的空間
比如對象頭,如果在64位的linux操作系統上,會占用16字節,然后如果你的實例對象內部有個int類型的實例變量,他會占用4個字節,如果是long類型的實例變量,會占用8個字節。如果是數組、Map之類的,那么就會占用更多的內存了。
另外JVM對這塊有很多優化的地方,比如補齊機制、指針壓縮機制,等等…
總結
以上是生活随笔為你收集整理的jvm性能调优 - 02JVM中内存区域的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jvm性能调优 - 01类加载机制Rev
- 下一篇: jvm性能调优 - 03垃圾回收机制