虛擬機學習之一:java內存區域與內存溢出異常

1.運行時數據區域

java虛擬機在執行java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域都有各自的用途和建立、銷燬時間,有的區域伴隨虛擬機進程的啓動而存在,有些區域則依賴用戶線程的啓動和結束而創建和銷燬。java

1.1程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠當作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏,字節碼解釋器工做就是經過改變這個計數器的值來選取下一條要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等都依賴與這個計數器完成。程序員

java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式實現的,因此在任什麼時候刻一個處理器只能執行一條線程的指令。所以,爲了線程切換以後能恢復到正確的執行位置,每條線程都須要一個獨立的程序計數器,各條線程之間程序計數器互不影響,獨立存儲。這類內存區域被稱爲「線程私有」內存。算法

若是線程正在執行的是java方法,這個計數器記錄的就是正在執行的虛擬機字節碼指令的地址。若是線程正在執行的是Native方法,計數器值爲空。此區域是java虛擬機規範中惟一一個沒有規定任何「OutOfMemoryError」狀況的區域。api

1.2java虛擬機棧

與程序計數器同樣,java虛擬機棧也是線程私有的,他的聲明週期與線程相同。虛擬機棧描述的是java方法執行的內存模型:每一個方法執行的同時都會建立一個棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用到執行完成的過程,都對應着一個棧幀在虛擬機中從入棧到出棧的過程。數組

局部變量表存放了編譯期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用和returnAddress類型。其中long和double數據會佔用兩個局部變量空間,其餘的佔用一個局部變量空間。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時須要在棧中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。安全

在java虛擬機規範中對虛擬機棧規定了兩種異常情況:若是線程請求深度大於虛擬機運行的請求深度,拋出StackOverflowError異常;若是虛擬棧能夠擴展,當擴展時沒法申請到足夠的內存空間,拋出OutOfMemoryError異常。多線程

1.3本地方法棧

本地方法棧與java虛擬棧很是類似,他們之間的區別只不過是java虛擬機棧是爲虛擬機執行java方法服務的,而本地方法棧是爲虛擬機執行Native方法服務的。ide

1.4java堆

對於大多數應用來講,java堆是java虛擬機所管理的內存中最大的一塊。java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。次內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分片內存。函數

java堆也是垃圾收集器管理的主要內存區域,因爲如今的收集器基本都採用分代收集算法,因此java堆能夠再劃分爲新生代和老年代;再細緻一點能夠劃分爲Eden空間、From Survivor空間、To Survivor空間等。oop

在實現時java堆既能夠是固定大小的內存區域,也能夠是可擴展的,當前主流的虛擬機都是可擴展的(經過-Xmx和-Xms控制)。若是堆中內存用完,且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。

1.5方法區

方法區(Method Area)與java堆同樣,也是一個各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

不一樣的虛擬機在這塊的實現不同。

在java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。

1.6運行時常量池

運行時常量池是方法區的一部分。主要用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。java語言並不要求常量必定只有在編譯期才能產生,也就是說並不是預置入Class文件中的常量池內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中。這種特性被開發人員利用的比較多的是String的intern()方法。

當運行時常量池在沒法申請到內存時也會拋出OutOfMemoryError異常。

1.7直接內存

直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域。可是這塊內存也被頻繁的使用,並且也可能致使OutOfMemoryError異常出現。

被使用的例子:NIO類引入了一種基於通道與緩衝區的I/O方式。它可使用Native函數庫直接分配堆外內存,而後經過存儲在java堆中的一個DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣就避免了java堆和Native堆之間來回複製數據。

2.java中對象的建立和使用

以HotSpot虛擬機爲例介紹java堆內存對象的建立、內存佈局以及訪問定位。

2.1對象的建立

在java語言中對象的建立一般是經過new關鍵字進行建立,但在虛擬機中時一個什麼過程?

1.虛擬機接受到new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並檢查這個符號引用表明的類是否已被加載、解析和初始化。(若是沒有先進行類加載)

2.虛擬機將爲新對象分配內存,在類加載以後已經肯定對象須要的內存大小。這個過程等同於從堆內存中爲對象劃分出一塊肯定大小的內存空間。

分配方式

指針碰撞:若是java堆中內存時絕對規整的,全部用過的內存存放在一邊,空閒內存在另外一邊,中間放着一個指針做爲分界點指示器。那分配內存就是把指針向空閒內存方向移動對象大小相等的距離。

空閒列表:若是java堆中內存並不規整,虛擬機就必須維護一個列表,在列表上記錄那些內存時可用的,在分配的時候就從列表中找一塊足夠大的空間分給對象實例,並更新列表上的記錄。

分配時線程安全問題也有兩種方案

一種是在分配內存空間的動做進行同步處理,實際上虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性,

另外一種是爲每一個線程預先在java堆中分配一小塊內存空間(線程分配緩衝)哪一個線程須要分配內存就在該線程事先分好的內存空間進行。

3.內存分配完成以後,虛擬機須要將分配到的內存空間都初始化爲零值。

4.虛擬機對對象進行必要的設置,會在對象頭中設置例如該對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。

5.對象進行初始化,通常會執行<init>方法按照程序員的意願爲對象初始化。這樣一個真正可用的對象纔算建立出來。

2.2對象的內存佈局

在HotSpot虛擬機中對象在內存中的佈局能夠劃分爲3塊區域:對象頭、實例數據、對齊填充。

對象頭:對象頭又分爲兩部分信息,第一部分存儲對象自身運行時數據如哈希碼、GC分代年齡、所狀態標誌、線程所持有的鎖、偏向線程ID、偏向時間戳等;第二部分是類型指針,對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。(不是全部虛擬機實現都須要保留類型指針,固然若是是數組類型在對象頭中還要有一塊用於存儲數組長度)。

實例數據:對象真正存儲的有效信息,程序代碼中定義的各類類型字段的內容。

對齊填充:這部分並非必然存在的,由於HotSpot虛擬機要去對象起始地址必須是8字節的整數倍,換句話說也就是對象大小必須是8字節的整數倍,而對象頭部分正好是8字節的倍數,所以當對象實例數據內容沒有對齊時,就須要經過對齊填充來補全。

2.3對象的訪問定位

創建對象是爲了使用對象,java程序須要經過棧上的reference數據來操做堆上具體對象。對象的訪問方式根據虛擬機實現主要有兩種:使用句柄訪問和指針訪問。

句柄訪問:若是使用句柄訪問那麼java堆中就必須劃分出一塊內存來做爲句柄池,reference中存儲的就是句柄池中對象句柄的地址。而句柄中就要保存對象實例數據地址和對象類型數據地址。

指針訪問:若是使用直接指針訪問,在java堆中對象的內存佈局就要考慮如何放置對象類型數據的相關信息。

使用句柄的優點就是reference中存儲的是穩定的句柄地址,在對象被移動是隻會改變句柄中實例數據指針,而reference自己不須要修改。

使用直接指針訪問方式的好處就是速度更快,它節省了一次指針定位的時間開銷。

3.實戰:OutOfMemoryError異常

3.1java堆溢出

java堆用於存儲對象實例,只要不斷建立對象實例,而且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量達到最大堆的容量限制以後就會出現內存溢出的異常。

咱們能夠經過實例來演示:首先限制java堆內存大小,不可擴展而後不停建立對象。

虛擬機運行參數配置:

Dfile.encoding=UTF-8 -verbose.gc -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\data -XX:SurvivorRatio=8

代碼:

public class VmTest {

	static class OOMObject{
		
	}
	
	public static void main(String[] args) {
		List<OOMObject> list = new ArrayList<>();
		while(true){
			list.add(new OOMObject());
		}
	}
}

運行結果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:\data\java_pid8168.hprof ...
Heap dump file created [28070894 bytes in 0.145 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at com.sean.esapi.client.VmTest.main(VmTest.java:15)

java堆的內存OOM異常是實際應用中常見的內存溢出異常狀況。當出現java堆內存溢出時,異常堆棧信息「java.lang.OutOfMemoryError」會跟着進一步提示「Java heap space」。

要解決這個區的異常主要就是確認內存中的對象是否必要的,也就是要分清楚是內存泄露仍是內存溢出。若是是內存泄露就要找到泄露對象到GC Roots的引用鏈,因而就能找到泄露對象是經過怎樣的路徑與GC Roots相關聯並致使垃圾收集器沒法回收他們的。

若是不存內存泄露就要一方面根據物理機內存對比看看是否能夠擴充java堆內存空間。另外一方面從代碼層面看看是否有對象生命週期過長或持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。

3.2虛擬機棧和本地方法棧溢出

因爲在HotSpot虛擬機中並不區分虛擬機棧和本地方法棧,所以對於HotSpot來講雖然-Xoss參數(設置本地方法棧大小)存在但實際上無效。虛擬機提供了爲單個線程設置棧容量的參數設置-Xss。

java虛擬機規範中描述有兩種異常:

StackOverflowError:若是線程請求的棧深度大於虛擬機容許的最大深度,拋出StackOverflowError異常。

OutOfMemoryError:若是虛擬機在擴展棧時沒法申請到足夠的內存空間,拋出OutOfMemoryError異常。

在單線程模式下經過遞歸方法測試設置-Xss參數減少棧內存的容量,出現StackOverflowError異常,減少-Xss參數以後再次運行發現出現StackOverflowError異常時遞歸的深度變小。說明當棧內存縮小時虛擬機容許的最大深度相應縮小。

設置:

-Xss128k

運行:

public class VMStackTest {
	
	private int stackLength = 1;
	
	public void stackLeak() {
		stackLength ++;
		System.out.println(stackLength);
		stackLeak();
	}
	
	public static void main(String[] args) {
		VMStackTest vmst = new VMStackTest();
		try {
			vmst.stackLeak();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

結果:

1
...
974
975
976
977
Exception in thread "main" java.lang.StackOverflowError
	at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
	at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)
	at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)
	at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
	at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
	at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
	at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
	at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
	at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
	at java.io.PrintStream.write(PrintStream.java:526)
	at java.io.PrintStream.print(PrintStream.java:597)
	at java.io.PrintStream.println(PrintStream.java:736)

從新設置-Xss參數:

-Xss512k

一樣運行上面代碼

結果:

1
...
5064
5065
5066
5067
Exception in thread "main" java.lang.StackOverflowError
	at java.io.FileOutputStream.write(FileOutputStream.java:326)
	at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
	at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
	at java.io.PrintStream.write(PrintStream.java:482)
	at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
	at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
	at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
	at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
	at java.io.PrintStream.write(PrintStream.java:527)
	at java.io.PrintStream.print(PrintStream.java:597)

當定義大量的本地變量,增大方法幀中本地變量表的長度,在較小的調用深度就會出現拋出StackOverflowError異常。

JVM未提供設置整個虛擬機棧佔用內存的配置參數。虛擬機棧的最大內存大體上等於「JVM進程能佔用的最大內存(依賴於具體操做系統) - 最大堆內存 - 最大方法區內存 - 程序計數器內存(能夠忽略不計) - JVM進程自己消耗內存」。當虛擬機棧可以使用的最大內存被耗盡後,便會拋出OutOfMemoryError,能夠經過不斷開啓新的線程來模擬這種異常(這種方式容易耗盡操做系統資源致使宕機)

配置:

-Xss128k

運行以下代碼沒有獲得OutOfMemoryError異常:

public class VMStackTest {

	public void stackLeak() {
		String name;
		while (true) {
			name = "nihao";
			try {
				Thread.sleep(100000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

	}
	
	public void stackLeakByThread(){
		int count = 0;
		while (true) {
			count ++;
			Thread t = new Thread(new Runnable(){
				@Override
				public void run() {
					stackLeak();
				}
			});
			t.start();
			System.out.println(count);
		}
	}

	public static void main(String[] args) {
		VMStackTest vmst = new VMStackTest();
		vmst.stackLeakByThread();
	}

}

3.3方法區和運行時常量池溢出

方法區用於存放Class的相關信息,如類名、訪問修飾符、常量池、字符描述、方法描述等。對於這些區域的測試,基本思路就是運行時產生大量的類去填滿方法區,直到溢出。這裏經過CGLib直接操做字節碼運行時生成了大量的動態類。

注意JVM8中把運行時常量池、靜態變量也移到堆區進行存儲。方法區方法區主要是存儲類的元數據的,如虛擬機加載的類信息、編譯後的代碼等。JDK8以前方法區的實現是被稱爲一種「永久代」的區域,這部分區域使用JVM內存,可是JDK8的時候便移除了「永久代(Per Gen)」,轉而使用「元空間(MetaSpace)」的實現,並且很大的不一樣就是元空間不在共用JVM內存,而是使用的系統內存。

 

3.4本機直接內存溢出

DirectMemory容量可經過-XX:MaxDirectMemorySize指定,若是不指定,則默認爲與java堆最大值(-Xmx指定)同樣,咱們測試時直接越過DirectByteBuffer類,直接經過反射獲取Unsafe實例進行內存分配。由於雖然使用DirectByteBuffer分配內存也會出現內存溢出異常,但它拋出異常時並無去向操做系統申請內存,而是經過計算得知內存沒法分配手動拋出內存溢出異常。真正申請分配內存的方法是unsafe.allocateMemory()。

配置:

-Xmx20M -XX:MaxDirectMemorySize=10M

運行:

public class DirectMeoryOOM {

	private static final int _1MB = 1024 * 1024;
	
	public static void main(String[] args) throws Exception {
        //經過反射獲取到unsafe實例,再經過unsafe實例申請內存
		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.sean.esapi.client.DirectMeoryOOM.main(DirectMeoryOOM.java:16)

由DirectMemory致使內存溢出的一個明顯特徵就是在Heap Dump文件中不會看到明顯的異常,若是發現OOM以後Dump文件有很小,程序直接或間接又使用了NIO,能夠考慮這方面緣由。

相關文章
相關標籤/搜索