JVM — JVM運行時數據區域(一)

1. 運行時數據區域

JVM定義了若干個程序執行期間使用的數據區域。這個區域裏的一些數據在JVM啓動的時候創建,在JVM退出的時候銷燬。而其他的數據依賴於每一個線程,在線程創建時創建,在線程退出時銷燬。如下圖所示:
在這裏插入圖片描述

1.1 程序計數器

1.1.1 程序計數器

       程序計數器是一個記錄着當前線程所執行的字節碼的行號指示器。JAVA代碼編譯後的字節碼在未經過JIT(實時編譯器)編譯前,其執行方式是通過“字節碼解釋器”進行解釋執行。簡單的工作原理爲解釋器讀取 裝載入內存的 字節碼,按照順序讀取字節碼指令。讀取一個指令後,將該指令“翻譯”成固定的操作,並根據這些操作進行分支、循環、跳轉等流程。
  從上面的描述中,可能會產生程序計數器是否是多餘的疑問。因爲沿着指令的順序執行下去,即使是分支跳轉這樣的流程,跳轉到指定的指令處按順序繼續執行是完全能夠保證程序的執行順序的。假設程序永遠只有一個線程,這個疑問沒有任何問題,也就是說並不需要程序計數器。但實際上程序是通過多個線程協同合作執行的。
  首先我們要搞清楚JVM的多線程實現方式。JVM的多線程是通過CPU時間片輪轉(即線程輪流切換並分配處理器執行時間)算法來實現的。也就是說,某個線程在執行過程中可能會因爲時間片耗盡而被掛起,而另一個線程獲取到時間片開始執行。當被掛起的線程重新獲取到時間片的時候,它要想從被掛起的地方繼續執行,就必須知道它上次執行到哪個位置,在JVM中,通過程序計數器來記錄某個線程的字節碼執行位置。因此,程序計數器是具備線程隔離的特性,也就是說,每個線程工作時都有屬於自己的獨立計數器。

1.1.2 程序計數器的特點

1.線程隔離性,每個線程工作時都有屬於自己的獨立計數器。
  2.執行java方法時,程序計數器是有值的,且記錄的是正在執行的字節碼指令的地址。
  3.執行native本地方法時,程序計數器的值爲空(Undefined)。因爲native方法是java通過JNI直接調用本地C/C++庫,可以近似的認爲native方法相當於C/C++暴露給java的一個接口,java通過調用這個接口從而調用到C/C++方法。由於該方法是通過C/C++而不是java進行實現。那麼自然無法產生相應的字節碼,並且C/C++執行時的內存分配是由自己語言決定的,而不是由JVM決定的。
在這裏插入圖片描述
  4.程序計數器佔用內存很小,在進行JVM內存計算時,可以忽略不計。
  5.程序計數器,是唯一一個在java虛擬機規範中沒有規定任何OutOfMemoryError的區域。

1.2 java虛擬機棧

1.2.1 什麼是虛擬機棧?

       虛擬機棧是用於描述java方法執行的內存模型。
  每個java方法在執行時,會創建一個“棧幀(stack frame)”,棧幀的結構分爲“局部變量表、操作數棧、動態鏈接、方法出口”幾個部分,這裏只需要瞭解棧幀是一個方法執行時所需要數據的結構。我們常說的“堆內存、棧內存”中的“棧內存”指的便是虛擬機棧,確切地說,指的是虛擬機棧的棧幀中的局部變量表,因爲這裏存放了一個方法的所有局部變量。
  方法調用時,創建棧幀,並壓入虛擬機棧;方法執行完畢,棧幀出棧並被銷燬,如下圖所示:
  在這裏插入圖片描述

  • 棧幀(Stack Frame)
      棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構。它是虛擬機運行時數據區中的java虛擬機棧的棧元素。
      棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息。
      每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機裏面從入棧到出棧的過程。在編譯程序代碼的時候,棧幀中需要多大的局部變量表內存,多深的操作數棧都已經完全確定了。
           因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。
      棧結構圖如下:
      在這裏插入圖片描述
  • 局部變量表
  1. 局部變量表(Local Variable Table)是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。並且在Java編譯爲Class文件時,就已經確定了該方法所需要分配的局部變量表的最大容量。
  2. 局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)「String是引用類型」,對象引用(reference類型) 和 returnAddress類型(它指向了一條字節碼指令的地址)
  • 變量槽(Variable Slot)
  1. 局部變量表的容量以變量槽爲最小單位,每個變量槽都可以存儲32位長度的內存空間,例如boolean、byte、char、short、int、float、reference。
  2. 對於64位長度的數據類型(long,double),虛擬機會以高位對齊方式爲其分配兩個連續的Slot空間,也就是相當於把一次long和double數據類型讀寫分割成爲兩次32位讀寫。
  • reference(對象實例的引用)
    一般來說,虛擬機都能從引用中直接或者間接的查找到對象的以下兩點 :
    a.在Java堆中的數據存放的起始地址索引。
    b.所屬數據類型在方法區中的存儲類型。

例如:我們在創建一個Student對象時的數據存儲結構:
在這裏插入圖片描述

  • 動態連接
    每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接(Dynamic Linking)。
    在類加載階段中的解析階段會將符號引用轉爲直接引用,這種轉化也稱爲靜態解析。
    另外的一部分將在每一次運行時期轉化爲直接引用。這部分稱爲動態連接。

  • 方法出口
    當一個方法開始執行後,只有2種方式可以退出這個方法 :
    方法返回指令 : 執行引擎遇到一個方法返回的字節碼指令,這時候有可能會有返回值傳遞給上層的方法調用者,這種退出方式稱爲正常完成出口。
    異常退出 : 在方法執行過程中遇到了異常,並且沒有處理這個異常,就會導致方法退出。
    無論採用任何退出方式,在方法退出之後,都需要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能需要在棧幀中保存一些信息。
    一般來說,方法正常退出時,調用者的PC計數器的值可以作爲返回地址,棧幀中會保存這個計數器值。
    而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。

1.2.2 虛擬機棧的特點

1.虛擬機棧是線程隔離的,即每個線程都有自己獨立的虛擬機棧,即線程私有的,它的生命週期與線程相同(隨線程而生,隨線程而滅)
2. 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;
如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常;(當前大部分JVM都可以動態擴展,只不過JVM規範也允許固定長度的虛擬機棧)
3. Java虛擬機棧描述的是Java方法執行的內存模型:每個方法執行的同時會創建一個棧幀。對於我們來說,主要關注的stack棧內存,就是虛擬機棧中局部變量表部分。

1.2.3 虛擬機棧的StackOverflowError

       若單個線程請求的棧深度大於虛擬機允許的深度,則會拋出StackOverflowError(棧溢出錯誤)。
  JVM會爲每個線程的虛擬機棧分配一定的內存大小(-Xss參數),因此虛擬機棧能夠容納的棧幀數量是有限的,若棧幀不斷進棧而不出棧,最終會導致當前線程虛擬機棧的內存空間耗盡。

1.2.4 虛擬機棧的OutOfMemoryError

       不同於StackOverflowError,OutOfMemoryError指的是當整個虛擬機棧內存耗盡,並且無法再申請到新的內存時拋出的異常。
  JVM未提供設置整個虛擬機棧佔用內存的配置參數。虛擬機棧的最大內存大致上等於“JVM進程能佔用的最大內存(依賴於具體操作系統) - 最大堆內存 - 最大方法區內存 - 程序計數器內存(可以忽略不計) - JVM進程本身消耗內存”。當虛擬機棧能夠使用的最大內存被耗盡後,便會拋出OutOfMemoryError。

1.3 本地方法棧

       本地方法棧的功能和特點類似於虛擬機棧,均具有線程隔離的特點以及都能拋出StackOverflowError和OutOfMemoryError異常。
  不同的是,本地方法棧服務的對象是JVM執行的native方法,而虛擬機棧服務的是JVM執行的java方法。

1.4 java堆

  • java堆(Java Heap)是java虛擬機管理的內存最大的一塊
  • java堆是被所有線程共享的
  • java堆的主要作用是存放對象實例以及數組
  • java堆是垃圾收集器管理的主要區域
  • 收集器基本都採用分代收集算法
  • java堆可以分爲新生代和老年代,再細緻還可以分爲Eden區,From Survivor區,To Survivor區
  • 根據java虛擬機規範,java堆可以處在物理上不連續的內存空間中,只要邏輯上是連續即可

1.5 方法區

       方法區(Method Area)同Java 堆,是各個線程共享的內存區域,用於存儲虛擬機已經加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是方法區卻有一個別名叫做Non-Heap(非堆),目的就是把方法區和Java堆區分開來。

       方法區保存在着被加載過的每一個類的信息;這些信息由類加載器在加載類的時候,從類的源文件中抽取出來;
       static變量信息也保存在方法區中;
       可以看做是將類(Class)的元數據,保存在方法區裏;
       方法區是線程共享的;當有多個線程都用到一個類的時候,而這個類還未被加載,則應該只有一個線程去加載類,讓其他線程等待;
       方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。jvm也可以允許用戶和程序指定方法區的初始大小,最小和最大限制;
       方法區同樣存在垃圾收集,因爲通過用戶定義的類加載器可以動態擴展Java程序,這樣可能會導致一些類,不再被使用,變爲垃圾。這時候需要進行垃圾清理。
       JDK 1.8 的HotSpot中方法區已經被徹底移出了,取而代之使用的是元空間(Metaspace),元空間使用的是本地內存(Native Memory)。以下是一些常用參數:

  • -XX:MetaspaceSize=N //設置Metaspace的初始和最小大小

  • -XX : MaxMetaspaceSize=N //設置Metaspace的最大大小

       與永久代不同,如果元空間不指定大小,隨着更多類的創建,虛擬機會耗盡所有可用的系統內存。

       Java虛擬機規範對方法區的限制非常寬鬆,除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集,相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名字一樣是永久存在了。這個區域內存回收目標主要是這對常量池的回收和對類型的卸載,但是,這個區域的回收成績卻難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分的回收確實是必要的。在SUN公司的BUG列表中,曾出現過若干個嚴重的bug就是由於低版本的HotSpot虛擬機對此區域未完全回收而導致內存泄露。當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
在這裏插入圖片描述
       JDK 1.8 廢棄永久代而使用元空間就是因爲,這個永久代有一個JVM本身設置的固定大小線,無法進行調整,而且設置爲多大的空間很難確定,PermSize的大小依賴於很多因素,如JVM加載的class總數,常量池的大小,方法的大小等,而元空間使用的是本地內存,受本機可用內存的限制,永遠也不會有OutOfMemoryError異常。JVM不再有PermGen但類的元數據信息還在,只不過放到了本地內存,用戶可以爲類元數據信息指定最大可利用的本地內存空間,JVM也可以增加本地內存空間來滿足類元數據信息的存儲。此外,在HotSpot中的每個垃圾收集器需要專門的代碼來處理存儲在PermGen中的類元數據信息,從PermGen分離類的元數據信息到元空間,由於元空間的分配具有和Java 堆相同的地址空間,因此元空間和Java堆可以無縫的管理,而且簡化了FullGC的過程,這樣將來可以並行的對元數據信息進行垃圾收集,而沒有GC暫停。

1.6 運行時常量池

       運行時常量池是方法區的一部分。Class文件(也就是.java文件編譯後生成的字節碼.class文件)中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用。這部分內容將在JVM完成類加載後進入方法區的運行時常量池中存放。這裏既然是方法區的一部分,自然也受到方法區內存的限制,當常量池無法再申請到內存時會拋OutOfMemoryError異常;
  常量池主要用於存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當於Java語言層面常量的概念,如文本字符串,聲明爲final的常量值等,符號引用則屬於編譯原理方面的概念,包括瞭如下三種類型的常量:

  • 類和接口的全限定名
  • 字段名稱和描述符
  • 方法名稱和描述符

一般來說除了保存class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池與Class文件常量池的區別
運行時常量池相對於Class文件常量池的另外一個重要特徵是具備動態性。運行期間也能將新的常量放入池中,比如String類的intern()方法,在編譯階段就把所有的字符串文字放到一個常量池中。

舉例:String s1=new String(“abc”)
這裏將會有兩個String被創建,一個是你的Class被ClassLoader加載時,你的"abc"被作爲常量讀入,在constant pool裏創建了一個共享的"abc" ,然後,當調用到new String(“abc”)的時候,會在heap裏創建這個new String(“abc”);

考慮類加載階段和實際執行時:

  • (1)類加載對一個類只會進行一次。"abc"在類加載時就已經創建並駐留了(如果該類被加載之前已經有"abc"字符串被駐留過則不需要重複創建用於駐留的X實例)。駐留的字符串是放在全局共享的字符串常量池中的。

  • (2)在這段代碼後續被運行的時候,"abc"字面量對應的String實例已經固定了,不會再被重複創建。所以這段代碼將常量池中的對象複製一份放到heap中,並且把heap中的這個對象的引用交給s1 持有。

1.7 直接內存

       直接內存並不是虛擬機運行時數據區的一部分,也不是Java 虛擬機規範中農定義的內存區域。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可以使用native 函數庫直接分配堆外內存,然後通脫一個存儲在Java堆中的DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆中來回複製數據。
直接內存特點:

  • 本機直接內存的分配不會受到Java 堆大小的限制,受到本機總內存大小限制
  • 配置虛擬機參數時,不要忽略直接內存 防止出現OutOfMemoryError異常

直接內存(堆外內存)與堆內存比較:

  • 直接內存申請空間耗費更高的性能,當頻繁申請到一定量時尤爲明顯。
  • 直接內存IO讀寫的性能要優於普通的堆內存,在多次讀寫操作的情況下差異明顯。

直接內存使用場景

  • 有很大的數據需要存儲,它的生命週期很長。
  • 適合頻繁的IO操作,例如網絡併發場景。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章