日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深入浅出JVM-内存模型

發布時間:2023/12/10 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入浅出JVM-内存模型 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

JVM內存模型

    • 總圖
    • 程序計數寄存器
    • Java虛擬機棧(JVM Stack)
      • 定義
      • 特點
    • 本地方法棧(Native Method Stack)
    • Java堆(Java Heap)
      • 定義
      • 特點
    • 方法區
      • 定義
      • 特性
      • 可能拋出的異常
    • 直接內存(Direct Memory)
    • Metaspace (元空間)
      • 元空間特色
      • GC
      • 元空間內存分配模型
    • 從GC角度看Java堆
    • JVM關閉

總圖

程序計數寄存器

Java虛擬機棧(JVM Stack)

定義

相對于基于寄存器的運行環境來說,JVM是基于棧結構的運行環境
棧結構移植性更好,可控性更強
JVM中的虛擬機棧是描述Java方法執行的內存區域,它是線程私有的

棧中的元素用于支持虛擬機進行方法調用,每個方法從開始調用到執行完成的過程,就是棧幀從入棧到出棧的過程
在活動線程中,只有位于棧頂的幀才是有效的,稱為當前棧幀
正在執行的方法稱為當前方法
棧幀是方法運行的基本結構

在執行引擎運行時,所有指令都只能針對當前棧幀進行操作
StackOverflowError表示請求的棧溢出,導致內存耗盡,通常出現在遞歸方法中
JVM能夠橫掃千軍,虛擬機棧就是它的心腹大將,當前方法的棧幀,都是正在戰斗的戰場,其中的操作棧是參與戰斗的士兵

虛擬機棧通過壓/出棧的方式,對每個方法對應的活動棧幀進行運算處理,方法正常執行結束,肯定會跳轉到另一個棧幀上
在執行的過程中,如果出現異常,會進行異常回溯,返回地址通過異常處理表確定
棧幀在整個JVM體系中的地位頗高,包括局部變量表、操作棧、動態連接、方法返回地址等

  • 局部變量表
    存放方法參數和局部變量
    相對于類屬性變量的準備階段和初始化階段來說,局部變量沒有準備階段,必須顯式初始化
    如果是非靜態方法,則在index[0]位置上存儲的是方法所屬對象的實例引用,隨后存儲的是參數和局部變量
    字節碼指令中的STORE指令就是將操作棧中計算完成的局部變量寫回局部變量表的存儲空間內

  • 操作棧
    操作棧是一個初始狀態為空的桶式結構棧
    在方法執行過程中,會有各種指令往棧中寫入和提取信息
    JVM的執行引擎是基于棧的執行引擎,其中的棧指的就是操作棧
    字節碼指令集的定義都是基于棧類型的,棧的深度在方法元信息的stack屬性中
    下面用一段簡單的代碼說明操作棧與局部變量表的交互

  • 詳細的字節碼操作順序如下:

第1處說明:局部變量表就像個中藥柜,里面有很多抽屜,依次編號為0, 1, 2,3,.,. n
字節碼指令istore_ 1就是打開1號抽屜,把棧頂中的數13存進去
棧是一個很深的豎桶,任何時候只能對桶口元素進行操作,所以數據只能在棧頂進行存取
某些指令可以直接在抽屜里進行,比如inc指令,直接對抽屜里的數值進行+1操作
程序員面試過程中,常見的i++和++i的區別,可以從字節碼上對比出來

  • iload_ 1從局部變量表的第1號抽屜里取出一個數,壓入棧頂,下一步直接在抽屜里實現+1的操作,而這個操作對棧頂元素的值沒有影響
    所以istore_ 2只是把棧頂元素賦值給a

  • 表格右列,先在第1號抽屜里執行+1操作,然后通過iload_ 1 把第1號抽屜里的數壓入棧頂,所以istore_ 2存入的是+1之后的值
    這里延伸一個信息,i++并非原子操作。即使通過volatile關鍵字進行修飾,多個線程同時寫的話,也會產生數據互相覆蓋的問題.

  • 動態連接
    每個棧幀中包含一個在常量池中對當前方法的引用,目的是支持方法調用過程的動態連接

  • 方法返回地址
    方法執行時有兩種退出情況
    1.正常退出
    正常執行到任何方法的返回字節碼指令,如RETURN、IRETURN、ARETURN等
    2.異常退出
    無論何種退出情況,都將返回至方法當前被調用的位置。方法退出的過程相當于彈出當前棧幀

退出可能有三種方式:

  • 返回值壓入,上層調用棧幀
  • 異常信息拋給能夠處理的棧幀
  • PC計數器指向方法調用后的下一條指令

Java虛擬機棧是描述Java方法運行過程的內存模型

Java虛擬機棧會為每一個即將運行的Java方法創建“棧幀”
用于存儲該方法在運行過程中所需要的一些信息

  • 局部變量表
  • 存放基本數據類型變量、引用類型的變量、returnAddress類型的變量
  • 操作數棧
  • 動態鏈接
  • 當前方法的常量池指針
  • 當前方法的返回地址
  • 方法出口等信息

每一個方法從被調用到執行完成的過程,都對應著一個個棧幀在JVM棧中的入棧和出棧過程

注意:人們常說,Java的內存空間分為“棧”和“堆”,棧中存放局部變量,堆中存放對象。
這句話不完全正確!這里的“堆”可以這么理解,但這里的“棧”就是現在講的虛擬機棧,或者說Java虛擬機棧中的局部變量表部分.
真正的Java虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息.

特點

局部變量表的創建是在方法被執行的時候,隨著棧幀的創建而創建.
而且表的大小在編譯期就確定,在創建的時候只需分配事先規定好的大小即可.
在方法運行過程中,表的大小不會改變

Java虛擬機棧會出現兩種異常

  • StackOverFlowError
    若Java虛擬機棧的內存大小不允許動態擴展,那么當線程請求的棧深度大于虛擬機允許的最大深度時(但內存空間可能還有很多),就拋出此異常
  • OutOfMemoryError
    若Java虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出OutOfMemoryError異常
    Java虛擬機棧也是線程私有的,每個線程都有各自的Java虛擬機棧,而且隨著線程的創建而創建,隨著線程的死亡而死亡.

本地方法棧(Native Method Stack)

本地方法棧和Java虛擬機棧實現的功能與拋出異常幾乎相同
只不過虛擬機棧是為虛擬機執行Java方法(也就是字節碼)服務,本地方法區則為虛擬機使用到的Native方法服務.

在JVM內存布局中,也是線程對象私有的,但是虛擬機棧“主內”,而本地方法棧“主外”
這個“內外”是針對JVM來說的,本地方法棧為Native方法服務
線程開始調用本地方法時,會進入一個不再受JVM約束的世界
本地方法可以通過JNI(Java Native Interface)來訪問虛擬機運行時的數據區,甚至可以調用寄存器,具有和JVM相同的能力和權限
當大量本地方法出現時,勢必會削弱JVM對系統的控制力,因為它的出錯信息都比較黑盒.
對于內存不足的情況,本地方法棧還是會拋出native heap OutOfMemory

最著名的本地方法應該是System.currentTimeMillis(),JNI 使Java深度使用OS的特性功能,復用非Java代碼
但是在項目過程中,如果大量使用其他語言來實現JNI,就會喪失跨平臺特性,威脅到程序運行的穩定性
假如需要與本地代碼交互,就可以用中間標準框架進行解耦,這樣即使本地方法崩潰也不至于影響到JVM的穩定
當然,如果要求極高的執行效率、偏底層的跨進程操作等,可以考慮設計為JNI調用方式

Java堆(Java Heap)

Heap是OOM故障最主要的發源地,它存儲著幾乎所有的實例對象,堆由垃圾收集器自動回收,堆區由各子線程共享使用
通常情況下,它占用的空間是所有內存區域中最大的,但如果無節制地創建大量對象,也容易消耗完所有的空間
堆的內存空間既可以固定大小,也可運行時動態地調整,通過如下參數設定初始值和最大值,比如

-Xms256M. -Xmx1024M

其中-X表示它是JVM運行參數

  • ms是memorystart的簡稱 最小堆容量
  • mx是memory max的簡稱 最大堆容量

但是在通常情況下,服務器在運行過程中,堆空間不斷地擴容與回縮,勢必形成不必要的系統壓力,所以在線上生產環境中,JVM的Xms和Xmx設置成一樣大小,避免在GC后調整堆大小時帶來的額外壓力

堆分成兩大塊:新生代和老年代
對象產生之初在新生代,步入暮年時進入老年代,但是老年代也接納在新生代無法容納的超大對象

新生代= 1個Eden區+ 2個Survivor區
絕大部分對象在Eden區生成,當Eden區裝填滿的時候,會觸發Young GC。垃圾回收的時候,在Eden區實現清除策略,沒有被引用的對象則直接回收。依然存活的對象會被移送到Survivor區,這個區真是名副其實的存在
Survivor 區分為S0和S1兩塊內存空間,送到哪塊空間呢?每次Young GC的時候,將存活的對象復制到未使用的那塊空間,然后將當前正在使用的空間完全清除,交換兩塊空間的使用狀態
如果YGC要移送的對象大于Survivor區容量上限,則直接移交給老年代
假如一些沒有進取心的對象以為可以一直在新生代的Survivor區交換來交換去,那就錯了。每個對象都有一個計數器,每次YGC都會加1。

-XX:MaxTenuringThreshold

參數能配置計數器的值到達某個閾值的時候,對象從新生代晉升至老年代。如果該參數配置為1,那么從新生代的Eden區直接移至老年代。默認值是15,可以在Survivor 區交換14次之后,晉升至老年代

若Survivor區無法放下,或者超大對象的閾值超過上限,則嘗試在老年代中進行分配;
如果老年代也無法放下,則會觸發Full Garbage Collection(Full GC);
如果依然無法放下,則拋OOM.

堆出現OOM的概率是所有內存耗盡異常中最高的
出錯時的堆內信息對解決問題非常有幫助,所以給JVM設置運行參數-

XX:+HeapDumpOnOutOfMemoryError

讓JVM遇到OOM異常時能輸出堆內信息

在不同的JVM實現及不同的回收機制中,堆內存的劃分方式是不一樣的

存放所有的類實例及數組對象
除了實例數據,還保存了對象的其他信息,如Mark Word(存儲對象哈希碼,GC標志,GC年齡,同步鎖等信息),Klass Pointy(指向存儲類型元數據的指針)及一些字節對齊補白的填充數據(若實例數據剛好滿足8字節對齊,則可不存在補白)

特點

Java虛擬機所需要管理的內存中最大的一塊.

堆內存物理上不一定要連續,只需要邏輯上連續即可,就像磁盤空間一樣.
堆是垃圾回收的主要區域,所以也被稱為GC堆.

堆的大小既可以固定也可以擴展,但主流的虛擬機堆的大小是可擴展的(通過-Xmx和-Xms控制),因此當線程請求分配內存,但堆已滿,且內存已滿無法再擴展時,就拋出OutOfMemoryError.

線程共享
整個Java虛擬機只有一個堆,所有的線程都訪問同一個堆.
它是被所有線程共享的一塊內存區域,在虛擬機啟動時創建.
而程序計數器、Java虛擬機棧、本地方法棧都是一個線程對應一個

定義

Java虛擬機規范中定義方法區是堆的一個邏輯部分,但是別名Non-Heap(非堆),以與Java堆區分.
方法區中存放已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據.

特點

  • 線程共享
    方法區是堆的一個邏輯部分,因此和堆一樣,都是線程共享的.整個虛擬機中只有一個方法區.
  • 永久代
    方法區中的信息一般需要長期存在,而且它又是堆的邏輯分區,因此用堆的劃分方法,我們把方法區稱為永久代.
  • 內存回收效率低
    Java虛擬機規范對方法區的要求比較寬松,可以不實現垃圾收集.
    方法區中的信息一般需要長期存在,回收一遍內存之后可能只有少量信息無效.
    對方法區的內存回收的主要目標是:對常量池的回收和對類型的卸載
    和堆一樣,允許固定大小,也允許可擴展的大小,還允許不實現垃圾回收。

當方法區內存空間無法滿足內存分配需求時,將拋出OutOfMemoryError異常.

方法區

定義

運行時常量池是方法區的一部分.
方法區中存放三種數據:類信息、常量、靜態變量、即時編譯器編譯后的代碼.其中常量存儲在運行時常量池中.

我們知道,.java文件被編譯之后生成的.class文件中除了包含:類的版本、字段、方法、接口等信息外,還有一項就是常量池
常量池中存放編譯時期產生的各種字面量和符號引用,.class文件中的常量池中的所有的內容在類被加載后存放到方法區的運行時常量池中。
PS:int age = 21;//age是一個變量,可以被賦值;21就是一個字面值常量,不能被賦值;
int final pai = 3.14;//pai就是一個符號常量,一旦被賦值之后就不能被修改。

Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池( Constant pool table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入運行時常量池中存放。運行時常量池相對于class文件常量池的另外一個特性是具備動態性,java語言并不要求常量一定只有編譯器才產生,也就是并非預置入class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中。

在近三個JDK版本(6、7、8)中, 運行時常量池的所處區域一直在不斷的變化,
在JDK6時它是方法區的一部分
7又把他放到了堆內存中
8之后出現了元空間,它又回到了方法區。
其實,這也說明了官方對“永久代”的優化從7就已經開始了

特性

class文件中的常量池具有動態性.
Java并不要求常量只能在編譯時候產生,Java允許在運行期間將新的常量放入方法區的運行時常量池中.
String類中的intern()方法就是采用了運行時常量池的動態性.當調用 intern 方法時,如果池已經包含一個等于此 String 對象的字符串,則返回池中的字符串.否則,將此 String 對象添加到池中,并返回此 String 對象的引用.

可能拋出的異常

運行時常量池是方法區的一部分,所以會受到方法區內存的限制,因此當常量池無法再申請到內存時就會拋出OutOfMemoryError異常.

我們一般在一個類中通過public static final來聲明一個常量。這個類被編譯后便生成Class文件,這個類的所有信息都存儲在這個class文件中。

當這個類被Java虛擬機加載后,class文件中的常量就存放在方法區的運行時常量池中。而且在運行期間,可以向常量池中添加新的常量。如:String類的intern()方法就能在運行期間向常量池中添加字符串常量。

當運行時常量池中的某些常量沒有被對象引用,同時也沒有被變量引用,那么就需要垃圾收集器回收。

直接內存(Direct Memory)

直接內存不是虛擬機運行時數據區的一部分,也不是JVM規范中定義的內存區域,但在JVM的實際運行過程中會頻繁地使用這塊區域.而且也會拋OOM

在JDK 1.4中加入了NIO(New Input/Output)類,引入了一種基于管道和緩沖區的IO方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在堆里的DirectByteBuffer對象作為這塊內存的引用來操作堆外內存中的數據.
這樣能在一些場景中顯著提升性能,因為避免了在Java堆和Native堆中來回復制數據.

  • 綜上看來
    程序計數器、Java虛擬機棧、本地方法棧是線程私有的,即每個線程都擁有各自的程序計數器、Java虛擬機棧、本地方法區。并且他們的生命周期和所屬的線程一樣。
    而堆、方法區是線程共享的,在Java虛擬機中只有一個堆、一個方法棧。并在JVM啟動的時候就創建,JVM停止才銷毀。

Metaspace (元空間)

在JDK8,元空間的前身Perm區已經被淘汰,在JDK7及之前的版本中,只有Hotspot才有Perm區(永久代),它在啟動時固定大小,很難進行調優,并且Full GC時會移動類元信息

在某些場景下,如果動態加載類過多,容易產生Perm區的OOM.
比如某個實際Web工程中,因為功能點比較多,在運行過程中,要不斷動態加載很多的類,經常出現致命錯誤:

Exception in thread ‘dubbo client x.x connector' java.lang.OutOfMemoryError: PermGenspac

為解決該問題,需要設定運行參數

-XX:MaxPermSize= l280m

如果部署到新機器上,往往會因為JVM參數沒有修改導致故障再現。不熟悉此應用的人排查問題時往往苦不堪言,除此之外,永久代在GC過程中還存在諸多問題

所以,JDK8使用元空間替換永久代.區別于永久代,元空間在本地內存中分配.
也就是說,只要本地內存足夠,它不會出現像永久代中java.lang.OutOfMemoryError: PermGen space

同樣的,對永久代的設置參數PermSize和MaxPermSize也會失效
在JDK8及以上版本中,設定MaxPermSize參數,JVM在啟動時并不會報錯,但是會提示:

Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize=2560m; support was removed in 8.0

默認情況下,“元空間”的大小可以動態調整,或者使用新參數MaxMetaspaceSize來限制本地內存分配給類元數據的大小.

在JDK8里,Perm 區所有內容中

  • 字符串常量移至堆內存
  • 其他內容包括類元信息、字段、靜態屬性、方法、常量等都移動至元空間

    比如上圖中的Object類元信息、靜態屬性System.out、整型常量000000等
    圖中顯示在常量池中的String,其實際對象是被保存在堆內存中的。

元空間特色

  • 充分利用了Java語言規范:類及相關的元數據的生命周期與類加載器的一致
  • 每個類加載器都有它的內存區域-元空間
  • 只進行線性分配
  • 不會單獨回收某個類(除了重定義類 RedefineClasses 或類加載失敗)
  • 沒有GC掃描或壓縮
  • 元空間里的對象不會被轉移
  • 如果GC發現某個類加載器不再存活,會對整個元空間進行集體回收

GC

  • Full GC時,指向元數據指針都不用再掃描,減少了Full GC的時間
  • 很多復雜的元數據掃描的代碼(尤其是CMS里面的那些)都刪除了
  • 元空間只有少量的指針指向Java堆
  • 這包括:類的元數據中指向java.lang.Class實例的指針;數組類的元數據中,指向java.lang.Class集合的指針。
  • 沒有元數據壓縮的開銷
  • 減少了GC Root的掃描(不再掃描虛擬機里面的已加載類的目錄和其它的內部哈希表)
  • G1回收器中,并發標記階段完成后就可以進行類的卸載

元空間內存分配模型

  • 絕大多數的類元數據的空間都在本地內存中分配

  • 用來描述類元數據的對象也被移除

  • 為元數據分配了多個映射的虛擬內存空間

  • 為每個類加載器分配一個內存塊列表

  • 塊的大小取決于類加載器的類型

  • Java反射的字節碼存取器(sun.reflect.DelegatingClassLoader )占用內存更小

  • 空閑塊內存返還給塊內存列表

  • 當元空間為空,虛擬內存空間會被回收

  • 減少了內存碎片
    最后,從線程共享的角度來看

  • 堆和元空間是所有線程共享的

  • 虛擬機棧、本地方法棧、程序計數器是線程內部私有的

從這個角度看一下Java內存結構

從GC角度看Java堆

堆和方法區都是線程共享的區域,主要用來存放對象的相關信息。我們知道,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序運行期間才能知道會創建哪些對象,因此, 這部分的內存和回收都是動態的,垃圾收集器所關注的就是這部分內存(本節后續所說的“內存”分配與回收也僅指這部分內存)。而在JDK1.7和1.8對這部分內存的分配也有所不同,下面我們來詳細看一下

JVM關閉

  • 正常關閉:當最后一個非守護線程結束或調用了System.exit或通過其他特定于平臺的方式,比如ctrl+c。
  • 強制關閉:調用Runtime.halt方法,或在操作系統中直接kill(發送single信號)掉JVM進程。
  • 異常關閉:運行中遇到RuntimeException 異常等
    在某些情況下,我們需要在JVM關閉時做一些掃尾的工作,比如刪除臨時文件、停止日志服務。為此JVM提供了關閉鉤子(shutdown hocks)來做這些事件。

Runtime類封裝java應用運行時的環境,每個java應用程序都有一個Runtime類實例,使用程序能與其運行環境相連。

關閉鉤子本質上是一個線程(也稱為hock線程),可以通過Runtime的addshutdownhock (Thread hock)向主jvm注冊一個關閉鉤子。hock線程在jvm正常關閉時執行,強制關閉不執行。

對于在jvm中注冊的多個關閉鉤子,他們會并發執行,jvm并不能保證他們的執行順序。

總結

以上是生活随笔為你收集整理的深入浅出JVM-内存模型的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。