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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

java 取栈顶元素_《Java实战之内存模型》详解篇

發(fā)布時(shí)間:2024/10/8 java 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java 取栈顶元素_《Java实战之内存模型》详解篇 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

內(nèi)存是非常重要的系統(tǒng)資源,是硬盤和CPU的中間倉庫及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實(shí)時(shí)運(yùn)行

JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過程中內(nèi)存申請(qǐng)、分配、管理的策略,保證了JVM的高效穩(wěn)定運(yùn)行

不同的JVM對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在著部分差異

結(jié)合JVM虛擬機(jī)規(guī)范,來探討經(jīng)典的JVM內(nèi)存布局

JVM內(nèi)存模型-1

JVM內(nèi)存模型-2

1 Program Counter Register (程序計(jì)數(shù)寄存器)

Register 的命名源于CPU的寄存器,CPU只有把數(shù)據(jù)裝載到寄存器才能夠運(yùn)行

寄存器存儲(chǔ)指令相關(guān)的現(xiàn)場(chǎng)信息,由于CPU時(shí)間片輪限制,眾多線程在并發(fā)執(zhí)行過程中,任何一個(gè)確定的時(shí)刻,一個(gè)處理器或者多核處理器中的一個(gè)內(nèi)核,只會(huì)執(zhí)行某個(gè)線程中的一條指令。這樣必然導(dǎo)致經(jīng)常中斷或恢復(fù),如何保證分毫無差呢?

每個(gè)線程在創(chuàng)建后,都會(huì)產(chǎn)生自己的程序計(jì)數(shù)器和棧幀,程序計(jì)數(shù)器用來存放執(zhí)行指令的偏移量和行號(hào)指示器等,線程執(zhí)行或恢復(fù)都要依賴程序計(jì)數(shù)器。程序計(jì)數(shù)器在各個(gè)線程之間互不影響,此區(qū)域也不會(huì)發(fā)生內(nèi)存溢出異常。

1.1. 定義

程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,可看作當(dāng)前線程正在執(zhí)行的字節(jié)碼的行號(hào)指示器

如果當(dāng)前線程正在執(zhí)行的是

  • Java方法
  • 計(jì)數(shù)器記錄的就是當(dāng)前線程正在執(zhí)行的字節(jié)碼指令的地址
  • 本地方法
  • 那么程序計(jì)數(shù)器值為undefined

1.2. 作用

程序計(jì)數(shù)器有兩個(gè)作用

  • 字節(jié)碼解釋器通過改變程序計(jì)數(shù)器來依次讀取指令,從而實(shí)現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
  • 在多線程的情況下,程序計(jì)數(shù)器用于記錄當(dāng)前線程執(zhí)行的位置,從而當(dāng)線程被切換回來的時(shí)候能夠知道該線程上次運(yùn)行到哪兒了。

1.3. 特點(diǎn)

一塊較小的內(nèi)存空間

線程私有。每條線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器。

是唯一一個(gè)不會(huì)出現(xiàn)OOM的內(nèi)存區(qū)域。

生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而死亡。

2. Java虛擬機(jī)棧(JVM Stack)

2.1. 定義

相對(duì)于基于寄存器的運(yùn)行環(huán)境來說,JVM是基于棧結(jié)構(gòu)的運(yùn)行環(huán)境

棧結(jié)構(gòu)移植性更好,可控性更強(qiáng)

JVM中的虛擬機(jī)棧是描述Java方法執(zhí)行的內(nèi)存區(qū)域,它是線程私有的

棧中的元素用于支持虛擬機(jī)進(jìn)行方法調(diào)用,每個(gè)方法從開始調(diào)用到執(zhí)行完成的過程,就是棧幀從入棧到出棧的過程

在活動(dòng)線程中,只有位于棧頂?shù)膸攀怯行У?#xff0c;稱為當(dāng)前棧幀

正在執(zhí)行的方法稱為當(dāng)前方法

棧幀是方法運(yùn)行的基本結(jié)構(gòu)

在執(zhí)行引擎運(yùn)行時(shí),所有指令都只能針對(duì)當(dāng)前棧幀進(jìn)行操作

StackOverflowError表示請(qǐng)求的棧溢出,導(dǎo)致內(nèi)存耗盡,通常出現(xiàn)在遞歸方法中

JVM能夠橫掃千軍,虛擬機(jī)棧就是它的心腹大將,當(dāng)前方法的棧幀,都是正在戰(zhàn)斗的戰(zhàn)場(chǎng),其中的操作棧是參與戰(zhàn)斗的士兵

操作棧的壓棧與出棧

虛擬機(jī)棧通過壓/出棧的方式,對(duì)每個(gè)方法對(duì)應(yīng)的活動(dòng)棧幀進(jìn)行運(yùn)算處理,方法正常執(zhí)行結(jié)束,肯定會(huì)跳轉(zhuǎn)到另一個(gè)棧幀上

在執(zhí)行的過程中,如果出現(xiàn)異常,會(huì)進(jìn)行異?;厮?#xff0c;返回地址通過異常處理表確定

棧幀在整個(gè)JVM體系中的地位頗高,包括局部變量表、操作棧、動(dòng)態(tài)連接、方法返回地址等

  • 局部變量表
  • 存放方法參數(shù)和局部變量
  • 相對(duì)于類屬性變量的準(zhǔn)備階段和初始化階段來說,局部變量沒有準(zhǔn)備階段,必須顯式初始化
  • 如果是非靜態(tài)方法,則在index[0]位置上存儲(chǔ)的是方法所屬對(duì)象的實(shí)例引用,隨后存儲(chǔ)的是參數(shù)和局部變量
  • 字節(jié)碼指令中的STORE指令就是將操作棧中計(jì)算完成的局部變量寫回局部變量表的存儲(chǔ)空間內(nèi)
  • 操作棧
  • 操作棧是一個(gè)初始狀態(tài)為空的桶式結(jié)構(gòu)棧
  • 在方法執(zhí)行過程中,會(huì)有各種指令往棧中寫入和提取信息
  • JVM的執(zhí)行引擎是基于棧的執(zhí)行引擎,其中的棧指的就是操作棧
  • 字節(jié)碼指令集的定義都是基于棧類型的,棧的深度在方法元信息的stack屬性中

下面用一段簡單的代碼說明操作棧與局部變量表的交互

  • 詳細(xì)的字節(jié)碼操作順序如下:
  • 第1處說明:局部變量表就像個(gè)中藥柜,里面有很多抽屜,依次編號(hào)為0, 1, 2,3,.,. n
  • 字節(jié)碼指令istore_ 1就是打開1號(hào)抽屜,把棧頂中的數(shù)13存進(jìn)去
  • 棧是一個(gè)很深的豎桶,任何時(shí)候只能對(duì)桶口元素進(jìn)行操作,所以數(shù)據(jù)只能在棧頂進(jìn)行存取

某些指令可以直接在抽屜里進(jìn)行,比如inc指令,直接對(duì)抽屜里的數(shù)值進(jìn)行+1操作

程序員面試過程中,常見的i++和++i的區(qū)別,可以從字節(jié)碼上對(duì)比出來

i++和++i的區(qū)別

  • iload_ 1 從局部變量表的第1號(hào)抽屜里取出一個(gè)數(shù),壓入棧頂,下一步直接在抽屜里實(shí)現(xiàn)+1的操作,而這個(gè)操作對(duì)棧頂元素的值沒有影響
  • 所以istore_ 2只是把棧頂元素賦值給a
  • 表格右列,先在第1號(hào)抽屜里執(zhí)行+1操作,然后通過iload_ 1 把第1號(hào)抽屜里的數(shù)壓入棧頂,所以istore_ 2存入的是+1之后的值

這里延伸一個(gè)信息,i++并非原子操作。即使通過volatile關(guān)鍵字進(jìn)行修飾,多個(gè)線程同時(shí)寫的話,也會(huì)產(chǎn)生數(shù)據(jù)互相覆蓋的問題.

  • 動(dòng)態(tài)連接
  • 每個(gè)棧幀中包含一個(gè)在常量池中對(duì)當(dāng)前方法的引用,目的是支持方法調(diào)用過程的動(dòng)態(tài)連接
  • 方法返回地址
  • 方法執(zhí)行時(shí)有兩種退出情況
  • 正常退出
  • 正常執(zhí)行到任何方法的返回字節(jié)碼指令,如RETURN、IRETURN、ARETURN等
  • 異常退出

無論何種退出情況,都將返回至方法當(dāng)前被調(diào)用的位置。方法退出的過程相當(dāng)于彈出當(dāng)前棧幀

退出可能有三種方式:

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

Java虛擬機(jī)棧是描述Java方法運(yùn)行過程的內(nèi)存模型

Java虛擬機(jī)棧會(huì)為每一個(gè)即將運(yùn)行的Java方法創(chuàng)建“棧幀”

用于存儲(chǔ)該方法在運(yùn)行過程中所需要的一些信息

  • 局部變量表
  • 存放基本數(shù)據(jù)類型變量、引用類型的變量、returnAddress類型的變量
  • 操作數(shù)棧
  • 動(dòng)態(tài)鏈接
  • 當(dāng)前方法的常量池指針
  • 當(dāng)前方法的返回地址
  • 方法出口等信息

每一個(gè)方法從被調(diào)用到執(zhí)行完成的過程,都對(duì)應(yīng)著一個(gè)個(gè)棧幀在JVM棧中的入棧和出棧過程

注意:人們常說,Java的內(nèi)存空間分為“棧”和“堆”,棧中存放局部變量,堆中存放對(duì)象。

這句話不完全正確!這里的“堆”可以這么理解,但這里的“?!本褪乾F(xiàn)在講的虛擬機(jī)棧,或者說Java虛擬機(jī)棧中的局部變量表部分.

真正的Java虛擬機(jī)棧是由一個(gè)個(gè)棧幀組成,而每個(gè)棧幀中都擁有:局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口信息.

2.2. 特點(diǎn)

局部變量表的創(chuàng)建是在方法被執(zhí)行的時(shí)候,隨著棧幀的創(chuàng)建而創(chuàng)建.

而且表的大小在編譯期就確定,在創(chuàng)建的時(shí)候只需分配事先規(guī)定好的大小即可.

在方法運(yùn)行過程中,表的大小不會(huì)改變

Java虛擬機(jī)棧會(huì)出現(xiàn)兩種異常

  • StackOverFlowError
  • 若Java虛擬機(jī)棧的內(nèi)存大小不允許動(dòng)態(tài)擴(kuò)展,那么當(dāng)線程請(qǐng)求的棧深度大于虛擬機(jī)允許的最大深度時(shí)(但內(nèi)存空間可能還有很多),就拋出此異常
  • OutOfMemoryError
  • 若Java虛擬機(jī)棧的內(nèi)存大小允許動(dòng)態(tài)擴(kuò)展,且當(dāng)線程請(qǐng)求棧時(shí)內(nèi)存用完了,無法再動(dòng)態(tài)擴(kuò)展了,此時(shí)拋出OutOfMemoryError異常

Java虛擬機(jī)棧也是線程私有的,每個(gè)線程都有各自的Java虛擬機(jī)棧,而且隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的死亡而死亡.

3. 本地方法棧(Native Method Stack)

本地方法棧和Java虛擬機(jī)棧實(shí)現(xiàn)的功能與拋出異常幾乎相同

只不過虛擬機(jī)棧是為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),本地方法區(qū)則為虛擬機(jī)使用到的Native方法服務(wù).

在JVM內(nèi)存布局中,也是線程對(duì)象私有的,但是虛擬機(jī)?!爸鲀?nèi)”,而本地方法?!爸魍狻?/p>

這個(gè)“內(nèi)外”是針對(duì)JVM來說的,本地方法棧為Native方法服務(wù)

線程開始調(diào)用本地方法時(shí),會(huì)進(jìn)入一個(gè)不再受JVM約束的世界

本地方法可以通過JNI(Java Native Interface)來訪問虛擬機(jī)運(yùn)行時(shí)的數(shù)據(jù)區(qū),甚至可以調(diào)用寄存器,具有和JVM相同的能力和權(quán)限

當(dāng)大量本地方法出現(xiàn)時(shí),勢(shì)必會(huì)削弱JVM對(duì)系統(tǒng)的控制力,因?yàn)樗某鲥e(cuò)信息都比較黑盒.

對(duì)于內(nèi)存不足的情況,本地方法棧還是會(huì)拋出native heap OutOfMemory

最著名的本地方法應(yīng)該是System.currentTimeMillis(),JNI 使Java深度使用OS的特性功能,復(fù)用非Java代碼

但是在項(xiàng)目過程中,如果大量使用其他語言來實(shí)現(xiàn)JNI,就會(huì)喪失跨平臺(tái)特性,威脅到程序運(yùn)行的穩(wěn)定性

假如需要與本地代碼交互,就可以用中間標(biāo)準(zhǔn)框架進(jìn)行解耦,這樣即使本地方法崩潰也不至于影響到JVM的穩(wěn)定

當(dāng)然,如果要求極高的執(zhí)行效率、偏底層的跨進(jìn)程操作等,可以考慮設(shè)計(jì)為JNI調(diào)用方式

4 Java堆(Java Heap)

Heap是OOM故障最主要的發(fā)源地,它存儲(chǔ)著幾乎所有的實(shí)例對(duì)象,堆由垃圾收集器自動(dòng)回收,堆區(qū)由各子線程共享使用

通常情況下,它占用的空間是所有內(nèi)存區(qū)域中最大的,但如果無節(jié)制地創(chuàng)建大量對(duì)象,也容易消耗完所有的空間

堆的內(nèi)存空間既可以固定大小,也可運(yùn)行時(shí)動(dòng)態(tài)地調(diào)整,通過如下參數(shù)設(shè)定初始值和最大值,比如

-Xms256M. -Xmx1024M

其中-X表示它是JVM運(yùn)行參數(shù)

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

但是在通常情況下,服務(wù)器在運(yùn)行過程中,堆空間不斷地?cái)U(kuò)容與回縮,勢(shì)必形成不必要的系統(tǒng)壓力,所以在線上生產(chǎn)環(huán)境中,JVM的Xms和Xmx設(shè)置成一樣大小,避免在GC后調(diào)整堆大小時(shí)帶來的額外壓力

堆分成兩大塊:新生代和老年代

對(duì)象產(chǎn)生之初在新生代,步入暮年時(shí)進(jìn)入老年代,但是老年代也接納在新生代無法容納的超大對(duì)象

新生代= 1個(gè)Eden區(qū)+ 2個(gè)Survivor區(qū)

絕大部分對(duì)象在Eden區(qū)生成,當(dāng)Eden區(qū)裝填滿的時(shí)候,會(huì)觸發(fā)Young GC。垃圾回收的時(shí)候,在Eden區(qū)實(shí)現(xiàn)清除策略,沒有被引用的對(duì)象則直接回收。依然存活的對(duì)象會(huì)被移送到Survivor區(qū),這個(gè)區(qū)真是名副其實(shí)的存在

Survivor 區(qū)分為S0和S1兩塊內(nèi)存空間,送到哪塊空間呢?每次Young GC的時(shí)候,將存活的對(duì)象復(fù)制到未使用的那塊空間,然后將當(dāng)前正在使用的空間完全清除,交換兩塊空間的使用狀態(tài)

如果YGC要移送的對(duì)象大于Survivor區(qū)容量上限,則直接移交給老年代

假如一些沒有進(jìn)取心的對(duì)象以為可以一直在新生代的Survivor區(qū)交換來交換去,那就錯(cuò)了。每個(gè)對(duì)象都有一個(gè)計(jì)數(shù)器,每次YGC都會(huì)加1。

-XX:MaxTenuringThreshold

參數(shù)能配置計(jì)數(shù)器的值到達(dá)某個(gè)閾值的時(shí)候,對(duì)象從新生代晉升至老年代。如果該參數(shù)配置為1,那么從新生代的Eden區(qū)直接移至老年代。默認(rèn)值是15,可以在Survivor 區(qū)交換14次之后,晉升至老年代

對(duì)象分配與簡要GC流程圖

若Survivor區(qū)無法放下,或者超大對(duì)象的閾值超過上限,則嘗試在老年代中進(jìn)行分配;

如果老年代也無法放下,則會(huì)觸發(fā)Full Garbage Collection(Full GC);

如果依然無法放下,則拋OOM.

堆出現(xiàn)OOM的概率是所有內(nèi)存耗盡異常中最高的

出錯(cuò)時(shí)的堆內(nèi)信息對(duì)解決問題非常有幫助,所以給JVM設(shè)置運(yùn)行參數(shù)-

XX:+HeapDumpOnOutOfMemoryError

讓JVM遇到OOM異常時(shí)能輸出堆內(nèi)信息

在不同的JVM實(shí)現(xiàn)及不同的回收機(jī)制中,堆內(nèi)存的劃分方式是不一樣的

存放所有的類實(shí)例及數(shù)組對(duì)象

除了實(shí)例數(shù)據(jù),還保存了對(duì)象的其他信息,如Mark Word(存儲(chǔ)對(duì)象哈希碼,GC標(biāo)志,GC年齡,同步鎖等信息),Klass Pointy(指向存儲(chǔ)類型元數(shù)據(jù)的指針)及一些字節(jié)對(duì)齊補(bǔ)白的填充數(shù)據(jù)(若實(shí)例數(shù)據(jù)剛好滿足8字節(jié)對(duì)齊,則可不存在補(bǔ)白)

特點(diǎn)

Java虛擬機(jī)所需要管理的內(nèi)存中最大的一塊.

堆內(nèi)存物理上不一定要連續(xù),只需要邏輯上連續(xù)即可,就像磁盤空間一樣.

堆是垃圾回收的主要區(qū)域,所以也被稱為GC堆.

堆的大小既可以固定也可以擴(kuò)展,但主流的虛擬機(jī)堆的大小是可擴(kuò)展的(通過-Xmx和-Xms控制),因此當(dāng)線程請(qǐng)求分配內(nèi)存,但堆已滿,且內(nèi)存已滿無法再擴(kuò)展時(shí),就拋出OutOfMemoryError.

線程共享

整個(gè)Java虛擬機(jī)只有一個(gè)堆,所有的線程都訪問同一個(gè)堆.

它是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建.

而程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧都是一個(gè)線程對(duì)應(yīng)一個(gè)

5 方法區(qū)

5.1 定義

Java虛擬機(jī)規(guī)范中定義方法區(qū)是堆的一個(gè)邏輯部分,但是別名Non-Heap(非堆),以與Java堆區(qū)分.

方法區(qū)中存放已經(jīng)被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù).

5.2 特點(diǎn)

  • 線程共享
  • 方法區(qū)是堆的一個(gè)邏輯部分,因此和堆一樣,都是線程共享的.整個(gè)虛擬機(jī)中只有一個(gè)方法區(qū).
  • 永久代
  • 方法區(qū)中的信息一般需要長期存在,而且它又是堆的邏輯分區(qū),因此用堆的劃分方法,我們把方法區(qū)稱為永久代.
  • 內(nèi)存回收效率低
  • Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的要求比較寬松,可以不實(shí)現(xiàn)垃圾收集.
  • 方法區(qū)中的信息一般需要長期存在,回收一遍內(nèi)存之后可能只有少量信息無效.
  • 對(duì)方法區(qū)的內(nèi)存回收的主要目標(biāo)是:對(duì)常量池的回收和對(duì)類型的卸載

和堆一樣,允許固定大小,也允許可擴(kuò)展的大小,還允許不實(shí)現(xiàn)垃圾回收。

當(dāng)方法區(qū)內(nèi)存空間無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常.

5.3 運(yùn)行時(shí)常量池(Runtime Constant Pool)

5.3.1 定義

運(yùn)行時(shí)常量池是方法區(qū)的一部分.

方法區(qū)中存放三種數(shù)據(jù):類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼.其中常量存儲(chǔ)在運(yùn)行時(shí)常量池中.

我們知道,.java文件被編譯之后生成的.class文件中除了包含:類的版本、字段、方法、接口等信息外,還有一項(xiàng)就是常量池

常量池中存放編譯時(shí)期產(chǎn)生的各種字面量和符號(hào)引用,.class文件中的常量池中的所有的內(nèi)容在類被加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。

PS:int age = 21;//age是一個(gè)變量,可以被賦值;21就是一個(gè)字面值常量,不能被賦值;

int final pai = 3.14;//pai就是一個(gè)符號(hào)常量,一旦被賦值之后就不能被修改。

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

在近三個(gè)JDK版本(6、7、8)中, 運(yùn)行時(shí)常量池的所處區(qū)域一直在不斷的變化,

在JDK6時(shí)它是方法區(qū)的一部分

7又把他放到了堆內(nèi)存中

8之后出現(xiàn)了元空間,它又回到了方法區(qū)。

其實(shí),這也說明了官方對(duì)“永久代”的優(yōu)化從7就已經(jīng)開始了

5.3.2 特性

class文件中的常量池具有動(dòng)態(tài)性.

Java并不要求常量只能在編譯時(shí)候產(chǎn)生,Java允許在運(yùn)行期間將新的常量放入方法區(qū)的運(yùn)行時(shí)常量池中.

String類中的intern()方法就是采用了運(yùn)行時(shí)常量池的動(dòng)態(tài)性.當(dāng)調(diào)用 intern 方法時(shí),如果池已經(jīng)包含一個(gè)等于此 String 對(duì)象的字符串,則返回池中的字符串.否則,將此 String 對(duì)象添加到池中,并返回此 String 對(duì)象的引用.

5.3.3 可能拋出的異常

運(yùn)行時(shí)常量池是方法區(qū)的一部分,所以會(huì)受到方法區(qū)內(nèi)存的限制,因此當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)就會(huì)拋出OutOfMemoryError異常.

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

當(dāng)這個(gè)類被Java虛擬機(jī)加載后,class文件中的常量就存放在方法區(qū)的運(yùn)行時(shí)常量池中。而且在運(yùn)行期間,可以向常量池中添加新的常量。如:String類的intern()方法就能在運(yùn)行期間向常量池中添加字符串常量。

當(dāng)運(yùn)行時(shí)常量池中的某些常量沒有被對(duì)象引用,同時(shí)也沒有被變量引用,那么就需要垃圾收集器回收。

6 直接內(nèi)存(Direct Memory)

直接內(nèi)存不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是JVM規(guī)范中定義的內(nèi)存區(qū)域,但在JVM的實(shí)際運(yùn)行過程中會(huì)頻繁地使用這塊區(qū)域.而且也會(huì)拋OOM

在JDK 1.4中加入了NIO(New Input/Output)類,引入了一種基于管道和緩沖區(qū)的IO方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在堆里的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用來操作堆外內(nèi)存中的數(shù)據(jù).

這樣能在一些場(chǎng)景中顯著提升性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù).

綜上看來

程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧是線程私有的,即每個(gè)線程都擁有各自的程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法區(qū)。并且他們的生命周期和所屬的線程一樣。

而堆、方法區(qū)是線程共享的,在Java虛擬機(jī)中只有一個(gè)堆、一個(gè)方法棧。并在JVM啟動(dòng)的時(shí)候就創(chuàng)建,JVM停止才銷毀。

7 Metaspace (元空間)

在JDK8,元空間的前身Perm區(qū)已經(jīng)被淘汰,在JDK7及之前的版本中,只有Hotspot才有Perm區(qū)(永久代),它在啟動(dòng)時(shí)固定大小,很難進(jìn)行調(diào)優(yōu),并且Full GC時(shí)會(huì)移動(dòng)類元信息

在某些場(chǎng)景下,如果動(dòng)態(tài)加載類過多,容易產(chǎn)生Perm區(qū)的OOM.

比如某個(gè)實(shí)際Web工程中,因?yàn)楣δ茳c(diǎn)比較多,在運(yùn)行過程中,要不斷動(dòng)態(tài)加載很多的類,經(jīng)常出現(xiàn)致命錯(cuò)誤:

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

為解決該問題,需要設(shè)定運(yùn)行參數(shù)

-XX:MaxPermSize= l280m

如果部署到新機(jī)器上,往往會(huì)因?yàn)镴VM參數(shù)沒有修改導(dǎo)致故障再現(xiàn)。不熟悉此應(yīng)用的人排查問題時(shí)往往苦不堪言,除此之外,永久代在GC過程中還存在諸多問題

所以,JDK8使用元空間替換永久代.區(qū)別于永久代,元空間在本地內(nèi)存中分配.

也就是說,只要本地內(nèi)存足夠,它不會(huì)出現(xiàn)像永久代中java.lang.OutOfMemoryError: PermGen space

同樣的,對(duì)永久代的設(shè)置參數(shù) PermSize 和MaxPermSize也會(huì)失效

在JDK8及以上版本中,設(shè)定MaxPermSize參數(shù),JVM在啟動(dòng)時(shí)并不會(huì)報(bào)錯(cuò),但是會(huì)提示:

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

默認(rèn)情況下,“元空間”的大小可以動(dòng)態(tài)調(diào)整,或者使用新參數(shù)MaxMetaspaceSize來限制本地內(nèi)存分配給類元數(shù)據(jù)的大小.

在JDK8里,Perm 區(qū)所有內(nèi)容中

  • 字符串常量移至堆內(nèi)存
  • 其他內(nèi)容包括類元信息、字段、靜態(tài)屬性、方法、常量等都移動(dòng)至元空間
  • 比如上圖中的Object類元信息、靜態(tài)屬性System.out、整型常量000000等
  • 圖中顯示在常量池中的String,其實(shí)際對(duì)象是被保存在堆內(nèi)存中的。

元空間特色

  • 充分利用了Java語言規(guī)范:類及相關(guān)的元數(shù)據(jù)的生命周期與類加載器的一致
  • 每個(gè)類加載器都有它的內(nèi)存區(qū)域-元空間
  • 只進(jìn)行線性分配
  • 不會(huì)單獨(dú)回收某個(gè)類(除了重定義類 RedefineClasses 或類加載失敗)
  • 沒有GC掃描或壓縮
  • 元空間里的對(duì)象不會(huì)被轉(zhuǎn)移
  • 如果GC發(fā)現(xiàn)某個(gè)類加載器不再存活,會(huì)對(duì)整個(gè)元空間進(jìn)行集體回收

GC

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

元空間內(nèi)存分配模型

  • 絕大多數(shù)的類元數(shù)據(jù)的空間都在本地內(nèi)存中分配
  • 用來描述類元數(shù)據(jù)的對(duì)象也被移除
  • 為元數(shù)據(jù)分配了多個(gè)映射的虛擬內(nèi)存空間
  • 為每個(gè)類加載器分配一個(gè)內(nèi)存塊列表
  • 塊的大小取決于類加載器的類型
  • Java反射的字節(jié)碼存取器(sun.reflect.DelegatingClassLoader )占用內(nèi)存更小
  • 空閑塊內(nèi)存返還給塊內(nèi)存列表
  • 當(dāng)元空間為空,虛擬內(nèi)存空間會(huì)被回收
  • 減少了內(nèi)存碎片

最后,從線程共享的角度來看

  • 堆和元空間是所有線程共享的
  • 虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器是線程內(nèi)部私有的

從這個(gè)角度看一下Java內(nèi)存結(jié)構(gòu)

Java 的線程與內(nèi)存

8 從GC角度看Java堆

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

Java8中堆內(nèi)存分配如下圖:

9 JVM關(guān)閉

  • 正常關(guān)閉:當(dāng)最后一個(gè)非守護(hù)線程結(jié)束或調(diào)用了System.exit或通過其他特定于平臺(tái)的方式,比如ctrl+c。
  • 強(qiáng)制關(guān)閉:調(diào)用Runtime.halt方法,或在操作系統(tǒng)中直接kill(發(fā)送single信號(hào))掉JVM進(jìn)程。
  • 異常關(guān)閉:運(yùn)行中遇到RuntimeException 異常等

在某些情況下,我們需要在JVM關(guān)閉時(shí)做一些掃尾的工作,比如刪除臨時(shí)文件、停止日志服務(wù)。為此JVM提供了關(guān)閉鉤子(shutdown hocks)來做這些事件。

Runtime類封裝java應(yīng)用運(yùn)行時(shí)的環(huán)境,每個(gè)java應(yīng)用程序都有一個(gè)Runtime類實(shí)例,使用程序能與其運(yùn)行環(huán)境相連。

關(guān)閉鉤子本質(zhì)上是一個(gè)線程(也稱為hock線程),可以通過Runtime的addshutdownhock (Thread hock)向主jvm注冊(cè)一個(gè)關(guān)閉鉤子。hock線程在jvm正常關(guān)閉時(shí)執(zhí)行,強(qiáng)制關(guān)閉不執(zhí)行。

對(duì)于在Jvm中注冊(cè)的多個(gè)關(guān)閉鉤子,他們會(huì)并發(fā)執(zhí)行,jvm并不能保證他們的執(zhí)行順序。

感謝您耐心看完了文章,小編在此為你準(zhǔn)備了福利。

關(guān)注作者,私信關(guān)鍵詞:( 資料 )

可獲取一份Java進(jìn)階學(xué)習(xí)資料包,主要有:(Java工程化、分布式架構(gòu)、高并發(fā)、高性能、深入淺出、微服務(wù)架構(gòu)、Spring、MyBatis、Netty、源碼分析、JVM原理解析等...這些成為架構(gòu)師必備的內(nèi)容)以及Java進(jìn)階學(xué)習(xí)路線圖。

歡迎轉(zhuǎn)發(fā)!

總結(jié)

以上是生活随笔為你收集整理的java 取栈顶元素_《Java实战之内存模型》详解篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。