1. 程式人生 > 實用技巧 >深入理解jvm筆記(一)

深入理解jvm筆記(一)

深入瞭解java虛擬機器筆記(一)

第一章 走進java

第一章主要介紹了java的發展史和JDK編譯。java發展史粗略看了下,瞭解一下即可。至於JDK編譯,暫且跳過,等有時間,有需要去閱讀JDK原始碼時在去實操。

第二章 java記憶體區域與記憶體溢位異常

2.1 概述

在記憶體管理領域,c++開發人員即可以擁有一個物件的“所有權”,又需要肩負對每個物件從開始到結束的維護責任。而對於java開發人員來說,java虛擬機器就是個boss級別的存在,擁有控制記憶體的權力,不需要為每個new操作去delete/free。但是一但遇到問題,不瞭解jvm,debug就會很艱難。

2.2 記憶體區域

image-20201011185410191

2.2.1 程式計數器

概念:程式計數器是當前執行緒所執行的位元組碼的行號指示器

特性:

  • 分支,迴圈,跳轉,異常處理,執行緒恢復等基礎功能都需要依賴程式計數器,是程式控制流的指示器
  • 執行緒私有。各執行緒間的計數器互不影響,獨立儲存
  • 多執行緒執行時通過計數器恢復到正確的執行位置

異常:在執行Java方法時,計數器記錄的是正在執行的是虛擬機器位元組碼指令的地址;如果執行的是一個本地(Native)方法,計數器則為空(Undefined)。這個記憶體區域是唯一一個沒有規定任何OutOfMemoryEttor情況的區域。

2.2.2 Java虛擬機器棧

概念和儲存:虛擬機器描述的是Java方法執行的執行緒記憶體模型:每個方法被執行的時候,Java虛擬機器都會建立一個棧幀,用於儲存區域性變量表,運算元棧,動態連線,方法出口等資訊,每個方法被呼叫直至執行完畢,對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程,即先進後出,呼叫遞迴的原理基於此。

特性:

  • 執行緒私有,生命週期與執行緒相同

對於Java虛擬機器棧,主要關心的就是其區域性變量表的儲存,在我們定義一個變數的時候,經常會被問到,這個變數到底被儲存在哪裡。而區域性變量表存放了各種Java虛擬機器基本資料型別(boolean,byte,char,short,int,float,long,double),物件引用(reference型別,一個指向物件的指標或者是一個控制代碼,不是物件本身)和returnAddress型別(指向了一條位元組碼指令的地址)。

區域性變量表在編譯期就已經完全確定好大小了,在方法執行期間不會改變區域性變量表的大小。同時,對於64位長度的long和double型別的資料會佔用兩個變數槽(Slot),其餘資料型別只佔用一個。

異常:在Java虛擬機器規範中,對這個記憶體規定了兩類異常狀況:StackOverflowError和ouOutOfMemoryError異常。

2.2.3 本地方法棧

本地方法棧和Java虛擬機器棧類似,唯一區別是本地方法棧是用來執行Native方法的。而有些虛擬機器(Hot-Spot虛擬機器)將Java虛擬機器棧和本地方法棧合二為一。

2.2.4 Java堆

概念:Java堆是虛擬機器所管理的記憶體中最大的一塊,被所有執行緒共享,此記憶體的唯一目的就是存放物件例項。Java堆還是垃圾收集器管理的記憶體區域。

Java虛擬機器規範中描述:所有的物件例項以及陣列都應當在堆上分配。

特性:

  • 從分配記憶體的角度看,所有執行緒共享的Java堆中可以劃分出多個執行緒私有的分配緩衝區。
  • Java堆可以處於物理上不連續的記憶體空間,但在邏輯上它應該被視為連續的。
  • Java堆既可以被實現成固定大小,也可以進行擴充套件。

ps:將Java堆進行細分是為了更好地回收記憶體,更快地分配記憶體。

異常:在Java虛擬機器規範中,對這個記憶體規定了ouOutOfMemoryError異常。

2.2.5 方法區

概念:方法區是各個執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的型別資訊,常量,靜態變數,即時編譯器編譯後的程式碼快取等資料。

在JDK7的時候,把原本放在永久代(不完全等價方法區)的字串常量池,靜態變數等移出;在JDK8的時候,將永久代還剩餘的內容(主要是型別資訊)全部移動到元空間。

特性:

  • 方法區不需要連續的記憶體和可以選擇固定大小或可擴充套件,還可以選擇不實現垃圾收集
  • 方法區記憶體回收目標主要是針對常量池的回收和對型別的解除安裝

異常:如果方法區無法滿足新的記憶體分配需求時,將丟擲OutOfMemoryError異常

2.2.7 直接記憶體

概念:直接記憶體不是虛擬機器執行時資料區的一部分,也不是《Java虛擬機器規範》中定義的記憶體區域。

特性:

  • 在JDK1.4中引入了NIO類,可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆裡面的DirectByteBuffer物件作為這塊記憶體的引用進行操作,能夠顯著提高效能,避免了在Java堆和Native堆中來回複製

異常:動態擴充套件時可能會出現OutOfMemoryError異常

2.3 HotSpot虛擬機器物件探祕

2.3.1 物件的建立
  • 虛擬機器遇到一條new指令時,首先檢查這個指令的引數是否能在常量池裡定位到一個類的符號引用。並檢查這個類是否已經載入,解析和初始化過,如果沒有,先執行類的載入過程。
  • 類載入檢測通過後,jvm為新生物件分配記憶體,類載入完成後,物件的記憶體大小就定下來了。為物件分配空間等同與把一塊確定的記憶體從java堆中分出來。
  • 假設堆記憶體是規整的,有用的空閒的記憶體中間放一個指標當分界點,如果指標挪動,則可為物件分配空間,這種方式叫指標碰撞。
  • 假設堆記憶體是不規整的,jvm需要維護一個空閒列表來記錄哪塊記憶體是可用的,這種方式叫空閒列表。
  • 執行緒安全的解決方法:1.對分配記憶體空間的動作進行同步處理。2.每個執行緒在Java堆中預先分配一塊小記憶體,稱為本地執行緒分配緩衝(TLAB)。當TLAB用完後,才需要同步鎖。配置方法:-XX:+/-UseTLAB。
  • 記憶體分配完之後,JVM將分配的記憶體空間初始化為0(但不包括物件頭),如果使用TLAB,這一步可提前到TLAB分配時進行,這一步保證欄位有初始值。
  • 接下來,JVM對物件進行必要的設定,如這個物件是哪個類的例項,hashcode等,這些都是放在物件頭中。
  • 執行new之後會繼續執行方法,按照程式碼進行初始化。
2.3.2 物件的記憶體佈局
  • 物件分為三塊,頭(header),例項資料(instance data),對齊補充(padding)
  • header包括兩部分資訊。一部分儲存物件執行時資料如hashcode,GC分代年齡,鎖的標誌,執行緒持有的鎖,偏向執行緒ID,偏向時間戳等。這部分是Mark Word,Mark Word的資料結構是不固定的。另外一部分是型別指標,如果是陣列,還要記錄陣列的長度。
  • 例項資料儲存順序受分配策略引數和欄位定義的順序的影響,並且相同寬度的欄位會被排到一起。父類的變數會被安排到子類之前。如果CompactFields設為true。則子類的較窄的變數會插入到父類變數的空隙中。
  • 對齊補充是保證物件初始地址必須是8位元組的整數倍。
2.3.3 物件的訪問定位
  • 通過棧上的reference來操作堆上的物件。
  • 控制代碼訪問。Java堆中會分出一塊記憶體作為控制代碼池,在reference中儲存物件的控制代碼地址。物件被移動的時候不用改reference。
  • 直接指標訪問。reference直接儲存物件地址。節省了一次指標定位的時間開銷。

2.4 OOM異常

2.4.1 Java堆溢位
  • 不斷建立物件,超過容量限制。
  • 將堆的最小值-Xms與最大值-Xmx引數設定為一樣可避免自動擴充套件。
  • 用引數-XX:+HeapDumpOnOutOf-MemoryError。可以讓jvm在出現記憶體溢位的時候Dump出當前的記憶體堆轉儲快照以便分析。
  • 錯誤OutOfMemoryError:Java heap space
  • 如果是記憶體洩漏,通過工具集檢查洩露物件到GC Roots的引用鏈,檢查為什麼無法回收。
  • 如果記憶體中的物件確實都還活著。檢視某些物件是不是生命週期過長,持有時間過長等。
2.4.2 虛擬機器棧和本地方法棧溢位
  • HotSpot虛擬機器不區分虛擬機器棧和本地方法棧。
  • 棧容量由-Xss引數決定。
  • 棧的深度過大,將丟擲StackOverflowError異常。記憶體不足,丟擲OutMemoryError異常。
  • 單執行緒下,這兩種情況一般丟擲的都是StackOverflowError異常。
  • 作業系統分配給每個程序的記憶體是有限的,虛擬機器棧+本地方法棧=2GB-Xmx-MaxPermSize。
2.4.3 方法區和執行時常量池溢位
  • String:intern()是一個本地方法,如果字串常量池中已經存在了,則返回池中這個物件的引用;否則,將此值放在常量池中,並返回這個String物件的引用。在JDK7的intern方法不會複製例項。
  • 方法區存放的主要是class的相關資訊,如類名,欄位描述,訪問修飾符,常量池等等。
2.4.4 本機記憶體直接溢位
  • Direct Memory的容量大小通過-XX:MaxDirectMemorySize引數指定,預設與-Xmx一致。
  • 用DirectByteBuffer類分配記憶體丟擲記憶體溢位異常的時候並沒有真正向作業系統申請分配記憶體,而是通過計算得知無法分配記憶體。
  • 真正申請分配記憶體的方法是Unsafe::allocateMemory()。
  • 由Direct Memory導致的記憶體溢位,明顯特徵就是在Heap Dump檔案中不會有明顯的異常情況。