生活賦予我們一種巨大的和無限高貴的禮品履肃,這就是青春:充滿著力量啄骇,充滿著期待志愿突委,充滿著求知和斗爭的志向组力,充滿著希望信心和青春省容。 —— 奧斯特洛夫斯基
JVM內(nèi)存簡介
Java虛擬機在執(zhí)行Java程序的過程中會把它管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。這些區(qū)域都有各自的用途燎字,以及創(chuàng)建和銷毀的時間腥椒。Java虛擬機所管理的內(nèi)存區(qū)域包括以下幾個運行時數(shù)據(jù)區(qū)域,如下圖所示:
程序計數(shù)器
程序計數(shù)器是一塊較小的內(nèi)存空間候衍,可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器笼蛛。分支、循環(huán)蛉鹿、跳轉(zhuǎn)滨砍、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成妖异。
??在多線程環(huán)境中惋戏,每個線程都有一個獨立的程序計數(shù)器,各線程之間的計數(shù)器互不影響他膳,獨立存儲响逢,因此程序計數(shù)器是線程私有的。
??如果線程正在執(zhí)行的是一個Java方法矩乐,計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址龄句,如果執(zhí)行的是Native方法,這個計數(shù)器則為空(Undefined)散罕。
Java虛擬機棧
Java虛擬機棧也是線程私有的分歇,它和Java線程在同一時間創(chuàng)建,它保持方法的局部變量欧漱、部分結(jié)果职抡,并參與方法的調(diào)用和返回。
??Java虛擬機棧規(guī)范允許Java棧的大小是動態(tài)是動態(tài)或者固定的误甚。在Java虛擬機棧規(guī)范中缚甩,定義了兩種異常與椘拙唬空間有關(guān):
StackOverflowError和OutOfMemoryError。如果線程請求的棧深度大于虛擬機所允許的深度擅威,將拋出StackOverflowError異常壕探,如果虛擬機動態(tài)擴展時無法申請到足夠內(nèi)存時,將拋出OutOfMemoryError異常郊丛。
??在Hotspot虛擬機中李请,可以使用-Xss參數(shù)來設(shè)置棧的大小。棧的大小直接決定了函數(shù)調(diào)用的可達深度厉熟。
??以下示例展示了棧的溢出导盅。
public class StackTest {
private int count = 0;
public void recursion() {
count++;
recursion();
}
@Test
public void testStack() {
try {
recursion();
} catch (Throwable e) {
System.out.println("deep of stack is " + count);
e.printStackTrace();
}
}
}
默認情況下,程序輸出結(jié)果:
deep of stack is 18904
java.lang.StackOverflowError
使用參數(shù)-Xss2M再次執(zhí)行程序揍瑟,程序輸出結(jié)果:
deep of stack is 42442
java.lang.StackOverflowError
很明顯白翻,棧的內(nèi)存增大后,程序支持的函數(shù)調(diào)用深度也同時增大绢片。
public class StackTest {
private int count = 0;
public void recursion(long a, long b, long c, long d) {
long e = 0, f = 0, g = 0;
count++;
recursion(a, b, c, d);
}
@Test
public void testStack() {
try {
recursion(1L, 2L, 3L, 4L);
} catch (Throwable e) {
System.out.println("deep of stack is " + count);
e.printStackTrace();
}
}
}
同樣使用參數(shù)-Xss2M執(zhí)行程序滤馍,程序輸出結(jié)果:
deep of stack is 21055
java.lang.StackOverflowError
隨著參數(shù)和局部變量的增多,棧幀的空間也隨之增大杉畜。(函數(shù)調(diào)用次數(shù)由無參時的42442降至21055)纪蜒。
虛擬機棧在運行時使用棧幀的數(shù)據(jù)結(jié)構(gòu)保存上下文數(shù)據(jù)。在棧幀中此叠,存放了方法的局部變量表、操作數(shù)棧随珠、動態(tài)連接方法和返回地址等信息灭袁。每一個方法的調(diào)用都伴隨著棧幀的入棧操作,相應(yīng)地窗看,方法的返回則表示棧幀的出棧操作茸歧。方法調(diào)用時,方法參數(shù)和局部變量相對較多显沈,那么局部變量表會比較大软瞎,棧幀會膨脹以滿足需求,因此單個方法調(diào)用所需的椑叮空間大小也會比較多涤浇。
棧幀結(jié)構(gòu)圖如下:
注意:對一個函數(shù)而言,它的參數(shù)越多魔慷,內(nèi)部局部變量越多只锭,它的棧幀就越大,其可達深度就越低院尔。
局部變量表
??用于存放方法參數(shù)和方法內(nèi)部定義的局部變量蜻展,其大小在代碼編譯期間已經(jīng)確定喉誊,在方法運行期間不會改變。局部變量表以變量槽(Slot)為最小存儲單位纵顾,每個Slot能夠存放一個boolean伍茄、byte、char施逾、shot幻林、int、float音念、reference和returnAddress類型的32位數(shù)據(jù)沪饺,對于64位的數(shù)據(jù)類型long和double,虛擬機會以高位對齊的方式為其分配兩個連續(xù)的Slot空間闷愤。
??在方法執(zhí)行時整葡,如果是實例方法,即非static方法讥脐,局部變量表中第0位Slot默認存放對象實例的引用(虛擬機通過局部變量表將當前對象傳遞給當前方法)遭居,方法中可以通過關(guān)鍵字 this 進行訪問,方法參數(shù)按照參數(shù)列表順序旬渠,從第1位Slot開始分配俱萍,方法內(nèi)部變量則按照定義順序進行分配其余的Slot。操作數(shù)棧
??操作數(shù)棧是一個基本的棧告丢,那么它自然也遵守棧的后入先出的原則枪蘑。其次,它里面主要存放的是一些算數(shù)運算用到的參數(shù)也可能是中間結(jié)果岖免,也可能是在調(diào)用其他方法時需要用到的參數(shù)岳颇。通過這點可以看出,方法剛剛開始執(zhí)行的時候颅湘,這個里面是空的话侧。最后 要說明的是操作數(shù)棧中可以存放任意的Java數(shù)據(jù)類型,包括long和double,且32位的數(shù)據(jù)類型占一個棿巢危空間,64位的數(shù)據(jù)類型占2個椪芭簦空間。動態(tài)連接
??在說明什么是動態(tài)連接之前先看看方法的大概調(diào)用過程鹿寨。首先新博,在虛擬機運行的時候,運行時常量池會保存大量的符號引用释移,這些符號引用可以看成是每個方法的間接引用叭披。如果代表棧幀A的方法想調(diào)用代表棧幀B的方法,那么這個虛擬機的方法調(diào)用指令就會以B方法的符號引用作為參數(shù),但是因為符號引用并不是直接指向代表B方法的內(nèi)存位置涩蜘,所以在調(diào)用之前還必須要將符號引用轉(zhuǎn)換為直接引用嚼贡,然后通過直接引用才可以訪問到真正的方法。這時候就有一點需要注意同诫,如果符號引用是在類加載階段或者第一次使用的時候轉(zhuǎn)化為直接應(yīng)用粤策,那么這種轉(zhuǎn)換成為靜態(tài)解析,如果是在運行期間轉(zhuǎn)換為直接引用误窖,那么這種轉(zhuǎn)換就成為動態(tài)連接叮盘。
-
返回地址
??方法的返回分為兩種情況,一種是正常退出霹俺,退出后會根據(jù)方法的定義來決定是否要傳返回值給上層的調(diào)用者柔吼,一種是異常導(dǎo)致的方法結(jié)束,這種情況是不會傳返回值給上層的調(diào)用方法丙唧。
??不過無論是那種方式的方法結(jié)束愈魏,在退出當前方法時都會跳轉(zhuǎn)到當前方法被調(diào)用的位置,如果方法是正常退出的想际,則調(diào)用者的PC計數(shù)器的值就可以作為返回地址培漏,如果是因為異常退出的,則是需要通過異常處理表來確定胡本。
本地方法棧
本地方法棧和虛擬機棧類似牌柄,兩者之間的區(qū)別是本地方法棧為Native方法服務(wù)。
Java堆
Java堆是Java運行時內(nèi)存中最為重要的部分侧甫,幾乎所有的對象實例以及數(shù)組都都是在堆中分配空間的珊佣。Java堆是所有線程共享的內(nèi)存區(qū)域。
Java堆分為新生代和老年代兩部分闺骚,新生代用于存放剛剛產(chǎn)生的對象和年輕的對象彩扔,(大對象除外,直接進入老年代僻爽,因為大對象占用空間多,為了有足夠空間容納大對象贾惦,JVM不得不移動大量新生代中的年輕對象至老年代胸梆,這對GC來說是不利的,另外须板,若是由于內(nèi)存空間緊張碰镜,JVM很可能不得不將部分年輕對象提前向老年代壓縮
),如果對象經(jīng)歷過N(該次數(shù)可通過參數(shù)配置习瑰,默認是15
)次GC而未被回收绪颖,則會被移入老年代。
??新生代又可進一步分為eden甜奄、from space(s0)柠横、to space(s1)(默認eden:s0:s1=8:1:1窃款,該比例可配置
)。eden,即對象的出生地牍氛,大部分對象剛建立時晨继,都會存放在這里。s0和s1為survivor空間搬俊,直譯為幸存者紊扬,也就是說存放在其中的對象,至少經(jīng)歷了一次垃圾回收唉擂,并得以幸存餐屎,如果在幸存區(qū)的對象到了指定年齡仍未被回收,則有機會進入老年代玩祟。
方法區(qū)
方法區(qū)(又稱永久代)和Java堆一樣腹缩,是所有線程共享的內(nèi)存區(qū)域。方法區(qū)主要保存的是類的元數(shù)據(jù)卵凑。
??方法區(qū)中最為重要的是類的類型信息庆聘、常量池、域信息勺卢、方法信息伙判。類型信息包括類的完整名稱、父類的完整名稱黑忱、類型修飾符(public/protected/private)和類型的直接接口類表宴抚。常量池包括這個類的方法、域等信息所引用的常量甫煞。域信息包括域名稱菇曲、域類型和域修飾符。方法信息包括方法名稱抚吠、返回類型常潮、方法參數(shù)、方法修飾符楷力、方法字節(jié)碼喊式、操作數(shù)棧和方法棧幀的局部變量區(qū)大小以及異常表∠舫總之岔留,方法區(qū)內(nèi)保存的信息,大部分是來自于class文件检柬。
??運行時常量池用于存放編譯期間生成的各種字面常量(文本字符串献联、聲明為final的常量值)和符號引用(類和接口的完全限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符)里逆。在JDK1.6中进胯,常量池是方法區(qū)的一部分,在JDK1.7中运悲,常量池存放在堆內(nèi)存里龄减,在JDK1.8中,常量池存放在MetaSpace里班眯。
??目前1.8的HotSpot中希停,已經(jīng)將方法區(qū)移除,取而代之的是MetaSpace署隘。
??當方法區(qū)無法滿足內(nèi)存分配需求時宠能,將拋出OutOfMemoryError異常。
??在HotSpot虛擬機中磁餐,在永久區(qū)中的對象违崇,同樣也是可以被回收的。對永久區(qū)GC的回收诊霹,主要從以下兩個方面分析:一是GC對永久區(qū)的常量池的回收羞延,二是永久區(qū)對類元數(shù)據(jù)的回收。
- 常量池的回收
??只要常量池中的常量沒有被任何地方引用脾还,就可以被回收伴箩。 - 類元數(shù)據(jù)的回收
??所有該類的實例被回收,且裝載該類的ClassLoader被回收鄙漏。
JVM內(nèi)存分配參數(shù)
- -Xms:設(shè)置堆的初始大小嗤谚。
- -Xmx:設(shè)置堆的最大值。
- -Xss:設(shè)置線程棧的大小怔蚌。
- -XX:MinHeapFreeRatio:設(shè)置堆空間最小空閑比例巩步。當堆空間的空閑內(nèi)存小于這個值時,便會擴展堆空間桦踊。
- -XX:MaxHeapFreeRatio:設(shè)置堆空間最大空閑比例椅野。當堆空間的空閑內(nèi)存大于這個值時,便會壓縮堆空間籍胯,得到一個較小的堆鳄橘。
- -XX:NewSize:設(shè)置新生代大小。
- -XX:NewRatio:設(shè)置老年代與新生代的比例芒炼,它等于老年代大小除以新生代大小。
- -XX:SurvivorRatio:設(shè)置新生代中Eden與survivor區(qū)的比例术徊。
- -XX:MaxPerPermSize:設(shè)置最大的持久區(qū)大小本刽。
- -XX:PerPermSize:設(shè)置持久區(qū)的初始大小。
- -XX:TargetSurvivorRatio:設(shè)置survivor區(qū)的可使用率。當survivor區(qū)的空間使用率達到這個值時子寓,會將對象送入老年代暗挑。