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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JVM运行时数据区和各个区域的作用

發(fā)布時(shí)間:2024/2/28 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM运行时数据区和各个区域的作用 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一、JVM主要分為5個(gè)核心區(qū)域(6個(gè)子區(qū)域),分別是:

  • 程序計(jì)數(shù)器
  • Java虛擬機(jī)棧
  • 本地方法棧
  • Java堆
  • 方法區(qū)
  • *運(yùn)行時(shí)常量池(屬于“方法區(qū)”的一部分)
  • 二、各個(gè)區(qū)域作用和描述

    序號(hào)區(qū)域名稱共享作用異常備注
    1程序計(jì)數(shù)器線程私有記錄當(dāng)前線程鎖執(zhí)行的字節(jié)碼行號(hào)指示器。Java虛擬機(jī)規(guī)范中唯一一個(gè)沒有規(guī)定OutOfMemoryError(內(nèi)存不足錯(cuò)誤)的區(qū)域。--
    2Java虛擬機(jī)棧線程私有存放局部變量表、操作數(shù)據(jù)棧、動(dòng)態(tài)鏈接、方法出口等信息。棧深大于允許的最大深度,拋出StackOverflowError(棧溢出錯(cuò)誤)。
    內(nèi)存不足時(shí),拋出OutOfMemoryError(內(nèi)存不足錯(cuò)誤)。
    常說的“棧”說的就是Java虛擬機(jī)棧,或者是Java虛擬機(jī)棧中的局部變量表。
    3本地方法棧線程私有和Java虛擬機(jī)棧類似,不過是為JVM用到的Native方法服務(wù)。同上--
    4Java堆線程共享存放實(shí)例化數(shù)據(jù)。內(nèi)存不足時(shí),拋出OutOfMemoryError(內(nèi)存不足錯(cuò)誤)。通過-Xmx和-Xms控制大小。
    GC的主要管理對(duì)象。
    5方法區(qū)線程共享存放類信息(版本、字段、方法、接口等)、常量、靜態(tài)變量、即時(shí)編譯后的代碼等數(shù)據(jù)。內(nèi)存不足時(shí),拋出OutOfMemoryError(內(nèi)存不足錯(cuò)誤)。--
    6運(yùn)行時(shí)常量池線程共享存放編譯期生成的各種字面量和符號(hào)引用。內(nèi)存不足時(shí),拋出OutOfMemoryError(內(nèi)存不足錯(cuò)誤)。屬于“方法區(qū)”的一部分。
    7直接內(nèi)存--如NIO可以使用Native函數(shù)庫直接分配堆外內(nèi)存,該內(nèi)存受計(jì)算機(jī)內(nèi)存限制。內(nèi)存不足時(shí),拋出OutOfMemoryError(內(nèi)存不足錯(cuò)誤)。不是JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是JVM虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但這部分內(nèi)存也被頻繁的使用。所以放到一起。

    *參考《深入理解Java虛擬機(jī) JVM高級(jí)特性與最佳實(shí)踐》一書

    JVM在執(zhí)行JAVA程序時(shí)會(huì)把它管理的內(nèi)存區(qū)域劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,統(tǒng)稱為運(yùn)行時(shí)數(shù)據(jù)區(qū),由圖可見JVM程序所占的內(nèi)可劃分成5個(gè)部分:程序計(jì)數(shù)器、虛擬機(jī)棧(線程棧)、本地方法棧、堆(heap)和方法區(qū)(內(nèi)含常量池),其中方法區(qū)和堆被所有線程共享。下面分別介紹各部分的功能:

    程序計(jì)數(shù)器

    JVM是多線程的,每一個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器(JVM是多線程的,為了線程切換后能恢復(fù)到正確的執(zhí)行位置),是一塊較小的內(nèi)存空間,它與線程共存亡。JVM中的程序計(jì)數(shù)器指向的是正在執(zhí)行的字節(jié)碼地址,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。

    如果線程正在執(zhí)行的是一個(gè)java方法,程序計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址;
    若線程執(zhí)行的是Native方法,程序計(jì)數(shù)器則為Undefined。
    程序計(jì)數(shù)器是JVM中唯一一個(gè)沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

    Java虛擬機(jī)棧

    一個(gè)線程一個(gè)棧,并且生命周期與線程相同。它的內(nèi)部由一個(gè)個(gè)棧幀構(gòu)成,一個(gè)棧幀代表一個(gè)調(diào)用的方法,線程在每次方法調(diào)用執(zhí)行時(shí)創(chuàng)建一個(gè)棧幀然后壓棧,棧幀用于存放局部變量、操作數(shù)、動(dòng)態(tài)鏈接、方法出口等信息。方法執(zhí)行完成后對(duì)應(yīng)的棧幀出棧。我們平時(shí)說的棧內(nèi)存就是指這個(gè)棧。

    一個(gè)線程中的方法可能還會(huì)調(diào)用其他方法,這樣就會(huì)構(gòu)成方法調(diào)用鏈,而且這個(gè)鏈可能會(huì)很長,而且每個(gè)線程都有方法處于執(zhí)行狀態(tài)。對(duì)于執(zhí)行引擎來說,只有活動(dòng)線程棧頂?shù)臈攀怯行У?#xff0c;稱為當(dāng)前棧幀(Current Stack Frame),這個(gè)棧幀關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)。

    棧幀的大致結(jié)構(gòu)如下圖所示:


    每一個(gè)棧幀的結(jié)構(gòu)都包括了局部變量表、操作數(shù)棧、方法返回地址和一些額外的附加信息。某個(gè)方法的棧幀需要多大的局部變量表、多深的操作數(shù)棧都在編譯程序時(shí)完全確定了,并且寫入到類方法表的相應(yīng)屬性中了,因此某個(gè)方法的棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)變化的影響,而僅僅取決于具體虛擬機(jī)的實(shí)現(xiàn)。

    局部變量區(qū)域:存儲(chǔ)方法的局部變量和參數(shù),存儲(chǔ)單位以slot(4 byte)為最小單位。局部變量存放的數(shù)據(jù)類型有:基本數(shù)據(jù)類型、對(duì)象引用和return address(指向一條字節(jié)碼指令的地址)。其中64位長度的long和double類型的變量會(huì)占用2個(gè)slot,其它數(shù)據(jù)類型只占用1個(gè)slot。

    類的靜態(tài)方法和對(duì)象的實(shí)例方法被調(diào)用時(shí),各自棧幀對(duì)應(yīng)的局部變量結(jié)構(gòu)基本類似。但有以下如圖示區(qū)別:實(shí)例方法中第一個(gè)位置存放的是它所屬對(duì)象的引用,而靜態(tài)方法則沒有對(duì)象的引用。另外靜態(tài)方法里所操作的靜態(tài)變量存放在方法區(qū)。

    void test(Object object){int i=0;Boolean b=false;} static void test1(int i ,Object object,boolean b){...}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14


    關(guān)于局部變量表,還有一點(diǎn)需要強(qiáng)調(diào),就是局部變量不像類的實(shí)例變量那樣會(huì)有默認(rèn)初始化值。所以局部變量需要手工初始化,如果一個(gè)局部變量定義了但沒有賦初始值是不能使用的。

    操作數(shù)棧:?所謂操作數(shù)是指那些被指令操作的數(shù)據(jù)。當(dāng)需要對(duì)參數(shù)操作時(shí)如c=a+b,就將即將被操作的參數(shù)數(shù)據(jù)壓棧,如將a 和b 壓棧,然后由操作指令將它們彈出,并執(zhí)行操作。虛擬機(jī)將操作數(shù)棧作為工作區(qū)。Java虛擬機(jī)沒有寄存器,所有參數(shù)傳遞、值返回都是使用操作數(shù)棧來完成的。

    Java虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“棧”就是操作數(shù)棧。

    例如:

    public static int add(int a,int b){int c=0;c=a+b;return c;}add(25,23);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    主要操作步驟:

    壓棧步驟:

    0: ....1: iload_0 // 把局部變量0壓棧,int a;2: iload_1 // 局部變量1壓棧,int b;3: iadd //彈出2個(gè)變量,求和,結(jié)果壓棧484: istore_2 //彈出結(jié)果,放于局部變量2;int c;5: ...
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    動(dòng)態(tài)連接:它是個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,這個(gè)引用是為了支持方法調(diào)用過程中能進(jìn)行動(dòng)態(tài)連接。我們知道Class文件的常量池存有方法的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以指向常量池中方法的符號(hào)引用為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。余下部分將在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,這部分稱為動(dòng)態(tài)連接。
    方法返回地址:
    正常退出,執(zhí)行引擎遇到方法返回的字節(jié)碼,將返回值傳遞給調(diào)用者;

    異常退出,遇到Exception,并且方法未捕捉異常,返回地址由異常處理器來確定,并且不會(huì)有任何返回值。

    方法退出的過程實(shí)際上等同于把當(dāng)前棧幀出棧,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整PC計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。

    額外附加信息:虛擬機(jī)規(guī)范沒有明確規(guī)定,由具體虛擬機(jī)實(shí)現(xiàn)。

    Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域有兩種異常:
    StackOverFlowError:當(dāng)線程請(qǐng)求棧深度超出虛擬機(jī)棧所允許的深度時(shí)拋出
    OutOfMemoryError:當(dāng)Java虛擬機(jī)動(dòng)態(tài)擴(kuò)展到無法申請(qǐng)足夠內(nèi)存時(shí)拋出

    另外需要提醒一下,在規(guī)范模型中,棧幀相互之間是完全獨(dú)立的。但在大多數(shù)虛擬機(jī)的實(shí)現(xiàn)里都會(huì)做一些優(yōu)化處理,這樣兩個(gè)棧幀可能會(huì)出現(xiàn)一部分重疊。這樣在下面的棧幀會(huì)有部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起,這樣在進(jìn)行方法調(diào)用時(shí)就可以有部分?jǐn)?shù)據(jù)共享,而無須進(jìn)行額外的參數(shù)復(fù)制傳遞了。具體情形如下圖所示:

    本地方法棧

    Java可以通過java本地接口JNI(Java Native Interface)來調(diào)用其它語言編寫(如C)的程序,在Java里面用native修飾符來描述一個(gè)方法是本地方法。本地方法棧就是虛擬機(jī)線程調(diào)用Native方法執(zhí)行時(shí)的棧,它與虛擬機(jī)棧發(fā)揮類似的作用。但是要注意,虛擬機(jī)規(guī)范中沒有對(duì)本地方法棧作強(qiáng)制規(guī)定,虛擬機(jī)可以自由實(shí)現(xiàn),所以可以不是字節(jié)碼。如果是以字節(jié)碼實(shí)現(xiàn)的話,虛擬機(jī)棧本地方法棧就可以合二為一,事實(shí)上,OpenJDK和SunJDK所自帶的HotSpot虛擬機(jī)就是直接將虛擬機(jī)棧和本地方法棧合二為一的。

    Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域也可拋出StackOverFlowError和OutOfMemoryError。

    這個(gè)區(qū)域用來放置所有對(duì)象實(shí)例以及數(shù)組,不過在JIT(Just-in-time)情況下有些時(shí)候也有可能在棧上分配對(duì)象實(shí)例。堆也是java垃圾收集器管理的主要區(qū)域(所以很多時(shí)候會(huì)稱它為GC堆),被所有線程共享。

    從GC回收的角度看,由于現(xiàn)在GC基本都是采用的分代收集算法,所以堆內(nèi)存結(jié)構(gòu)還可以分塊成:新生代和老年代;再細(xì)一點(diǎn)的有Eden空間、From Survivor空間、To Survivor空間等。如下圖:

    對(duì)象在堆內(nèi)分配內(nèi)存的兩種方法:
    為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。

    指針碰撞(Serial、ParNew等帶Compact過程的收集器)
    假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。
    空閑列表(CMS這種基于Mark-Sweep算法的收集器)
    如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。
    選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時(shí),通常采用空閑列表。

    方法區(qū)

    它是虛擬機(jī)在加載類文件時(shí),用于存放已加載的類的類信息,常量,靜態(tài)變量,及jit編譯后的代碼(類方法)等數(shù)據(jù)的內(nèi)存區(qū)域,是線程共享的。

    方法區(qū)存放的信息包括:

    1.類的基本信息:

    每個(gè)類的全限定名

    每個(gè)類的直接超類的全限定名(可約束類型轉(zhuǎn)換)

    該類是類還是接口

    該類型的訪問修飾符

    直接超接口的全限定名的有序列表

    2.已裝載類的詳細(xì)信息:

    3.運(yùn)行時(shí)常量池:

    類信息除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量、符號(hào)引用,文字字符串、final變量值、類名和方法名常量,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。它們以數(shù)組形式訪問,是調(diào)用方法、與類聯(lián)系及類的對(duì)象化的橋梁。

    這里再講一下,JDK1.7之前運(yùn)行時(shí)常量池是方法區(qū)的一部分,JDK1.7及之后版本已經(jīng)將運(yùn)行時(shí)常量池從方法區(qū)中移了出來,在堆(Heap)中開辟了一塊區(qū)域存放運(yùn)行時(shí)常量池。

    運(yùn)行時(shí)常量池除了存放編譯期產(chǎn)生的Class文件的常量外,還可存放在程序運(yùn)行期間生成的新常量,比較常見增加新常量方法有String類的intern()方法。String.intern()是一個(gè)Native方法,它的作用是:如果運(yùn)行時(shí)常量池中已經(jīng)包含一個(gè)等于此String對(duì)象內(nèi)容的字符串,則返回常量池中該字符串的引用;如果沒有,則在常量池中創(chuàng)建與此String內(nèi)容相同的字符串,并返回常量池中創(chuàng)建的字符串的引用。不過JDK7的intern()方法的實(shí)現(xiàn)有所不同,當(dāng)常量池中沒有該字符串時(shí),不再是在常量池中創(chuàng)建與此String內(nèi)容相同的字符串,而改為在常量池中記錄堆中首次出現(xiàn)的該字符串的引用,并返回該引用。

    4.字段信息:

    字段信息存放類中聲明的每一個(gè)字段(實(shí)例變量)的信息,包括字段的名、類型、修飾符。

    如private String a=“”;則a為字段名,String為描述符,private為修飾符。

    5.方法信息:

    類中聲明的每一個(gè)方法的信息,包括方法名、返回值類型、參數(shù)類型、修飾符、異常、方法的字節(jié)碼。(在編譯的時(shí)候,就已經(jīng)將方法的局部變量表、操作數(shù)棧大小等完全確定并存放在字節(jié)碼中,在加載載的時(shí)候,隨著類一起裝入方法區(qū)。)

    在運(yùn)行時(shí),虛擬機(jī)線程調(diào)用方法時(shí)從常量池中獲得符號(hào)引用,然后在運(yùn)行時(shí)解析成方法的實(shí)際地址,最后通過常量池中的全限定名、方法和字段描述符,把當(dāng)前類或接口中的代碼與其它類或接口中的代碼聯(lián)系起來。

    5.靜態(tài)變量:

    就是類變量,被類的所有實(shí)例對(duì)象共享,我們只需知道,在方法區(qū)有個(gè)靜態(tài)區(qū),靜態(tài)區(qū)專門存放靜態(tài)變量和靜態(tài)塊。

    6.到類ClassLoader的引用:到該類的類裝載器的引用。

    7.到類Class的引用:虛擬機(jī)為每一個(gè)被裝載的類型創(chuàng)建一個(gè)Class實(shí)例,用來代表這個(gè)被裝載的類。

    Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域可拋出OutOfMemoryError。

    直接內(nèi)存

    直接內(nèi)存(Direct Memory)雖然不是程序運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但這部分內(nèi)存也被頻繁使用,而且它也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。

    在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native方法庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆里面的DirecByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在某些應(yīng)用場景中顯著提高性能,因?yàn)樗苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。

    顯然,本機(jī)直接內(nèi)存的分配不會(huì)受到Java堆大小的限制,但是,還是會(huì)受到本機(jī)總內(nèi)存(包括RAM及SWAP區(qū)或者分頁文件)的大小及處理器尋址空間的限制,從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)OutOfMemoryError異常。

    總結(jié)

    在程序運(yùn)行時(shí)類是在方法區(qū),實(shí)例對(duì)象本身在堆里面。

    方法字節(jié)碼在方法區(qū)。線程調(diào)用方法執(zhí)行時(shí)創(chuàng)建棧幀并壓棧,方法的參數(shù)和局部變量在棧幀的局部變量表。

    對(duì)象的實(shí)例變量和對(duì)象一起在堆里,所以各個(gè)線程都可以共享訪問對(duì)象的實(shí)例變量。

    靜態(tài)變量在方法區(qū),所有對(duì)象共享。字符串常量等常量在運(yùn)行時(shí)常量池。

    各線程調(diào)用的方法,通過堆內(nèi)的對(duì)象,方法區(qū)的靜態(tài)數(shù)據(jù),可以共享交互信息。

    各線程調(diào)用的方法所有參數(shù)傳遞、方法返回值的返回,都是使用棧幀里的操作數(shù)棧來完成的。

    ?

    -------------------------------------------------------------------------------------------------------------------------------

    3.1.1 程序計(jì)數(shù)器

    Program Counter Register

    內(nèi)存空間小,線程私有.它可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器.也就是說,線程主要是執(zhí)行任務(wù),而執(zhí)行到哪里,需要使用程序計(jì)數(shù)器來記錄.字節(jié)碼解釋器工作是就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行指令的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴計(jì)數(shù)器完成.

    由于java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,所以,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),所以我們說,它是線程私有的.

    如果線程正在執(zhí)行一個(gè) Java 方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器的值則為 (Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。

    3.1.2 虛擬機(jī)棧

    java virtual Machine Stacks

    線程私有,生命周期和線程一致。描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行結(jié)束,就對(duì)應(yīng)著一個(gè)棧幀從虛擬機(jī)棧中入棧到出棧的過程。

    在編譯程序代碼的時(shí)候,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受程序運(yùn)行時(shí)期變量數(shù)據(jù)的影響.

    一個(gè)線程中的方法調(diào)用鏈可能會(huì)很長,很多方法都處于同時(shí)執(zhí)行狀態(tài).對(duì)于執(zhí)行引擎來說,在活動(dòng)線程中,只有位于棧頂?shù)臈攀怯行У?/strong>,執(zhí)行引擎運(yùn)行的所有的字節(jié)碼指令都只針對(duì)當(dāng)前棧幀來進(jìn)行操作的.

    局部變量表

    局部變量表是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量.

    操作數(shù)棧

    Operand Stack

    是一個(gè)后進(jìn)先出棧.其最大深度在編譯的時(shí)候已經(jīng)確定了.當(dāng)一個(gè)方法剛剛開始執(zhí)行的時(shí)候,這個(gè)方法的操作數(shù)占是空的,在方法的執(zhí)行過程中,會(huì)有各種字節(jié)碼指令往操作數(shù)占中寫入和提取內(nèi)容,這就是出棧/入棧動(dòng)作.

    另外,在概念模型中,兩個(gè)棧幀作為虛擬機(jī)棧的元素,是完全相互獨(dú)立的,但大多數(shù)虛擬機(jī)的實(shí)現(xiàn)都會(huì)做一些優(yōu)化處理,讓兩個(gè)棧幀出現(xiàn)部分重疊,讓下面的棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起,這樣在進(jìn)行方法調(diào)用的時(shí)候就可以共用一部分?jǐn)?shù)據(jù).無須進(jìn)行額外的參數(shù)的復(fù)制傳遞.

    image

    動(dòng)態(tài)連接

    每一個(gè)棧幀都包含一個(gè)執(zhí)行運(yùn)行時(shí)常量池中該棧幀所屬方法的引用.持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接.

    這個(gè)引用是一個(gè)符號(hào)引用,不是方法實(shí)際運(yùn)行的入口地址,需要?jiǎng)討B(tài)的找到具體的方法入口.

    這個(gè)特性給java帶來了更強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力,但也使得java方法調(diào)用過程變得相對(duì)復(fù)雜起來,需要在類加載期間,甚至到運(yùn)行期間才能夠確定目標(biāo)方法的直接引用.

    方法返回地址

    正常完成出口:方法正確執(zhí)行,執(zhí)行引擎遇到方法返回的指令,回到上層的方法調(diào)用者.

    異常完成出口:方法執(zhí)行過程中發(fā)生異常,并且沒有處理異常,這樣是不會(huì)給上層調(diào)用者產(chǎn)生任何返回值.

    方法正常退出,將會(huì)返回程序結(jié)束其的值給上層方法,經(jīng)過調(diào)整之后以指向方法調(diào)用指令后面的一條指令,繼續(xù)執(zhí)行上層方法.

    3.1.3 本地方法棧

    Native Method Stack

    區(qū)別于Java 虛擬機(jī)棧的是,Java 虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native 方法服務(wù)。也會(huì)有 StackOverflowError 和 OutOfMemoryError 異常。

    3.1.4 堆

    heap

    對(duì)于絕大多數(shù)應(yīng)用來說,這塊區(qū)域是 JVM 所管理的內(nèi)存中最大的一塊。線程共享,主要是存放對(duì)象實(shí)例和數(shù)組。內(nèi)部可以設(shè)置劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)。可以位于物理上不連續(xù)的空間,但是邏輯上要連續(xù)。

    從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本上都采用分代收集算法,所以對(duì)空間還可以細(xì)分為:新生代(年輕代),老年代(年老代).再細(xì)致一點(diǎn),可以分為Eden空間,From Survivor空間, To Survivor空間.

    不論如何劃分,都與存放內(nèi)容無關(guān),都是存放的是對(duì)象實(shí)例,進(jìn)一步劃分的目的是為了更好的回收內(nèi)存,或者更快的分配內(nèi)存.

    3.1.5 方法區(qū)

    Method Area

    屬于共享內(nèi)存區(qū)域,存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。

    對(duì)于習(xí)慣在HotSpot虛擬機(jī)上開發(fā)和部署程序的開發(fā)者來說,很多人愿意把方法區(qū)稱為“永久代”(Permanent Generation)(java8之前,使用永久代來實(shí)現(xiàn)方法區(qū),在java8之后,廢除永久代,將字符串常量池移動(dòng)到堆中,并新增Meta space,直接在系統(tǒng)內(nèi)存中.),本質(zhì)上兩者并不等價(jià),僅僅是因?yàn)镠otSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把GC分代收集擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已。

    Java虛擬機(jī)規(guī)范對(duì)這個(gè)區(qū)域的限制非常寬松,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不實(shí)現(xiàn)垃圾收集。相對(duì)而言,垃圾收集行為在這個(gè)區(qū)域是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣“永久”存在了。這個(gè)區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載,一般來說這個(gè)區(qū)域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當(dāng)苛刻,但是這部分區(qū)域的回收確實(shí)是有必要的。

    3.1.6 運(yùn)行時(shí)常量池

    Runtime Constant Pool

    屬于方法區(qū)一部分,用于存放編譯期生成的各種字面量和符號(hào)引用。內(nèi)存有限,無法申請(qǐng)時(shí)拋出 OutOfMemoryError.

    3.1.7 直接內(nèi)存

    Direct Memory

    非虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的部分.

    在 JDK 1.4 中新加入 NIO (New Input/Output) 類,引入了一種基于通道(Channel)和緩存(Buffer)的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。可以避免在 Java 堆和 Native 堆中來回的數(shù)據(jù)耗時(shí)操作。

    本機(jī)直接內(nèi)存的分配不會(huì)受到j(luò)ava堆大小的限制,但是,既然是內(nèi)存,肯定還是會(huì)受到本機(jī)總內(nèi)存大小的限制.所以我們?cè)谂渲锰摂M機(jī)參數(shù)時(shí),不要忽略直接內(nèi)存,否則可能因?yàn)閯?dòng)態(tài)擴(kuò)展導(dǎo)致出現(xiàn)OutOfMemoryError.

    3.2 基于棧的執(zhí)行過程分析

    下面我們通過一個(gè)小程序,來分析一下,虛擬機(jī)中實(shí)際是如何執(zhí)行代碼的.

    public int calc(){int a = 100;int b = 200;int c = 300;return (a + b) * c;}

    代碼非常簡單,我們可以直接使用javap命令(javap -c CalcTest.class > calc.txt),通過反匯編操作,來查看對(duì)應(yīng)的字節(jié)碼指令.

    image

    查閱虛擬機(jī)字節(jié)碼指令表,我們先將上面的反匯編代碼翻譯一下:

    image

    我們從這段程序的執(zhí)行中,也可以回過來再次認(rèn)識(shí)棧結(jié)構(gòu).整個(gè)過程的中間變量都是以操作數(shù)棧的出棧,入棧為信息交換途徑.

    3.3 HotSpot虛擬機(jī)對(duì)象探秘

    堆,是我們最實(shí)用的一塊內(nèi)存空間,分析完棧幀的執(zhí)行過程之后,現(xiàn)在我們?cè)賮矸治鲆幌?虛擬機(jī)在java堆中對(duì)象的創(chuàng)建,布局和訪問的過程.

    3.3.1 對(duì)象的創(chuàng)建

    Java是一門面向?qū)ο蟮恼Z言,在運(yùn)行過程中無時(shí)無刻都有對(duì)象的創(chuàng)建,在語言層面,僅僅是一個(gè)關(guān)鍵字new,那么在虛擬機(jī)中,對(duì)象是如何創(chuàng)建出來的呢?

    檢查類是否已經(jīng)被加載:虛擬機(jī)遇到 new 指令時(shí),首先檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)被加載、解析和初始化過。如果沒有,執(zhí)行相應(yīng)的類加載。

    為新生對(duì)象分配內(nèi)存:類加載檢查通過之后,為新對(duì)象分配內(nèi)存(內(nèi)存大小在類加載完成后便可確認(rèn))。

    如果堆內(nèi)存絕對(duì)規(guī)整,使用指針碰撞.否則使用空閑列表,找到一塊足夠大的內(nèi)存劃分給對(duì)象實(shí)例.

    image

    堆內(nèi)存是否規(guī)整,主要是看GC回收了內(nèi)存之后是否包含壓縮或者整理功能.如果有,那么內(nèi)存就比較規(guī)整.否則如果沒有,創(chuàng)建對(duì)象就需要采用空閑列表的方式.

    比如:

    serial,ParNew等帶有整理的收集器,可以使用指針碰撞.

    CMS使用簡單清除的算法,可以使用空閑列表.

    如果線程支持在堆中都有私有的分配緩沖區(qū)(TLAB),這樣可以很大程度避免在并發(fā)情況下頻繁創(chuàng)建對(duì)象造成的線程不安全。

    內(nèi)存空間分配完成后會(huì)將整個(gè)空間都初始化為零值(不包括對(duì)象頭).

    接下來就是填充對(duì)象頭,把對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的 GC 分代年齡等信息存入對(duì)象頭。

    執(zhí)行 new 指令后,執(zhí)行 init 方法(由字節(jié)碼指令invokespecial決定,執(zhí)行初始化方法)后,才算一份真正可用的對(duì)象創(chuàng)建完成.

    3.3.2 對(duì)象的內(nèi)存布局

    在上文中,我們講到一個(gè)步驟是,填充對(duì)象頭.那什么是對(duì)象頭呢?

    在 HotSpot 虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding).

    對(duì)象頭(Header)

    包含兩部分,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、GC 分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等,根據(jù)不同的系統(tǒng)為數(shù)固定大小.官方稱為 ‘Mark Word’。第二部分是類型指針,即對(duì)象指向它的類的元數(shù)據(jù)指針,虛擬機(jī)通過這個(gè)指針確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。另外,如果是 Java 數(shù)組,對(duì)象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因?yàn)槠胀▽?duì)象可以通過 Java 對(duì)象元數(shù)據(jù)確定大小,而數(shù)組對(duì)象不可以。

    實(shí)例數(shù)據(jù)(Instance Data):程序代碼中所定義的各種類型的字段內(nèi)容(包含父類繼承下來的和子類中定義的)。

    對(duì)齊填充(Padding):不是必然需要,主要是占位,保證對(duì)象大小是某個(gè)字節(jié)的整數(shù)倍。

    3.3.3 對(duì)象的訪問定位

    使用對(duì)象時(shí),通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對(duì)象

    由于java虛擬機(jī)只規(guī)定要一個(gè)執(zhí)行對(duì)象的引用,而沒有規(guī)定以何種方式去定位.所以對(duì)象訪問方式取決于虛擬機(jī)的實(shí)現(xiàn).主流的方式有兩種:

    1.通過句柄訪問.Java 堆中會(huì)分配一塊內(nèi)存作為句柄池。reference 存儲(chǔ)的是句柄地址.

    2.使用指針訪問.reference 中直接存儲(chǔ)對(duì)象地址.

    比較:使用句柄的最大好處是 reference 中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象移動(dòng)(GC)是只改變實(shí)例數(shù)據(jù)指針地址,reference 自身不需要修改。直接指針訪問的最大好處是速度快,節(jié)省了一次指針定位的時(shí)間開銷。如果是對(duì)象頻繁 GC 那么句柄方法好,如果是對(duì)象頻繁訪問則直接指針訪問好.

    HotSpot使用第二種方式進(jìn)行對(duì)象訪問的.




    ?

    ?

    ?

    總結(jié)

    以上是生活随笔為你收集整理的JVM运行时数据区和各个区域的作用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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