Java記憶體模型學習總結

johnychen發表於2021-09-09

1. 程式計數器(Program Counter Register)

      當前執行緒所執行的位元組碼的行號指示器。如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是native方法,這個計數器值則為空(Undefined)。執行緒私有。唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

2. Java虛擬機器棧(Java Virtual Machine Stack)

       虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應一個棧幀在虛擬機器棧中入棧到出棧的過程。執行緒私有,隨執行緒建立而生,執行緒結束而死。大小透過-Xss設定,例如:-Xss1m(HotSpot預設);此引數的大小和能建立的最大執行緒數量有著密切的關係,在堆記憶體固定的情況下此引數值越大能夠建立的執行緒越小。在此引數固定的情況下堆記憶體越大能夠建立的執行緒越少,如下是32位HotSpot虛擬機器1.7.0_80版本下不同-Xss值能夠建立的最大執行緒的數量:

堆記憶體 虛擬機器棧 最大執行緒數量
-Xmx1g -Xss128k 2000
-Xmx1g -Xss256k 1512
-Xmx1g -Xss512k 1017
-Xmx1g -Xss1m 620

之所以會出現這種情況是因為作業系統分配給每個程式的記憶體是有限制的,譬如32位的Windows限制為2GB。虛擬機器提供了引數來控制Java堆和方法區的這兩部分記憶體的最大值。剩餘的記憶體為2GB(作業系統限制)減去Xmx(最大堆記憶體),再減去MaxPermSize(最大方法區容量),程式計數器消耗記憶體很少,可以忽略不計。如果虛擬機器程式本身消耗的記憶體不計算在內,剩下的記憶體就由虛擬機器棧和本地方法棧瓜分了。每個執行緒分配到的棧容量越大,可以建立的執行緒數量自然就越少,建立執行緒時就越容易把剩下的記憶體耗盡。當不能建立更多執行緒的時候就會丟擲java.lang.OutOfMemoryError: unable to create new native thread異常,例如下面的程式:

package com.zws.jvm;import java.util.concurrent.atomic.AtomicInteger;/** * JVM args: -server -Xmx1g -Xss512k * @author wensh.zhu * */public class XssMaxThread {	public static final AtomicInteger counter = new AtomicInteger();	public static void main(String[] args) {		try {			while (true) {				new Thread(new Runnable() {					public void run() {						try {							counter.incrementAndGet();							Thread.sleep(1000 * 60 * 60);						} catch (InterruptedException e) {							e.printStackTrace();						}					}				}).start();			}		} catch (Throwable e) {			System.out.println(counter.get());			e.printStackTrace();		}			}}

輸出如下:

980java.lang.OutOfMemoryError: unable to create new native thread	at java.lang.Thread.start0(Native Method)	at java.lang.Thread.start(Thread.java:714)	at com.zws.jvm.XssMaxThread.main(XssMaxThread.java:24)

      綜上此異常可透過適當減小堆記憶體或虛擬機器棧記憶體解決。Java虛擬機器規範中有兩種異常:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常;如果虛擬機器棧可以動態擴充套件,如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。如下程式碼:

package com.zws.jvm;/** * JVM args:-server -Xss128k * @author wensh.zhu * */public class StackOver {	int num = 0;	public static void main(String[] args) {		StackOver stackOver = new StackOver();		try {			stackOver.count();		} catch (Throwable e) {			System.out.println("stack length:" + stackOver.num);			e.printStackTrace();		}	}		public void count() {		num ++;		count();	}}

輸出如下:

stack length:2105java.lang.StackOverflowError	at com.zws.jvm.StackOver.count(StackOver.java:17)

3. 本地方法棧(Native Method Stack)

      類似於Java虛擬機器棧,服務於native方法。HotSpot虛擬機器直接把本地方法棧和虛擬機器棧合二為一;此記憶體空間會丟擲StackOverflowError和OutOfMemoryError異常。

4. Java堆(Java Heap)

      用於存放物件例項和陣列,java虛擬機器規範規定:所有物件例項以及陣列都要在堆上分配。可細分為:新生代和老年代。新生代又分為:Eden空間、From Survivor空間、To Survivor空間。之所以這麼分是為了最佳化GC效能。大小透過-Xms和-Xmx兩個引數設定其初始大小和最大大小。例如將初始大小和最大大小都設定為1g:-Xms1024m  -Xmx1024m。還可以透過以下引數設定各個空的大小以及比例:

-XX:NewRatio     老年代大小與新生代大小的比例,預設-XX:NewRatio=2-XX:NewSize     新生代初始大小,預設-XX:NewSize=2m-XX:MaxNewSize    最大新生代大小,預設值透過-XX:NewRatio計算得來-XX:SurvivorRatio  Eden和其中一個Survivor的比值 預設-XX:SurvivorRatio=8,即Edem:from:to = 8:1:1

此記憶體空間執行緒共享。可丟擲OutOfMemoryError異常。如下程式碼:

package com.zws.jvm;import java.util.LinkedList;import java.util.List;/** * JVM args:-server -Xmx128m * @author wensh.zhu * */public class OOMTest {	private int[] numbers = new int[1024];	public static void main(String[] args) {		List testObjs = new LinkedList();		for (;;) {			testObjs.add(new OOMTest());		}	}	public int[] getNumbers() {		return numbers;	}	public void setNumbers(int[] numbers) {		this.numbers = numbers;	}}

輸出如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space	at com.zws.jvm.OOMTest.(OOMTest.java:11)	at com.zws.jvm.OOMTest.main(OOMTest.java:16)

5. 方法區(Method Area)

      用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、及時編譯器編譯後的程式碼等資料。HotSpot虛擬機器習慣上稱之為永久代,可透過-XX:PermSize和-XX:MaxPermSize兩個引數設定其初始值以和最大值,例如:-XX:PermSize=128m、-XX:MaxPermSize=128m,在Java8中這兩個引數以及被移除,取而代之的是-XX:MetaspaceSize和-XX:MaxMetaspaceSize;執行緒共享。可丟擲OutOfMemoryError異常。

6. 執行時常量池(Runtime Constant Pool)

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

7. 直接記憶體

      直接記憶體(DirectMemory)並不是虛擬機器執行時資料區的一部分,在JDK1.4中新加入了NIO(New Iput/Output)類,引入一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後透過一個儲存在Java堆中的DirectByteBufTer物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提髙效能,閃為避免了在Java堆和Native堆中來回複製資料。顯然,本機直接記憶體的分配不會受到Java堆大小的限制,但是,既然是記憶體,肯定還是會受到本機總記憶體(包括RAM以及SWAP區或者分頁檔案)大小以及處理器定址空間的限制。伺服器管理員在配置虛擬機器引數時,會根據實際記憶體設定-Xmx等引數資訊,但經常忽略直接記憶體,使得各個記憶體區域總和大於實體記憶體限制(包括物理的和作業系統級的限制),從而導致動態擴充套件時出現OutOfMemoryError異常。DirectMemory容量可透過-XX:MaxDirectMemorySize指定,如果不指定,則預設與Java堆最大值(-Xmx指定)一樣。當Direct ByteBuffer分配的堆外記憶體到達指定大小後,即觸發Full GC。如下程式碼浮現了直接記憶體溢位異常:

package com.zws.jvm;import java.lang.reflect.Field;import sun.misc.Unsafe;/** *  * @author wensh.zhu * */public class DirectMemoryOOM {	private static final int _1MB = 1024 * 1024;	public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {		Field unsafeField = Unsafe.class.getDeclaredFields()[0];		unsafeField.setAccessible(true);		Unsafe unsafe = (Unsafe) unsafeField.get(null);		while (true) {			unsafe.allocateMemory(_1MB);		}	}}

異常資訊:

Exception in thread "main" java.lang.OutOfMemoryError	at sun.misc.Unsafe.allocateMemory(Native Method)	at com.zws.jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:15)

      由DirectMemory導致的記憶體溢位,一個明顯的特徵是在Heap Dump檔案中不會看見明顯的異常,如果發現OOM之後Dump檔案很小,而程式中有直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。

                                                                                      參考周志明《深入理解Java虛擬機器 JVM高階特性與最佳實踐》


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4301/viewspace-2813820/,如需轉載,請註明出處,否則將追究法律責任。

相關文章