JVM 內存詳解
簡(jiǎn)介
JVM 是 Java Virtual Machine(Java虛擬機)的縮寫(xiě),JVM 是一種用于計算設備的規范,它是一個(gè)虛構出來(lái)的計算機,是通過(guò)在實(shí)際的計算機上仿真模擬各種計算機功能來(lái)實(shí)現的。
JVM 內存結構
JVM 的內存空間分為 3 大部分:
- 堆內存:
- 方法區
- 棧內存
其中棧內存可以再細分為Java 虛擬機棧和本地方法棧,堆內存可以劃分為新生代和老年代,新生代中還可以再次劃分為 Eden 區、From Survivor 區和 To Survivor 區。
其中一部分是線(xiàn)程共享的,包括 Java 堆和方法區;另一部分是線(xiàn)程私有的,包括虛擬機棧和本地方法棧,以及程序計數器這一小部分內存。
堆內存(Heap)
Java 堆(Java Heap)是 Java 虛擬機所管理的內存中最大的一塊。堆是被所有線(xiàn)程共享的區域,實(shí)在虛擬機啟動(dòng)時(shí)創(chuàng )建的。堆里面存放的都是對象的實(shí)例(new 出來(lái)的對象都存在堆中)。
此內存區域的唯一目的就是存放對象實(shí)例(new 的對象),幾乎所有的對象實(shí)例都在這里分配內存。
堆內存分為兩個(gè)部分:年輕代和老年代。我們平常所說(shuō)的垃圾回收,主要回收的就是堆區。更細一點(diǎn)劃分新生代又可劃分為 Eden 區和 2 個(gè) Survivor 區(From Survivor 和 To Survivor)。
下圖中的 Perm 代表的是永久代,但是注意永久代并不屬于堆內存中的一部分,同時(shí) jdk1.8 之后永久代已經(jīng)被移除,存放到了元空間里面。
新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過(guò)參數 –XX:NewRatio 來(lái)指定).
默認的,Eden : from : to = 8 : 1 : 1 ( 可以通過(guò)參數 –XX:SurvivorRatio 來(lái)設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。
方法區(Method Area)
方法區也稱(chēng)”永久代“,它用于存儲虛擬機加載的類(lèi)信息、常量、靜態(tài)變量、是各個(gè)線(xiàn)程共享的內存區域。
在 JDK8 之前的 HotSpot JVM,存放這些”永久的”的區域叫做“永久代(permanent generation)”。永久代是一片連續的堆空間,在 JVM 啟動(dòng)之前通過(guò)在命令行設置參數-XX:MaxPermSize 來(lái)設定永久代最大可分配的內存空間,默認大小是 64M(64 位 JVM 默認是 85M)。
隨著(zhù) JDK8 的到來(lái),JVM 不再有 永久代(PermGen)。但類(lèi)的元數據信息(metadata)還在,只不過(guò)不再是存儲在連續的堆空間上,而是移動(dòng)到叫做“Metaspace”的本地內存(Native memory)。
方法區或永生代相關(guān)設置:
- -XX:PermSize=64MB 最小尺寸,初始分配
- -XX:MaxPermSize=256MB 最大允許分配尺寸,按需分配
- XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 設置垃圾不回收
- 默認大小
- -server 選項下默認 MaxPermSize 為 64m
- -client 選項下默認 MaxPermSize 為 32m
虛擬機棧(JVM Stack)
Java 虛擬機棧是線(xiàn)程私有,生命周期與線(xiàn)程相同。創(chuàng )建線(xiàn)程的時(shí)候就會(huì )創(chuàng )建一個(gè) Java 虛擬機棧。
虛擬機執行 Java 程序的時(shí)候,每個(gè)方法都會(huì )創(chuàng )建一個(gè)棧幀,棧幀存放在 Java 虛擬機棧中,通過(guò)壓棧出棧的方式進(jìn)行方法調用。
棧幀又分為一下幾個(gè)區域:局部變量表、操作數棧、動(dòng)態(tài)連接、方法出口等。
局部變量表
局部變量表是變量值的存儲空間,用于存放方法參數和方法內部定義的局部變量。在 Java 編譯成 class 文件的時(shí)候,就在方法的 Code 屬性的 max_locals 數據項中確定該方法需要分配的最大局部變量表的容量。
局部變量表的容量以變量槽(Slot)為最小單位,32 位虛擬機中一個(gè) Slot 可以存放 32 位(4 字節)以?xún)鹊臄祿?lèi)型( boolean、byte、char、short、int、float、reference 和 returnAddress 八種)
對于 64 位長(cháng)度的數據類(lèi)型(long,double),虛擬機會(huì )以高位對齊方式為其分配兩個(gè)連續的 Slot 空間,也就是相當于把一次 long 和 double 數據類(lèi)型讀寫(xiě)分割成為兩次 32 位讀寫(xiě)。
reference 類(lèi)型虛擬機規范沒(méi)有明確說(shuō)明它的長(cháng)度,但一般來(lái)說(shuō),虛擬機實(shí)現至少都應當能從此引用中直接或者間接地查找到對象在 Java 堆中的起始地址索引和方法區中的對象類(lèi)型數據。
Slot 是可以重用的,當 Slot 中的變量超出了作用域,那么下一次分配 Slot 的時(shí)候,將會(huì )覆蓋原來(lái)的數據。Slot 對對象的引用會(huì )影響 GC(要是被引用,將不會(huì )被回收)。 系統不會(huì )為局部變量賦予初始值(實(shí)例變量和類(lèi)變量都會(huì )被賦予初始值)。也就是說(shuō)不存在類(lèi)變量那樣的準備階段。
系統不會(huì )為局部變量賦予初始值(實(shí)例變量和類(lèi)變量都會(huì )被賦予初始值)。也就是說(shuō)不存在類(lèi)變量那樣的準備階段。
操作數棧
操作數棧和局部變量表一樣,在編譯時(shí)期就已經(jīng)確定了該方法所需要分配的局部變量表的最大容量。
操作數棧的每一個(gè)元素可用是任意的 Java 數據類(lèi)型,包括 long 和 double。32 位數據類(lèi)型所占的棧容量為 1,64 位數據類(lèi)型占用的棧容量為 2。
當一個(gè)方法剛剛開(kāi)始執行的時(shí)候,這個(gè)方法的操作數棧是空的,在方法執行的過(guò)程中,會(huì )有各種字節碼指令往操作數棧中寫(xiě)入和提取內容,也就是出棧 / 入棧操作(例如:在做算術(shù)運算的時(shí)候是通過(guò)操作數棧來(lái)進(jìn)行的,又或者在調用其它方法的時(shí)候是通過(guò)操作數棧來(lái)進(jìn)行參數傳遞的)。
在概念模型里,棧幀之間是應該是相互獨立的,不過(guò)大多數虛擬機都會(huì )做一些優(yōu)化處理,使局部變量表和操作數棧之間有部分重疊,這樣在進(jìn)行方法調用的時(shí)候可以直接共用參數,而不需要做額外的參數復制等工作。重疊過(guò)程如圖所示:
動(dòng)態(tài)連接
每個(gè)棧幀都包含一個(gè)指向運行時(shí)常量池中該棧幀所屬方法的引用,Class 文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中方法的符號引用為參數。這些符號引用一部分會(huì )在類(lèi)加載階段或者第一次使用的時(shí)候就轉化為直接引用(靜態(tài)方法,私有方法等),這種轉化稱(chēng)為靜態(tài)解析,另一部分將在每一次運行期間轉化為直接引用,這部分稱(chēng)為動(dòng)態(tài)連接。
方法返回地址
當一個(gè)方法開(kāi)始執行后,只有兩種方式可以退出這個(gè)方法:
- 執行引擎遇到任意一個(gè)方法返回的字節碼指令:傳遞給上層的方法調用者,是否有返回值和返回值類(lèi)型將根據遇到何種方法來(lái)返回指令決定,這種退出的方法稱(chēng)為正常完成出口。
- 方法執行過(guò)程中遇到異常: 無(wú)論是 Java 虛擬機內部產(chǎn)生的異常還是代碼中 thtrow 出的異常,只要在本方法的異常表中沒(méi)有搜索到匹配的異常處理器,就會(huì )導致方法退出,這種退出的方式稱(chēng)為異常完成出口,一個(gè)方法若使用該方式退出,是不會(huì )給上層調用者任何返回值的。無(wú)論使用那種方式退出方法,都要返回到方法被調用的位置,程序才能繼續執行。方法返回時(shí)可能會(huì )在棧幀中保存一些信息,用來(lái)恢復上層方法的執行狀態(tài)。一般方法正常退出的時(shí)候,調用者的 pc 計數器的值可以作為返回地址,幀棧中很有可能會(huì )保存這個(gè)計數器的值作為返回地址。方法退出的過(guò)程就是棧幀在虛擬機棧上的出棧過(guò)程,因此退出時(shí)的操作可能有:恢復上層方法的局部變量表和操作數棧,把返回值壓入調用者的操作數棧每條整 pc 計數器的值指向調用該方法的后一條指令。
本地方法棧(Native Stack)
本地方法棧(Native Method Stacks)與虛擬機棧所發(fā)揮的作用是非常相似的,其區別不過(guò)是虛擬機棧為虛擬機執行 Java 方法(也就是字節碼)服務(wù),而本地方法棧則是為虛擬機使用到的 Native 方法服務(wù)。
程序計數器(PC Register)
程序計數器就是記錄當前線(xiàn)程執行程序的位置,改變計數器的值來(lái)確定執行的下一條指令,比如循環(huán)、分支、方法跳轉、異常處理,線(xiàn)程恢復都是依賴(lài)程序計數器來(lái)完成。
Java 虛擬機多線(xiàn)程是通過(guò)線(xiàn)程輪流切換并分配處理器執行時(shí)間的方式實(shí)現的。為了線(xiàn)程切換能恢復到正確的位置,每條線(xiàn)程都需要一個(gè)獨立的程序計數器,所以它是線(xiàn)程私有的。
直接內存
直接內存并不是虛擬機內存的一部分,也不是 Java 虛擬機規范中定義的內存區域。jdk1.4 中新加入的 NIO,引入了通道與緩沖區的 IO 方式,它可以調用 Native 方法直接分配堆外內存,這個(gè)堆外內存就是本機內存,不會(huì )影響到堆內存的大小。
JVM 內存參數設置
- -Xms 設置堆的最小空間大小。
- -Xmx 設置堆的最大空間大小。
- -Xmn:設置年輕代大小
- -XX:NewSize 設置新生代最小空間大小。
- -XX:MaxNewSize 設置新生代最大空間大小。
- -XX:PermSize 設置永久代最小空間大小。
- -XX:MaxPermSize 設置永久代最大空間大小。
- -Xss 設置每個(gè)線(xiàn)程的堆棧大小
- -XX:+UseParallelGC:選擇垃圾收集器為并行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用并發(fā)收集,而年老代仍舊使用串行收集。
- -XX:ParallelGCThreads=20:配置并行收集器的線(xiàn)程數,即:同時(shí)多少個(gè)線(xiàn)程一起進(jìn)行垃圾回收。此值最好配置與處理器數目相等。
參考文檔
