1. 程式人生 > >深入詳解JVM記憶體模型與JVM引數詳細配置

深入詳解JVM記憶體模型與JVM引數詳細配置

深入詳解JVM記憶體模型與JVM引數詳細配置

JVM記憶體結構

在這裡插入圖片描述
由上圖可以清楚的看到JVM的記憶體空間分為3大部分:堆記憶體

方法區棧記憶體
   其中棧記憶體可以再細分為java虛擬機器棧本地方法棧,堆記憶體可以劃分為新生代老年代,新生代中還可以再次劃分為Eden區From Survivor區To Survivor區。
其中一部分是執行緒共享的,包括 Java 堆和方法區;另一部分是執行緒私有的,包括虛擬機器棧和本地方法棧,以及程式計數器這一小部分記憶體。

堆記憶體(Heap)

java 堆(Java Heap)是Java 虛擬機器所管理的記憶體中最大的一塊。堆是被所有執行緒共享的區域,是在虛擬機器啟動時建立的。堆裡面存放的都是物件的例項(new 出來的物件都存在堆中)。
   此記憶體區域的唯一目的就是存放物件例項(new的物件),幾乎所有的物件例項都在這裡分配記憶體。
   堆記憶體分為兩個部分:年輕代和老年代。我們平常所說的垃圾回收,主要回收的就是堆區

。更細一點劃分新生代又可劃分為Eden區和2個Survivor區(From Survivor和To Survivor)。
   下圖中的Perm代表的是永久代,但是注意永久代並不屬於堆記憶體中的一部分,同時jdk1.8之後永久代已經被移除。
在這裡插入圖片描述
新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過引數 –XX:NewRatio 來指定 )
預設的,Eden : from : to = 8 : 1 : 1 ( 可以通過引數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

方法區(Method Area)

方法區也稱”永久代“,它用於儲存虛擬機器載入的類資訊、常量、靜態變數,是各個執行緒共享的記憶體區域。
   在JDK8之前的HotSpot JVM,存放這些”永久的”的區域叫做“永久代(permanent generation)”。永久代是一片連續的堆空間,在JVM啟動之前通過在命令列設定引數-XX:MaxPermSize來設定永久代最大可分配的記憶體空間,預設大小是64M(64位JVM預設是85M)。
   隨著JDK8的到來,JVM不再有永久代(PermGen)。但類的元資料資訊**(metadata)還在,只不過不再是儲存在連續的堆空間上,而是移動到叫做“Metaspace”的本地記憶體(Native memory。
   方法區或永生代相關設定

  • -XX:PermSize=64MB 最小尺寸,初始分配
  • -XX:MaxPermSize=256MB 最大允許分配尺寸,按需分配
  • XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 設定垃圾不回收
    預設大小
  • -server選項下預設MaxPermSize為64m
  • -client選項下預設MaxPermSize為32m

虛擬機器棧(JVM Stack)

  • 每個JVM執行緒有一個私有棧,棧線上程建立的同時被建立。
  • 棧由許多幀組成,也叫 “棧幀”
  • 每次方法呼叫都會建立一個棧幀

棧幀的含義?

每個方法的執行和結束對應著棧幀的入棧和出棧。入棧表示被呼叫,出棧表示執行完畢或者返回異常。
   一個虛擬機器棧對應一個執行緒,當前CPU排程的那個執行緒叫做活動執行緒;一個棧幀對應一個方法,活動執行緒的虛擬機器棧裡最頂部的棧幀代表了當前正在執行的方法,而這個棧幀也被叫做‘當前棧幀’。

棧幀的資料結構?

區域性變量表運算元棧動態連結方法返回地址附加資訊

  • 棧幀的大小是什麼時候確定的?
    編譯程式程式碼的時候,就已經確定了局部變量表和運算元棧的大小,而且在方法表的Code屬性中寫好了。不會受到執行期資料的影響。
  • 棧楨和棧楨是完全獨立的嗎?
    本來棧楨作為虛擬機器棧的一個單元,應該是棧楨之間完全獨立的。但是,虛擬機器進行了一些優化:為了避免過多的方法間引數的複製傳遞、方法返回值的複製傳遞等一些操作,就讓一部分資料進行棧楨間共享。

區域性變量表

是一片邏輯連續的記憶體空間,最小單位是Slot,用來存放方法引數和方法內部定義的區域性變數。我覺得可以想成Slot陣列。虛擬機器沒有明確指明一個Slot的記憶體空間大小。但是boolean、byte、char、short、int、float、reference、returnAddress型別的資料都可以用32位空間或更小的記憶體來存放。這些型別佔用一個Slot。Java中的long和double型別是64位,佔用兩個Slot。(只有double和long是jvms裡明確規定的64位資料型別)

運算元棧

簡單來說,運算元棧就是JVM執行引擎的一個工作區,當一個方法被呼叫的時候,一個新的棧幀也會隨之被創建出來,但這個時候棧幀中的運算元棧卻是空的,只有方法在執行的過程中,才會有各種各樣的位元組碼指令往運算元棧中執行入棧和出棧操作。示例程式碼如下所示:

public class NextIndex {
    private long index = 0;
    public long nextIndex() {
        return index++;
    }
}

位元組碼指令序列
public long nextIndex():
0: aload_0 // 將第1個區域性變數this壓入棧頂
1: dup // 複製棧頂this並壓入棧頂. 棧底到棧頂:this、this
2: getfield #12 // Field index:J. 獲取例項欄位index並壓入棧頂,消耗棧頂的1個this. 棧底到棧頂:this、index_for_ladd
5: dup2_x1 // 複製棧頂index數值,並插入第1個this下面. 棧底到棧頂:index_for_return、this、index_for_ladd
6: lconst_1 // 將long型別常量1壓入棧頂
7: ladd // 將棧頂的2個long型別數值相加,並將結果壓入棧頂. 棧底到棧頂:index_for_lreturn、this、index_for_putfield
8: putfield #12 // Field index:J. 將棧頂數值賦值給例項欄位index
11: lreturn

動態連結

一個方法呼叫另一個方法,或者一個類使用另一個類的成員變數時,總得知道被呼叫者的名字吧?(你可以不認識它本身,但呼叫它就需要知道他的名字)。符號引用就相當於名字,這些被呼叫者的名字就存放在Java位元組碼檔案裡。名字是知道了,但是Java真正執行起來的時候,真的能靠這個名字(符號引用)就能找到相應的類和方法嗎?需要解析成相應的直接引用,利用直接引用來準確地找到。
   舉個例子,就相當於我在0X0300H這個地址存入了一個數526,為了方便程式設計,我把這個給這個地址起了個別名叫A, 以後我程式設計的時候(執行之前)可以用別名A來暗示訪問這個空間的資料,但其實程式執行起來後,實質上還是去尋找0X0300H這片空間來獲取526這個資料的。這樣的符號引用和直接引用在執行時進行解析和連結的過程,叫動態連結。
   動態連結的前提每一個棧幀內部都要包含一個指向執行時常量池的引用,來支援動態連結的實現。

方法返回地址

返回一個值給呼叫它的方法,方法正常完成發生在一個方法執行過程中遇到了方法返回的位元組碼指令的時候,使用哪種返回指令取決於方法返回值的資料型別。
Java虛擬機器根據不同資料型別有不同的底層return指令。當被呼叫方法執行某條return指令時,會選擇相應的return指令來讓值返回。
在這種情況,當前棧楨就被用來恢復呼叫者的狀態,都恢復哪些呢?恢復區域性變量表、運算元棧 和程式計數器(pc指標),而這個程式計數器要適當地增加,來指向下一條指令(也就是呼叫函式的下一句)。使呼叫者方法能夠正常地繼續執行下去,而且返回值push到了呼叫方法的運算元棧中。

本地方法棧(Native Stack)

本地方法棧(Native Method Stacks)與虛擬機器棧所發揮的作用是非常相似的,其區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native方法服務

程式計數器(PC Register)

程式計數器就是記錄當前執行緒執行程式的位置,改變計數器的值來確定執行的下一條指令,比如迴圈、分支、方法跳轉、異常處理,執行緒恢復都是依賴程式計數器來完成。
   Java虛擬機器多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式實現的。為了執行緒切換能恢復到正確的位置,每條執行緒都需要一個獨立的程式計數器,所以它是執行緒私有的。

直接記憶體

直接記憶體並不是虛擬機器記憶體的一部分,也不是Java虛擬機器規範中定義的記憶體區域。jdk1.4中新加入的NIO,引入了通道與緩衝區的IO方式,它可以呼叫Native方法直接分配堆外記憶體,這個堆外記憶體就是本機記憶體,不會影響到堆記憶體的大小。

JVM記憶體引數設定

在這裡插入圖片描述

  • -Xms設定堆的最小空間大小。
  • -Xmx設定堆的最大空間大小。
  • -Xmn:設定年輕代大小。
  • -XX:NewSize設定新生代最小空間大小。
  • -XX:MaxNewSize設定新生代最大空間大小。
  • -XX:PermSize設定永久代最小空間大小。
  • -XX:MaxPermSize設定永久代最大空間大小。
  • -Xss設定每個執行緒的堆疊大小
  • -XX:+UseParallelGC:選擇垃圾收集器為並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用序列收集。
  • -XX:ParallelGCThreads=20:配置並行收集器的執行緒數,即:同時多少個執行緒一起進行垃圾回收。此值最好配置與處理器數目相等。

典型JVM引數配置參考:

  • java-Xmx3550m-Xms3550m-Xmn2g-Xss128k
  • -XX:ParallelGCThreads=20
  • -XX:+UseConcMarkSweepGC-XX:+UseParNewGC

-Xmx3550m:設定JVM最大可用記憶體為3550M。
-Xms3550m:設定JVM促使記憶體為3550m。此值可以設定與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配記憶體。
-Xmn2g:設定年輕代大小為2G。整個堆大小=年輕代大小+年老代大小+持久代大小。持久代一般固定大小為64m,所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,官方推薦配置為整個堆的3/8。
-Xss128k:設定每個執行緒的堆疊大小。JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K。根據應用的執行緒所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的執行緒。但是作業系統對一個程序內的執行緒數還是有限制的,不能無限生成,經驗值在3000~5000左右。