1. 程式人生 > >《深入理解Java虛擬機器》讀書筆記:第二章Java記憶體區域與記憶體溢位異常

《深入理解Java虛擬機器》讀書筆記:第二章Java記憶體區域與記憶體溢位異常

Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域:方法區、虛擬機器棧、本地方法棧、堆、程式計數器

程式計數器(ProgramCounterRegister):一塊較小的記憶體空間,看作當前執行緒所執行的位元組碼的行號指示器;位元組碼直譯器工作時通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。分支,迴圈,跳轉,異常處理,執行緒恢復等基礎功能都需要依賴這個計數器來完成

為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各執行緒之間計數器互不影響,獨立儲存(執行緒私有)

Java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的

Java虛擬機器棧(JavaVirtual Machine Stack):描述Java方法執行的記憶體模型 -- 每個方法在執行的同時都會建立一個棧幀用於儲存區域性變量表,運算元棧,動態連結,方法出口等資訊

區域性變量表存放了編譯器可知的各種基本資料型別,物件引用和returnAddress型別,區域性變量表所需的記憶體空間在編譯期間完成分配

  • 基本資料型別(boolean,byte,char,int,long,double,float其中64位長度的long和double型別的資料會佔用2個區域性變數空間(Slot),其餘1個)
  • 物件引用(reference,不同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是一個代表物件的控制代碼或其他與此物件相關的位置)
  • returnAddress型別(指向了一條位元組碼指令的指令) 

本地方法棧(Native Method Stack):虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,本地方法棧則為虛擬機器使用到的Native方法服務。兩者都會StackOverflowError(記憶體溢位,)和OutOfMenoryError(記憶體不足)

*有的虛擬機器把本地方法棧和虛擬機器棧合二為一  

Java(JavaHeap):堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立,唯一目的是存放物件例項(分配物件例項以及陣列),是垃圾收集器管理的主要區域

堆中細分: 新生代,老年代 ;新生代再細分 Eden,From Survivor,To Survivor空間;預設 Eden:Survivor=8:1

如果在堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將丟擲OutOfMenoryError

方法呼叫巢狀層數過大引起 StackOverflowError

方法區(Method Area):各個執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即編譯器編譯後的程式碼等資料.如類名,訪問修飾符,常量池,欄位描述等

方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError

執行時常量池(Runtime Constant Pool)是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放

直接記憶體(Direct Memory):並不是虛擬機器執行時資料區的一部分,而是本機直接記憶體.在NIO中,通過Native(本地)函式庫直接分配堆外記憶體,然後通過一個儲存在堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作,避免了在Java堆和Native堆(Java堆之外的本機記憶體)中來回複製資料

物件的建立(HotSpot為例)

虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入,解析和初始化過.如果沒有,那必須先執行相應的類載入過程

  • 符號引用是一個字串,它給出了被引用的內容的名字並且可能會包含一些其他關於這個被引用項的資訊——這些資訊必須足以唯一的識別一個類、欄位、方法

類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體

記憶體分配的方式:

  1. 指標碰撞(堆中的記憶體規整,中間一個指標作為分界點兩邊是用的和空閒的記憶體);
  2. 空閒列表(虛擬機器維護一個列表記錄記憶體可用,從列表中查詢)

*Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定

物件的建立十分頻繁,併發下非執行緒安全 .解決方法: 1.同步處理分配記憶體空間的動作;2.把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer,TLAB)

記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值,接下來對虛擬機器對物件進行必要的設定,將類的元資料資訊,物件的雜湊碼,物件的GC分代年齡等存放在物件的物件頭.此時一個新的物件已經產生,但<init>方法(初始化)還沒執行,所有欄位都是零.執行new 指令後接著執行init方法進行初始化

物件在記憶體中儲存的佈局分為3塊區域:物件頭(Header),例項資料(InstanceData)和對齊填充(Padding)

物件頭一部分儲存物件自身的執行時資料(稱為Mark Word),如雜湊碼,GC分代年齡,鎖狀態標誌,執行緒持有的鎖,偏向鎖ID,偏向時間戳;

另一部分是型別指標,即物件指向它的類元資料的指標.虛擬機器通過這個指標來確定這個物件是哪個類的例項

虛擬機器可以通過普通Java物件的元資料資訊確定Java物件的大小;如果物件是一個Java陣列,物件頭中還必須有一塊用於記錄陣列長度的資料

例項資料部分是物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容,無論是從父類繼承的還是在子類定義的

這部分儲存順序受到虛擬機器分配策略引數和欄位在Java原始碼中定義順序的影響

HotSpot虛擬機器預設的分配策略為longs/doubles,ints,shorts/chars,bytes/booleans,oops    相同寬度的欄位總被分配到一起

對齊填充並不是必然存在的,僅僅起著佔位符的作用HotSpotVM的自動記憶體管理系統要求物件的起始地址必須是8位元組的整數倍,而物件頭部分是8位元組的倍數,當例項資料部分沒有對齊時就需要對齊填充部分來補全

Java程式通過棧上的reference資料來操作堆上的具體物件.reference型別在Java虛擬機器規範中只規定了一個指向物件的引用,並沒有定義這個引用應該通過何種方式去定位,訪問堆中的物件的具體位置.所以物件的訪問方式取決於虛擬機器實現,主流的訪問方式有使用控制代碼和直接指標兩種

  • 控制代碼:Java堆中劃分一塊記憶體作為控制代碼,reference中儲存的就是物件的控制代碼地址.控制代碼中包含了物件例項資料與型別資料各自具體的地址資訊(可以把控制代碼看作是物件的索引)
  • 直接指標:reference中儲存的直接就是物件地址(比上面少了一層,索引只在reference中)


兩者各有優勢:使用控制代碼在物件被移動時,不改變reference而只改變控制代碼中的例項資料指標;而使用直接指標可以節省一次指標定位的時間開銷 

Java堆記憶體溢位異常時,異常堆疊資訊 java.lang.OutOfMemoryError: Java heap space,會進一步提示堆空間

一些JVM引數

-Xmx (指定JVM堆的最大記憶體,在JVM啟動以後,會分配-Xmx引數指定大小的記憶體給JVM,但是不一定全部使用,JVM會根據-Xms引數來調節真正用於JVM的記憶體

-Xms (指定了JVM初始啟動以後初始化最小堆記憶體);例如 -Xmx5m

-XX:+HeapDumpOnoutOfMemoryError(讓虛擬機器在出現記憶體溢位異常時備份出當前的記憶體堆轉儲快照)

虛擬機器棧和本地方法棧溢位:執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverflowError

虛擬機器在擴充套件棧時無法申請到足夠的記憶體空間則丟擲OutOfMemoryError,兩者本質上都是記憶體太小

-Xss:棧記憶體大小

JDK1.6執行時常量池溢位:在OutOfMemoryError後面跟隨的提示資訊是"PermGen space",(執行時常量池屬於方法區 永生代)

Java8 刪除了Hotspot JVM中的永生代記憶體(PermGen,永生代記憶體主要儲存一些需要常駐記憶體,不會被回收的資訊),改為使用本地記憶體來儲存類的元資料資訊,並將之稱為元空間(Metaspace),也就不會遇到java.lang.OutOfMemoryError:PermGen錯誤

引數: -XX:PermSize=64m  -XX:MaxPermSize=128m 需變成 -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m

關於常量池的一個有趣的例子

	public static void main(String[] args) {
	
		//intern返回常量池中記錄首次出現的例項
		//String.intern()方法  當呼叫 intern 方法時,如果池已經包含一個等於此 String 物件的字串(用 equals(Object) 方法確定),則返回池中的字串。否則,將此 String 物件新增到池中,並返回此 String 物件的引用。 
		String str1 = new StringBuilder("計算機").append("軟體").toString();
		System.out.println(str1.intern() == str1); //true
		
		String str2 = new StringBuilder("ja").append("va").toString(); //其他像int,float,double,byte等也是將會返回false
		System.out.println(str2.intern() == str2); //false   StringBuilder.toString之前,字串常量池裡面已經有了java這個字串,不是首次出現
	}

本機直接記憶體溢位:直接記憶體(Direct Memory)通過 -XX:MaxDirectMemorySize指定,不指定則預設與Java堆最大值一樣,明顯特徵是Heap Dump檔案中不會看到明顯的異常