(一)學習JVM ——運行時數據區域

(一)學習JVM ——運行時數據區域java

(二)學習JVM —— 垃圾回收機制 數組

(三)學習JVM —— 垃圾回收器 安全

(四)學習JVM —— 內存分配與回收策略 bash

1、概述

JVM是整個Java平臺的基石,是Java實現與硬件無關與操做系統無關的關鍵部分,是Java生成出極小體積的編譯代碼的運行平臺,是保障用戶機器免於惡意代碼損害的屏障。——《Java虛擬機規範》數據結構

JVM在執行Java程序時,會把它所管理的內存氛圍幾個不一樣的區域。其中有一些區域會隨着JVM啓動而建立,隨着JVM退出而銷燬。另一些則是與線程一一對應的,這些與線程對應的數據區域隨着線程的開始和結束而建立和銷燬。less

2、線程私有區域

2.一、程序計數器

程序計數器(Program Counter),也可成爲PC寄存器,它是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。eclipse

每一條JVM線程都有本身的PC寄存器。在虛擬機的概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令的。ide

任意時刻,一條JVM線程只會執行一個方法的代碼,這個正在被線程執行的方法稱爲該線程的當前方法(current method)。多個線程則是經過線程輪流切換並分配處理器執行時間的方式來實現的。函數

若是當前方法不是本地方法,那麼PC寄存器中就保存JVM正在執行的字節碼指令的地址,若是該方法是本地方法,那PC寄存器的值就是undefined。PC寄存器的容量至少應當能保存一個returnAddress類型的數據,或者保存一個與平臺相關的本地指針的值。工具

該內存區域是惟一一個在JVM規範中沒有規定任何OutOfMemoryError狀況的區域。

2.二、虛擬機棧

與程序計數器同樣,JVM棧也是線程私有的,它的生命週期與JVM線程相同。每一條JVM線程都有本身私有的JVM棧(Java Virtual Machine stack),這個棧與線程同時建立,用於存儲棧幀(Stack Frame)。

每一個方法在執行的同時都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態鏈接、方法出口等信息。每個方法從調用知道執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

除了棧幀入棧和出棧,JVM棧不會在受其餘因素的影響,因此棧幀能夠在堆中分配,JVM棧所使用的內存不須要保證是連續的。

JVM規範既容許JVM棧被設置爲固定大小,也容許根據計算動態擴展和收縮。若是採用固定大小,那麼每個線程的JVM棧容量能夠在線程建立的時候獨立選定。

JVM棧可能發生的異常狀況有兩種:若是線程請求分配的棧容量超過JVM棧容許的最大容量,會拋出:StackOverflowError異常。若是JVM棧能夠動態擴展,而且在嘗試擴展的時候沒法申請到足夠的內存,或者在建立新的線程的時候沒有足夠的內存去建立對應的JVM棧,會拋出:OutOfMemoryError異常。

JVM提供了-Xss參數,對於設置JVM棧的最大值,下面的代碼會產生StackOverflowError。

package lesson1.test;

public class JavaVMStackSOF {

	private int stackLength = 1;

	public void stackLeak() {
		stackLength++;
		stackLeak();
	}

	// -verbose: -Xss128k
	public static void main(String[] args) {
		JavaVMStackSOF sof = new JavaVMStackSOF();
		try {
			sof.stackLeak();
		} catch (Throwable e) {
			System.out.println("stack length: " + sof.stackLength);
			throw e;
		}
	}

}

上面代碼設置-Xss的最大值爲128k後,運行程序,結果返回:

tack length: 988
Exception in thread "main" java.lang.StackOverflowError
	at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
	at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
	at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
	at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
	at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
	at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
	at lesson1.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)

……後續異常堆棧信息省略

實驗結果代表:在單個線程下,不管是因爲棧幀太大仍是JVM棧容量過小,當內存沒法分配時,會拋出StackOverflowError異常。

2.三、本地方法棧

本地方法棧(Native Method Stack)與JVM棧所發揮的做用是很是類似的,不一樣之處在於JVM棧執行Java方法,而本地方法棧執行Native方法。例如,當JVM虛擬機使用其餘語言(好比C語言)來實現執行的解釋器時,就可使用本地方法棧。

JVM規範容許本地方法棧實現成固定大小或者根據計算動態擴展和收縮。若是採用固定大小,那麼每個線程的本地方法棧容量能夠在建立棧的時候獨立選定。

本地方法棧可能出現的異常狀況有兩種,若是線程請求分配的棧容量超過本地方法棧容許的最大容量,JVM會拋出一個StackOverflowError異常。若是本地方法棧能夠動態擴展,而且在嘗試擴展的時候沒法申請到足夠的內存,或者在建立新的線城時沒有足夠的內存區建立對應的本地方法棧,JVM會拋出一個OutOfMemoryError異常。

HotSpot虛擬機並不區分本地方法棧和JVM棧,所以,對於HotSpot來講,雖然-Xoss參數存在,可是無效,佔容量只由-Xss參數設置。

3、線程共享區域

3.一、堆

堆(Heap)是可供各個線程共享的運行時內存區域,也是供全部類實例和數組對象分配內存的區域。對於大多數應用來講,堆是JVM所管理的內存中最大的一塊。堆在JVM啓動的時候就被建立了,它存儲了被自動內存管理系統(automatic storage management system),也就是垃圾回收器(GC)所管理的各類對象,這些受到管理的對象無需也沒法顯示地銷燬。

堆能夠處於物理上不連續的內存中,只要邏輯上是連續便可,就像磁盤空間同樣。堆的容量能夠是固定的,也能夠隨着程序執行的需求動態擴展,並在不須要過多空間時自動收縮。

堆可能發生的異常狀況是,若是實際所需的堆超過了自動內存管理系統能提供的最大容量,那JVM將拋出一個OutOfMemoryError異常。

JVM提供了參數-Xmx設置堆的最大空間,-Xms設置堆的最小值。

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {

	static class OOMObject {
	}
	
	// -verbose: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
	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 java_pid6252.hprof ...
Heap dump file created [27974147 bytes in 0.083 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 cn.net.bysoft.lesson1.HeapOOM.main(HeapOOM.java:15)

上面代碼將堆的最小值Xms設置爲20M,最大值Xmx也設置爲20M,即爲不擴展堆大小。經過參數-XX:HeapDumpOnOutOfMemoryError可讓JVM在內存溢出時Dump出當前的內存堆快照,以便分析。

可使用Eclipse Memory Analyzer打開快照, http://archive.eclipse.org/mat/1.4/update-site/

分析若是是內存泄漏,可進一步經過工具查看泄露對象到GC Roots的引用鏈,若是不存在泄漏,就是內存中的對象都存活着,那就須要修改JVM的堆參數。或者從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況。

3.二、方法區

方法區(Method Area)是可供各個線程共享的運行時內存區域,它與傳統語言中的編譯代碼存儲區(storage area for compiled code)或者操做系統的正文段(text segment)的做用很是的相似,它存儲了每個類的結構信息,例如,運行時常量池(runtime constant pool)、字段和方法數據、構造函數和普通方法的字節碼內容,還包括類、實例、接口初始化時用到的特殊方法。

方法區是堆的邏輯組成部分,它有一個別名叫作Non-Heap(非堆),目的是與堆區分開來。

方法區的容量能夠是固定的,也能夠隨着程序執行的需求動態擴展,並在不須要過多空間的時候自動收縮。方法區在實際內存中能夠是不連續的。

方法區可能發生一個異常,若是方法區的內存空間不能知足內存分配需求,那麼JVM將拋出一個OutOfMemoryError異常。

JVM提供了參數-PermSize設置方法區的最小值,-MaxPermSize設置方法區的最大值。

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class RuntimeConstantPoolOOM {

	// -XX:PermSize=10M -XX:MaxPermSize=10M
	public static void main(String[] args) {
		while (true) {
			Enhancer enhancer = new Enhancer();
			enhancer.setSuperclass(OOMObject.class);
			enhancer.setUseCache(false);
			enhancer.setCallback(new MethodInterceptor() {

				@Override
				public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
					return arg3.invoke(arg0, arg2);
				}

			});
		}
	}

	static class OOMObject {
	}

}

上面代碼藉助CGLib直接操做字節碼,運行時生成大量的動態類來使方法區OOM。

方法區用於存放Class的相關信息,如類名稱、訪問修飾符、常量池、字段描述、方法描述等。對於這些區域的測試,基本的作法就是運行時產生大量的類去填滿方法區,直到溢出。

上述代碼在JDK1.8後就無效了,由於JDK1.8徹底移除來永久帶,取而代之的是Metaspace(元數據空間),一樣的,它也提供了幾個參數來設置其大小, -XX:MetaspaceSize 初始空間大小 , -XX:MaxMetaspaceSize 最大空間, -XX:MinMetaspaceFreeRatio 在GC以後,最小的Metaspace剩餘空間容量的百分比 , -XX:MaxMetaspaceFreeRatio 在GC以後,最大的Metaspace剩餘空間容量的百分比 。

JDK8測試Metaspace溢出的代碼以下:

import java.util.ArrayList;
import java.util.List;

public class RuntimeConstantPoolOOM {

	static String base = "string";

	//-XX:MetaspaceSize=1M
	//-XX:MaxMetaspaceSize=1M
	public static void main(String[] args) {
		List<String> list = new ArrayList<>();
		for (int i = 0; i < Integer.MAX_VALUE; i++) {
			String str = base + base;
			base = str;
			list.add(str.intern());
		}
	}

}
OutOfMemoryError: Metaspace

3.三、運行時常量池

運行時常量池(runtime constant pool)是方法區的一部分,是class文件中每個類或接口的常量池表(constant_pool table)的運行時表示形式,它包括了若干種不一樣的常量,從編譯期可知的數值字面量到必須在運行期解析後才能得到的方法或字段的引用。它相似於傳統語言中的符號表(symbol table),不過它存儲數據的範圍更普遍。

每一個運行時常量池都在JVM的方法區中分配,在加載類和接口到JVM後,就建立對應的運行時常量池。

建立運行時常量池時可能會發生一個異常,若是構造運行時常量池所需的內存超過了方法區所能提供的最大值,那麼JVM將會拋出一個OutOfMemoryError異常。

四、堆中的數據

4.一、堆中建立對象

在語言層面上,建立對象(例如克隆,反序列化)一般僅僅經過一個new關鍵字而已,但在JVM中,對象的建立卻更加細緻。

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那必須先執行相應的類加載過程(類加載需單獨說明)。

在類加載檢查經過後,JVM將爲新對象分配內存。對象所需的內存大小在類加載後即可徹底肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從堆中劃分出來。

劃份內存的方式通常有兩種:

指針碰撞(Bump the Pointer):假設堆中的內存是絕對規整的,全部用過的內存都放在一邊,空閒的內存放在一邊,中間放着一個指針做爲分界點的指示器,那所分配內存就僅僅是把分界點指針往空閒空間端,移動一段與對象大小相等的距離。

空閒列表(Free List):若是堆中的內存並非規整的,已使用的內存和空閒內存相互交錯,JVM就必須維護一個列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象,並更新列表上的記錄。

選擇哪一種分配方式取決於堆內存是否規整,而堆內存是否規整由取決與採用的GC是否帶有壓縮整理功能。

除此以外,分配內存的過程也不是線程安全的,解決這個問題有兩種方案:

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

另外一種是把內存分配的動做按照線程劃分在不一樣的空間中進行,即每一個線程在堆中預先分配一小塊內存,成爲本地線程分配緩衝(Thread Local Allocation Buffer, TLAB)。

JVM是否採用TLAB,能夠經過-XX:+/-UseTLAB參數來設定。

分配好內存空間後,JVM要堆對象進行必要的設置,例如這個對象是那個類的實例、如何採用找到類的元數據信息、對象的HashCode、對象的GC分代年齡等信息。這些都在對象的Object Header中。

從JVM的視角看,一個新的對象已經產生了,但從Java程序的視角看,對象建立纔剛剛開始,<init>方法尚未執行,全部的字段仍是默認零值。

4.二、對象的內存佈局

在HotSpot JVM中,對象在內存中存儲的佈局有3塊區域:

對象頭(Object Header);

實例數據(Instance Data);

對齊填充(Padding);

對象頭(Object Header)

HotSpot JVM的對象頭包括兩部分信息:

第一部分用於存儲對象自身的運行時數據,例如HashCode、GC分代年齡、鎖狀態標誌、線程持有鎖、偏向線程ID、偏向時間戳等。着部分數據的長度在32位和64位的JVM中分別爲32bit和64bit,官方稱之爲Mark Word;

例如,在32位的JVM中,若是對象處於未鎖定狀態下,那麼Mark Word的32bit中,25bit用於存儲對象HashCode,4bit用於存儲對象分代年齡,2bit用於存儲鎖標誌位,1bit固定爲0。

第二部分是類型指針,即對象指向它的類元數據的指針,JVM經過這個指針來肯定這個對象是哪一個類的實例。若是對象是一個Java數組,那麼在對象頭中還必須有一塊用於記錄數組長度的數據,由於JVM能夠經過普通Java對象的元數據信息肯定Java對象的大小,可是從數組的元數據中卻沒法肯定其大小。

實例數據(Instance Data)

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各類類型的字段內容。不管是從父類繼承下來的,仍是在子類中定義的,都須要記錄起來。着部分的存儲順序會受到JVM分配策略參數和字段在Java源碼中定義順序的影響。

HotSpot默認分配爲longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),寬度相同的字段老是被分配到一塊兒。知足這個前提下,父類中定義的變量會出如今子類以前。若是CompactFields參數值爲true(default),那麼子類之中較窄的變量也能夠會加入到父類變量的空隙。

對齊填充(Padding)

第三部分對齊填充並非必然的,也沒有特別的含義,它僅僅起到佔位符的做用。因爲HotSpot自動內存管理系統要求對象起始地址必須是8字節的整數倍,也就是說對象大小必須是8字節的倍數。而對象頭部分正好是8字節的倍數,所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

4.三、對象的訪問定位

Java程序須要經過棧上的reference數據來操做堆上的具體對象。目前主流的訪問方式有兩種:

句柄訪問:堆中會劃分出一塊內存來做爲句柄池,reference中存儲的是對象的句柄地址,而句柄中包含來對象實例數據與類型數據各自的具體地址信息;

直接指針訪問:堆對象的佈局中,必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址;

兩種對象訪問方式各有優點,句柄的好處就是reference中存儲的是穩定的句柄地址,在對象被移動時只要改變句柄中的實例數據指針,二reference不須要改變。

使用直接指針訪問的好處就是速度夠快,節省來一次指針定位的時間開銷,頻繁的訪問對象,指針定位時間聚沙成塔後也是很是可觀的。

五、棧中的數據

棧中是用棧幀(frame)來存儲數據和部分過程結果的數據結構,同時也用來處理動態鏈接(dynamic linking)、方法返回值和異常分派(dispatch exception)。

它隨着方法調用而建立,隨着方法結束而銷燬——不管方法是正常結束仍是異常結束都算做方法結束。棧幀的存儲空間由建立它的線程分配在JVM棧之中,每個棧幀都有本身的本地變量表(local variable)、操做數棧(operand stack)和指向當前方法所屬的類的運行時常量池的引用。

本地方法表和操做數棧的容量在編譯期肯定,並經過相關方法的code屬性保存及提供給棧幀使用。

某條線程執行過程當中的某個時間點上,只有目前正在執行的那個方法的棧幀的活動的,稱爲當前棧幀(current frame),對應的方法稱爲當前方法(current method),對應的類成爲當前類(current class)。若是當前方法調用了其餘方法,或者當前方法執行結束,那麼這個方法的棧幀就再也不是當前棧幀了。調用新方法,棧幀會隨着建立,併成爲新的當前棧幀。返回時,會回傳該方法的執行結果給前一個棧幀,而後丟棄當前棧幀,是的前一個棧幀成爲當前棧幀。

5.一、局部變量表

每一個棧幀內部都包含一組稱爲局部變量表的變量列表。棧幀中局部變量表的長度由編譯期決定,而且存儲於類或接口的二進制表示之中。

一個局部變量能夠保存一個類型爲boolean、byte、char、short、int、float、reference或returnAddress的數據。兩個局部變量能夠保存一個類型爲long或double的數據。

5.二、操做數棧

每一個棧幀內部都包含一個成爲操做數棧的後進先出(LIFO)棧。棧幀中操做數棧的最大深度由編譯期決定。

棧幀在剛剛建立時,操做數棧是空的。操做數棧在每個位置上能夠保存一個JVM中定義的任意數據類型的值,包括long和double數據,在操做數棧中的數據必須正確地操做。在任什麼時候刻,操做數棧都會有一個棧深度,一個long或者double類型的數據會佔用兩個單位的棧深度,其餘數據類型則佔用一個。

5.三、動態鏈接

每一個棧幀內部都包含一個指向當前方法所在類型的運行時常量池引用,以便對當前方法的代碼實現動態鏈接。在class文件裏,一個方法若要調用其餘方法,或者訪問成員變量,則須要經過符號引用來表示,動態鏈接的做用就是將這些以符號引用所表示的方法轉換爲對實際方法的直接引用。

因爲對其餘類型中的方法和變量進行了晚期綁定(late binding),因此即使那些類發生變化,也不會影響調用他們的方法。

5.四、方法調用正常完成

方法調用正常完成是指在方法的執行過程當中,沒有拋出任何異常——包括直接從JVM中拋出的異常以及在執行時經過throw語句顯示拋出的異常。若是當前方法調用正常完成,它極可能會返回一個值給調用它的方法。

在這種場景下,當前棧幀承擔着恢復調用者狀態的責任,包括恢復調用者的局部變量表和操做數棧,以及正確遞增程序計數器,以跳過剛纔執行的方法調用指令等。

5.五、方法調用異常完成

方法調用異常完成是指在方法的執行過程當中,某些指令致使了JVM會拋出異常,而且JVM拋出的異常在該方法中沒有辦法處理,或者在執行過程當中遇到athrow字節碼指令並顯示拋出異常,同時在該方法內部沒有捕獲異常。若是方法是異常調用完成的,那必定不會有方法返回值返回給其調用者。

(一)學習JVM ——運行時數據區域

(二)學習JVM —— 垃圾回收機制

(三)學習JVM —— 垃圾回收器

(四)學習JVM —— 內存分配與回收策略

相關文章
相關標籤/搜索