JVM全稱 Java Virtual Machine ,即Java虛擬機(jī),是用于運(yùn)行Java程序編譯后的字節(jié)碼文件(.class)。
我們知道,Java的口號(hào)是: “Write once, run anywhere”,即一次編寫,到處運(yùn)行。為什么可以做到這樣呢,其實(shí)就是依賴于JVM。

在不同的操作系統(tǒng)上,只要安裝了對(duì)應(yīng)的虛擬機(jī),那么同樣的一份代碼,就可以隨意移植。
當(dāng)編寫完Java代碼時(shí),即產(chǎn)生 .Java文件,會(huì)通過(guò)Java編譯器編譯為.class 文件,然后通過(guò)Class Loader把類信息加載到JVM中,最后JVM再去調(diào)用操作系統(tǒng)。這樣,只要JVM正確執(zhí)行.class文件,就可以實(shí)現(xiàn)跨平臺(tái)了。
1.JVM的內(nèi)存模型圖
在jvm1.8之前,jvm的邏輯結(jié)構(gòu)和物理結(jié)構(gòu)是對(duì)應(yīng)的。即Jvm在初始化的時(shí)候,會(huì)為堆(heap),棧(stack),元數(shù)據(jù)區(qū)(matespace)分配指定的內(nèi)存大小,Jvm線程啟動(dòng)的時(shí)候會(huì)向服務(wù)器申請(qǐng)指定的內(nèi)存地址空間進(jìn)行分配。在jdk1.8之后,使用了G1垃圾回收器,邏輯上依然存在堆,棧,元數(shù)據(jù)區(qū)。但是在物理結(jié)構(gòu)上,G1采用了分區(qū)(Region)的思路,將整個(gè)堆空間分成若干個(gè)大小相等的內(nèi)存區(qū)域,每次分配對(duì)象空間將逐段地使用內(nèi)存。因此,在堆的使用上,G1并不要求對(duì)象的存儲(chǔ)一定是物理上連續(xù)的,只要邏輯上連續(xù)即可;每個(gè)分區(qū)也不會(huì)確定地為某個(gè)代服務(wù),可以按需在年輕代和老年代之間切換。啟動(dòng)時(shí)可以通過(guò)參數(shù)-XX:G1HeapRegionSize=n可指定分區(qū)大小(1MB~32MB,且必須是2的冪),默認(rèn)將整堆劃分為2048個(gè)分區(qū)。

2.棧(虛擬機(jī)棧)

圖片來(lái)源:http://www.th7.cn/Program/java/201601/749326.shtml
虛擬機(jī)棧(Java Virtual Machine Stacks)是線程隔離的,每創(chuàng)建一個(gè)線程時(shí)就會(huì)對(duì)應(yīng)創(chuàng)建一個(gè)Java棧,即每個(gè)線程都有自己獨(dú)立的虛擬機(jī)棧。這個(gè)棧中又會(huì)對(duì)應(yīng)包含多個(gè)棧幀,每調(diào)用一個(gè)方法時(shí)就會(huì)往棧中創(chuàng)建并壓入一個(gè)棧幀,棧幀存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息,每一個(gè)方法從調(diào)用到最終返回結(jié)果的過(guò)程,就對(duì)應(yīng)一個(gè)棧幀從入棧到出棧的過(guò)程。
當(dāng)函數(shù)執(zhí)行結(jié)束返回時(shí),棧幀從Java棧中被彈出。Java方法有兩種返回的方式,一種是正常函數(shù)返回,即使用 return; 另外一種是拋出異常。不管哪種方式,都會(huì)導(dǎo)致棧幀被彈出。
因此虛擬機(jī)棧不會(huì)產(chǎn)生垃圾。
虛擬機(jī)棧與數(shù)據(jù)結(jié)構(gòu)上的棧有著類似的含義,它是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),線程運(yùn)行過(guò)程中,只有處于棧頂?shù)臈攀怯行У?,稱為當(dāng)前棧幀,與這個(gè)棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法,當(dāng)前活動(dòng)幀棧始終是虛擬機(jī)棧的棧頂元素。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型和對(duì)象引用類型。通常我們所說(shuō)的“棧內(nèi)存”指的就是局部變量表這一部分。
局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀分配多少內(nèi)存是固定的,運(yùn)行期間不會(huì)改變局部變量表的大小。
64位的long和double類型的數(shù)據(jù)會(huì)占用2個(gè)局部變量空間,其余的數(shù)據(jù)類型只占用1個(gè)。
棧的大小可以固定也可以動(dòng)態(tài)擴(kuò)展。
在固定大小的情況下,JVM會(huì)為每個(gè)線程的虛擬機(jī)棧分配一定的內(nèi)存大?。?Xss參數(shù)),因此虛擬機(jī)棧能夠容納的棧幀數(shù)量是有限的,若棧幀不斷進(jìn)棧而不出棧,最終會(huì)導(dǎo)致當(dāng)前線程虛擬機(jī)棧的內(nèi)存空間耗盡,會(huì)拋出StackOverflowError異常。
在動(dòng)態(tài)擴(kuò)展的情況下,當(dāng)整個(gè)虛擬機(jī)棧內(nèi)存耗盡,并且無(wú)法再申請(qǐng)到新的內(nèi)存時(shí),就會(huì)拋出OutOfMemoryError異常。
棧溢出代碼:
public class StackOverFlow {
public static void main(String[] args) {
new StackOverFlow().test();
}
private void test() {
System.out.println("run...");
test();
}
}
2.1、 局部變量表
局部變量表示棧幀的重要組成部分之一。它用于保存函數(shù)已經(jīng)局部變量。局部變量表中的變量只有在當(dāng)前的函數(shù)中調(diào)用有效,當(dāng)調(diào)用函數(shù)結(jié)束以后,隨著函數(shù)棧幀的銷毀,局部變量表也隨之銷毀。
2.2、 操作數(shù)棧
操作數(shù)棧也是棧幀中重要的內(nèi)容之一,它主要保存計(jì)算過(guò)程中的結(jié)果,同事作為計(jì)算過(guò)程臨時(shí)變量的存儲(chǔ)空間。
操作數(shù)棧也是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),只支持入棧和出棧的兩種操作,Java的很多字節(jié)碼指令都是通過(guò)操作數(shù)棧進(jìn)行參數(shù)傳遞的。比如iadd指令,它就會(huì)在操作數(shù)棧中彈出兩個(gè)整數(shù)進(jìn)行加法計(jì)算,計(jì)算結(jié)果會(huì)被入棧。入下圖所示:

2.3、 幀數(shù)據(jù)區(qū)
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬性方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接。在Class文件的常量池中存有大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另外一部分將在每一次的運(yùn)行期期間轉(zhuǎn)化為直接引用,這部分稱為動(dòng)態(tài)連接
3.本地方法棧
本地方法棧是虛擬機(jī)使用到的native方法服務(wù),因?yàn)镴ava無(wú)法直接操作系統(tǒng),底層調(diào)用的c或者c++,我們打開jdk安裝目錄可以看到也有很多用c編寫的文件,可能就是native方法所調(diào)用的c代碼。
本地方法棧的功能和特點(diǎn)類似于虛擬機(jī)棧,均具有線程隔離的特點(diǎn)以及都能拋出StackOverflowError和OutOfMemoryError異常。
不同的是,本地方法棧服務(wù)的對(duì)象是JVM執(zhí)行的native方法,而虛擬機(jī)棧服務(wù)的是JVM執(zhí)行的java方法。
HotSpot虛擬機(jī)不區(qū)分虛擬機(jī)棧和本地方法棧,兩者是一塊的。
4.程序計(jì)數(shù)器

程序計(jì)數(shù)器(Program Counter Register)是JVM中一塊較小的內(nèi)存區(qū)域,保存著當(dāng)前線程執(zhí)行的虛擬機(jī)字節(jié)碼指令的內(nèi)存地址(可以看作當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器)。
如果線程執(zhí)行的是java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址(可以理解為上圖所示的行號(hào)),如果正在執(zhí)行的是native方法,這個(gè)計(jì)數(shù)器的值為undefined。
此區(qū)域是唯一一個(gè)在java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域,因?yàn)槌绦蛴?jì)數(shù)器是由虛擬機(jī)內(nèi)部維護(hù)的,不需要開發(fā)者進(jìn)行操作。
程序計(jì)數(shù)器作用
JVM的多線程是通過(guò)線程輪流切換并分配CPU執(zhí)行時(shí)間片的方式來(lái)實(shí)現(xiàn)的,任何一個(gè)時(shí)刻,一個(gè)CPU都只會(huì)執(zhí)行一條線程中的指令。為了保證線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各線程間的程序計(jì)數(shù)器獨(dú)立存儲(chǔ),互不影響。
5.方法區(qū)(靜態(tài)區(qū))
方法去在JVM中是一個(gè)非常重要的區(qū)域,就像堆一樣,是被線程共享的區(qū)域。在方法區(qū)中,存儲(chǔ)了每個(gè)類的信息(包括類的名稱,方法信息,字段信息),靜態(tài)變量,常量以及編譯器編譯后的代碼等。

Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫做 Non-Heap(非堆),目的應(yīng)該是與 Java 堆區(qū)分開來(lái)。
JDK7 之前(永久代)用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、字符串常量、類靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
運(yùn)行時(shí)常量池(Runtime Constant Pool)
Class 文件中除了有類的版本/字段/方法/接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將類在加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。運(yùn)行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的是 String.intern() 方法。受方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常。

HotSpot中方法區(qū)的演進(jìn)
在jdk7及以前,習(xí)慣上把方法區(qū),稱為永久代。jdk8開始,使用元空間取代了永久代。

而到了JDK 8,終于完放棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Metaspace)來(lái)代替

元空間的本質(zhì)和永久代類似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代最大的區(qū)別在于:元空間不在虛擬機(jī)設(shè)置的內(nèi)存中、而是使用本地內(nèi)存。
永久代、元空間二者并不只是名字變了,內(nèi)存結(jié)構(gòu)也調(diào)整了。
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,如果方法區(qū)無(wú)法滿足新的內(nèi)存分配需求時(shí),將拋出OOM異常。
6.堆
Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。
此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存,每個(gè)對(duì)象都包含一個(gè)與之對(duì)應(yīng)的class的信息(class信息存放在方法區(qū))。
Java堆是垃圾收集器管理的主要區(qū)域,稱為"GC堆"
如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError異常。
堆區(qū)是JVM中最大一塊內(nèi)存區(qū)域,存儲(chǔ)著各類生成的對(duì)象、數(shù)組等,所有通過(guò)new創(chuàng)建的對(duì)象的內(nèi)存都在堆中分配,JVM8中把運(yùn)行時(shí)常量池、靜態(tài)變量也移到堆區(qū)進(jìn)行存儲(chǔ)。堆區(qū)被細(xì)化可以分為年輕代、老年代,其實(shí)不分代完全可以,分代的唯一理由就是優(yōu)化GC性能
堆內(nèi)存的劃分

Java虛擬機(jī)將堆內(nèi)存劃分為新生代、老年代和永久代,永久代是HotSpot虛擬機(jī)特有的概念(JDK1.8之后為metaspace替代永久代,并且改為存放在本地內(nèi)存中),它采用永久代的方式來(lái)實(shí)現(xiàn)方法區(qū),其他的虛擬機(jī)實(shí)現(xiàn)沒有這一概念,而且HotSpot也有取消永久代的趨勢(shì),在JDK 1.7中HotSpot已經(jīng)開始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、類信息、靜態(tài)變量等數(shù)據(jù),與垃圾回收關(guān)系不大,新生代和老年代是垃圾回收的主要區(qū)域。
新生代(Young Generation)
新生成的對(duì)象優(yōu)先存放在新生代中,新生代對(duì)象朝生夕死,存活率很低,在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70% ~ 95% 的空間,回收效率很高。
HotSpot將新生代劃分為三塊,一塊較大的Eden(伊甸)空間和兩塊較小的Survivor(幸存者)空間,默認(rèn)比例為8:1:1。劃分的目的是因?yàn)镠otSpot采用復(fù)制算法來(lái)回收新生代,設(shè)置這個(gè)比例是為了充分利用內(nèi)存空間,減少浪費(fèi)。新生成的對(duì)象在Eden區(qū)分配(大對(duì)象除外,大對(duì)象直接進(jìn)入老年代),當(dāng)Eden區(qū)沒有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。
GC開始時(shí),對(duì)象只會(huì)存在于Eden區(qū)和From Survivor區(qū),To Survivor區(qū)是空的(作為保留區(qū)域)。GC進(jìn)行時(shí),Eden區(qū)中所有存活的對(duì)象都會(huì)被復(fù)制到To Survivor區(qū),而在From Survivor區(qū)中,仍存活的對(duì)象會(huì)根據(jù)它們的年齡值決定去向,年齡值達(dá)到年齡閥值(默認(rèn)為15,新生代中的對(duì)象每熬過(guò)一輪垃圾回收,年齡值就加1,GC分代年齡存儲(chǔ)在對(duì)象的header中)的對(duì)象會(huì)被移到老年代中,沒有達(dá)到閥值的對(duì)象會(huì)被復(fù)制到To Survivor區(qū)。接著清空Eden區(qū)和From Survivor區(qū),新生代中存活的對(duì)象都在To Survivor區(qū)。接著, From Survivor區(qū)和To Survivor區(qū)會(huì)交換它們的角色,也就是新的To Survivor區(qū)就是上次GC清空的From Survivor區(qū),新的From Survivor區(qū)就是上次GC的To Survivor區(qū),總之,不管怎樣都會(huì)保證To Survivor區(qū)在一輪GC后是空的。GC時(shí)當(dāng)To Survivor區(qū)沒有足夠的空間存放上一次新生代收集下來(lái)的存活對(duì)象時(shí),需要依賴?yán)夏甏M(jìn)行分配擔(dān)保,將這些對(duì)象存放在老年代中。
老年代(Old Generationn)
在新生代中經(jīng)歷了多次(具體看虛擬機(jī)配置的閥值)GC后仍然存活下來(lái)的對(duì)象會(huì)進(jìn)入老年代中。老年代中的對(duì)象生命周期較長(zhǎng),存活率比較高,在老年代中進(jìn)行GC的頻率相對(duì)而言較低,而且回收的速度也比較慢。
永久代(Permanent Generationn)
永久代存儲(chǔ)類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù),對(duì)這一區(qū)域而言,Java虛擬機(jī)規(guī)范指出可以不進(jìn)行垃圾收集,一般而言不會(huì)進(jìn)行垃圾回收。
常用設(shè)置參數(shù)說(shuō)明:

-Xms256M:初始堆大小256M,默認(rèn)為物理內(nèi)存的1/64
-Xmx1024M:最大堆大小1024M,默認(rèn)為物理內(nèi)存的1/4,等于與-XX:MaxHeapSize=64M
-Xmn64M:年輕代大小為64M(JDK1.4后支持),相當(dāng)于同時(shí)設(shè)置NewSize和MaxNewSize為64M
-XX:NewSize=64M:初始年輕代大小
-XX:MaxNewSize=256M:最大年輕代大?。J(rèn)為堆最大值的1/3)
-XX:OldSize=64M:年老代大小64M(測(cè)試驗(yàn)證JDK1.8.191該參數(shù)設(shè)置無(wú)效,JDK11下設(shè)置成功)
-XX:NewRatio=4:年老代:年輕代=4:1,默認(rèn)值2
-XX:SurvivorRatio=8:年輕代中,2個(gè)Survivor區(qū)與1個(gè)Eden區(qū)比例=2:8,Survivor占新生代內(nèi)存比例為1/5,默認(rèn)值8
-XX:MaxHeapFreeRatio=70:堆內(nèi)存使用率大于70時(shí)擴(kuò)張堆內(nèi)存,xms=xmx時(shí)該參數(shù)無(wú)效,默認(rèn)值70
-XX:MinHeapFreeRatio=40:堆內(nèi)存使用率小于40時(shí)縮減堆內(nèi)存,xms=xmx時(shí)該參數(shù)無(wú)效,默認(rèn)值40-Xms256M:初始堆大小256M,默認(rèn)為物理內(nèi)存的1/64
-Xmx1024M:最大堆大小1024M,默認(rèn)為物理內(nèi)存的1/4,等于與-XX:MaxHeapSize=64M
-Xmn64M:年輕代大小為64M(JDK1.4后支持),相當(dāng)于同時(shí)設(shè)置NewSize和MaxNewSize為64M
-XX:NewSize=64M:初始年輕代大小
-XX:MaxNewSize=256M:最大年輕代大小(默認(rèn)為堆最大值的1/3)
-XX:OldSize=64M:年老代大小64M(測(cè)試驗(yàn)證JDK1.8.191該參數(shù)設(shè)置無(wú)效,JDK11下設(shè)置成功)
-XX:NewRatio=4:年老代:年輕代=4:1,默認(rèn)值2
-XX:SurvivorRatio=8:年輕代中,2個(gè)Survivor區(qū)與1個(gè)Eden區(qū)比例=2:8,Survivor占新生代內(nèi)存比例為1/5,默認(rèn)值8
-XX:MaxHeapFreeRatio=70:堆內(nèi)存使用率大于70時(shí)擴(kuò)張堆內(nèi)存,xms=xmx時(shí)該參數(shù)無(wú)效,默認(rèn)值70
-XX:MinHeapFreeRatio=40:堆內(nèi)存使用率小于40時(shí)縮減堆內(nèi)存,xms=xmx時(shí)該參數(shù)無(wú)效,默認(rèn)值40
JVM 參數(shù)設(shè)置大全:http://www.51gjie.com/java/551.html