JVM面試總結(jié)(一)
? ???JVM是面試必問的模塊,整個JVM我個人感覺可以分為內(nèi)存模型、類加載機制、GC垃圾回收和性能優(yōu)化四個大塊;
1、Java 是如何實現(xiàn)跨平臺的?
????我們寫的 Java 源碼,編譯后會生成一種 .class 文件,稱為字節(jié)碼文件。Java 虛擬機(JVM)就是負責(zé)將字節(jié)碼文件翻譯成特定平臺下的機器碼然后運行,也就是說,只要在不同平臺上安裝對應(yīng)的 JVM,就可以運行字節(jié)碼文件,運行我們編寫的 Java 程序。
而這個過程,我們編寫的 Java 程序沒有做任何改變,僅僅是通過 JVM 這一 “中間層” ,就能在不同平臺上運行,真正實現(xiàn)了 “一次編譯,到處運行” 的目的。
接下來就會問JVM是什么,由什么組成,怎么運行的?
2、JVM是什么?(談?wù)勀銓VM的理解)
????JVM,即 Java Virtual Machine,Java 虛擬機。它通過模擬一個計算機來達到一個計算機所具有的的計算功能。JVM 能夠跨計算機體系結(jié)構(gòu)來執(zhí)行 Java 字節(jié)碼,主要是由于 JVM 屏蔽了與各個計算機平臺相關(guān)的軟件或者硬件之間的差異,使得與平臺相關(guān)的耦合統(tǒng)一由 JVM 提供者來實現(xiàn)。
3、為什么可以跨平臺?
????1、Java 文件經(jīng)過編譯后生成和平臺無關(guān)的. class 文件。
? ?2、Java虛擬機(JVM)是不跨平臺的,Java工具會把統(tǒng)一的.class文件,加載到對應(yīng)的JVM,又因為該JVM是和這個系統(tǒng)是對應(yīng)的,所以就可以運行。
4、JVM由什么組成的?
JVM 主要由四大部分組成:ClassLoader(類加載器),Runtime Data Area(運行時數(shù)據(jù)區(qū)或者內(nèi)存分區(qū)),Execution Engine(執(zhí)行引擎),Native Interface(本地庫接口);

1、ClassLoader(類加載器):負責(zé)加載字節(jié)碼文件即 class 文件,class 文件在文件開頭有特定的文件標示,并且 ClassLoader 只負責(zé)class 文件的加載,至于它是否可以運行,則由 Execution Engine 決定。
2、Runtime Data Area(運行時數(shù)據(jù)區(qū)或者內(nèi)存分區(qū)):是存放數(shù)據(jù)的,分為五部分:Stack(虛擬機棧),Heap(堆),Method Area(方法區(qū)),PC Register(程序計數(shù)器),Native Method Stack(本地方法棧)。幾乎所有的關(guān)于 Java 內(nèi)存方面的問題,都是集中在這塊。
3、Execution Engine(執(zhí)行引擎):也叫 Interpreter;Class 文件被加載后,會把指令和數(shù)據(jù)信息放入內(nèi)存中,Execution Engine 則負責(zé)把這些命令解釋給操作系統(tǒng),即將 JVM 指令集翻譯為操作系統(tǒng)指令集。
4、Native Interface(本地庫接口):負責(zé)調(diào)用本地接口的。他的作用是調(diào)用不同語言的接口給 JAVA 用,他會在 Native Method Stack 中記錄對應(yīng)的本地方法,然后調(diào)用該方法時就通過 Execution Engine 加載對應(yīng)的本地 lib。
運行時數(shù)據(jù)區(qū)按照Java虛擬機規(guī)定分為以下5個部分:
1、程序計數(shù)器:通過改變計數(shù)器的值,來選取下一條需要執(zhí)行的字節(jié)碼指令。
2、Java虛擬機棧:用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。
3、本地方法棧:為虛擬機調(diào)用Native方法服務(wù)的。
4、Java堆(線程共享):Java虛擬機中內(nèi)存最大的一塊,是被所有線程共享的,幾乎所有的對象實例都在這里分配內(nèi)存,也是垃圾收集器管理的主要區(qū)域,因此也被稱為GC堆。
5、方法區(qū)(線程共享):用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯后的代碼等數(shù)據(jù),存在一個叫運行時常量池的區(qū)域,用來存放編譯器生成的各種字面量和符號引用。
5、JVM怎么運行的?(JVM的工作流程)
JVM的啟動過程分為如下四個步驟:
1、JVM的裝入環(huán)境和配置
java.exe負責(zé)查找JRE,并且它會按照如下的順序來選擇JRE:
????自己目錄下的JRE;
????父級目錄下的JRE;
????查注冊中注冊的JRE。
2、裝載JVM
????通過第一步找到JVM的路徑后,Java.exe通過LoadJavaVM來裝入JVM文件。LoadLibrary裝載JVM動態(tài)連接庫,然后把JVM中的到處函數(shù)JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 掛接到InvocationFunction 變量的CreateJavaVM和GetDafaultJavaVMInitArgs 函數(shù)指針變量上。JVM的裝載工作完成。
3、初始化JVM,獲得本地調(diào)用接口
????調(diào)用InvocationFunction -> CreateJavaVM,也就是JVM中JNI_CreateJavaVM方法獲得JNIEnv結(jié)構(gòu)的實例。
4、運行Java程序
????JVM運行Java程序的方式有兩種:jar包 與 class。
????運行jar 的時候,java.exe調(diào)用GetMainClassName函數(shù),該函數(shù)先獲得JNIEnv實例然后調(diào)用JarFileJNIEnv類中g(shù)etManifest(),從其返回的Manifest對象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主類名作為運行的主類。之后main函數(shù)會調(diào)用Java.c中LoadClass方法裝載該主類(使用JNIEnv實例的FindClass)。運行Class的時候,main函數(shù)直接調(diào)用Java.c中的LoadClass方法裝載該類。
6、Java程序是怎么運行的?
Java程序從源文件創(chuàng)建到程序運行要經(jīng)過兩大步驟:
????1、源文件由編譯器編譯成字節(jié)碼(ByteCode);
? ?2、字節(jié)碼由java虛擬機解釋運行。
第一步(編譯): 創(chuàng)建完源文件之后,程序會先被編譯為.class文件。Java編譯一個類時,如果這個類所依賴的類還沒有被編譯,編譯器就會先編譯這個被依賴的類,然后引用,否則直接引用。(如果java編譯器在指定目錄下找不到該類所其依賴的類的.class文件或者.java源文件的話,編譯器話報“cant find symbol”的錯誤。)
? ?編譯后的字節(jié)碼文件格式主要分為兩部分:常量池和方法字節(jié)碼。常量池記錄的是代碼出現(xiàn)過的所有token(類名,成員變量名等等)以及符號引用(方法引用,成員變量引用等等);方法字節(jié)碼放的是類中各個方法的字節(jié)碼。
第二步(運行):Java類運行的過程大概可分為兩個過程:1、類的加載 ?2、類的執(zhí)行。需要說明的是:JVM主要在程序第一次主動使用類的時候,才會去加載該類。也就是說,JVM并不是在一開始就把一個程序就所有的類都加載到內(nèi)存中,而是到不得不用的時候才把它加載進來,而且只加載一次。 ? ? ? ?下面是程序運行的詳細步驟:1、在編譯好Java程序得到MainApp.class文件后,在命令行上敲Java AppMain。系統(tǒng)就會啟動一個JVM進程,JVM進程從classpath路徑中找到一個名為MainApp.class的二進制文件,將MainApp的類信息加載到運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),這個過程叫做MainApp類的加載。2、然后JVM找到AppMain的主函數(shù)入口,開始執(zhí)行main函數(shù)。3、main函數(shù)的第一條命令是Animal ?animal = new Animal("Puppy");就是讓JVM創(chuàng)建一個Animal對象,但是這時候方法區(qū)中沒有Animal類的信息,所以JVM馬上加載Animal類,把Animal類的類型信息放到方法區(qū)中。4、加載完Animal類之后,Java虛擬機做的第一件事情就是在堆區(qū)中為一個新的Animal實例分配內(nèi)存, 然后調(diào)用構(gòu)造函數(shù)初始化Animal實例,這個Animal實例持有著指向方法區(qū)的Animal類的類型信息(其中包含有方法表,Java動態(tài)綁定的底層實現(xiàn))的引用。5、當(dāng)使用animal.printName()的時候,JVM根據(jù)animal引用找到Animal對象,然后根據(jù)Animal對象持有的引用定位到方法區(qū)中Animal類的類型信息的方法表,獲得printName()函數(shù)的字節(jié)碼的地址。6、開始運行printName()函數(shù)。
概況來說
????1、寫好的 Java 源代碼文件經(jīng)過 Java 編譯器編譯成字節(jié)碼文件;
????2、通過類加載器加載到內(nèi)存中,被實例化;
????4、然后到 Java 虛擬機中解釋執(zhí)行;
????5、最后通過操作系統(tǒng)操作 CPU 執(zhí)行獲取結(jié)果。
7、 JDK、JRE和JVM之間的關(guān)系
????JDK:Java Development Kit,Java 開發(fā)工具包 - 開發(fā)人員進行 Java 軟件開發(fā)測試的一套工具
????JRE:Java Runtime Environment,Java 運行時環(huán)境 - Java 軟件成品運行所依賴的環(huán)境
????JVM:Java Virtual Machine,Java 虛擬機 - Java 語言實現(xiàn)跨平臺運行的一種軟件
三者的關(guān)系是:JDK包含JRE,JRE包含JVM
8、JVM內(nèi)存分布
1、程序計數(shù)器
程序計數(shù)器:程序計數(shù)器就是臨時記錄方法運行到哪一行了,程序運行實際并不存在并行,而是不同的線程不斷的搶占cpu,然后執(zhí)行一段時間,又重新開始競爭,只是執(zhí)行時間太短,導(dǎo)致人根本感受不到,當(dāng)一個方法運行到某一行的時候開始重新競爭了,就需要記錄下當(dāng)前方法運行到哪了,用來下次cpu被該方法搶占時,從上次中斷的地方繼續(xù)執(zhí)行。
為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間計數(shù)器互不影響,獨立存儲,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存
2、Java虛擬機棧(Java棧)
線程私有,生命周期和線程,每個方法在執(zhí)行的同時都會創(chuàng)建一個 棧幀用于存儲局部變量表,操作數(shù)棧,動態(tài)鏈接,方法出口等信息。方法的執(zhí)行就對應(yīng)著棧幀在虛擬機棧中入棧和出棧的過程;棧里面存放著各種基本數(shù)據(jù)類型和對象的引用;
3、本地方法棧
Java 中有些代碼的實現(xiàn)是依賴于其他非 Java 語言的(C++),本地方法棧存儲的是維護非 Java 語句執(zhí)行過程中產(chǎn)生的數(shù)據(jù),一般我們認為本地方法棧不會出現(xiàn)內(nèi)存的問題;本地方法棧則是為虛擬機使用到的本地(Native)方法服務(wù)。也是線程私有的。
4、Java堆(堆)
方法區(qū)存放著class信息,而堆中存放了實例化的對象,同一個類的對象可以被實例化多次,對象是可以被其他線程使用的,所以堆也是共享數(shù)據(jù)區(qū)。實例化對象時,對象中有一個對象頭,其中有個類型指針會指向方法區(qū)的類元信息。
Java堆是程序員需要重點關(guān)注的一塊區(qū)域,因為涉及到內(nèi)存的分配(new關(guān)鍵字,反射等)與回收(回收算法,收集器等);
5、方法區(qū)
方法區(qū):也叫永久區(qū),用于存儲已經(jīng)被虛擬機加載的類信息,常量("zdy","123"等),靜態(tài)變量(static變量)等數(shù)據(jù)。 (jdk1.8已經(jīng)將方法區(qū)去掉了,將方法區(qū)移動到直接內(nèi)存)
在 Java8 之后,我們把方法區(qū)稱之為元空間(MetaSpace),方法區(qū)在邏輯上屬于堆
的一部分,但一些具體機制和堆有所區(qū)別,如:一些 JVM 的方法區(qū)是可以不進行垃圾回收
的,關(guān)閉 JVM 時才會釋放方法區(qū)內(nèi)存。所以方法區(qū)還有一個別名叫非堆,目的是和堆分開。
方法區(qū)會存儲類信息、靜態(tài)變量、常量(JDK8 之后不存放字符串常量)、本地機器指 令。
如果加載大量 class 文件,也會造成方法區(qū)內(nèi)存溢出,如一個 tomcat 運行 20~30 個 項目。
6、運行時常量池
運行時常量池是方法區(qū)的一部分,用于存放編譯期生成的各種字面("abc","123"等)和符號引用。
7、 直接內(nèi)存:
直接內(nèi)存:不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是java虛擬機規(guī)范中定義的內(nèi)存區(qū)域;
????1、如果使用了NIO,這塊區(qū)域會被頻繁使用,在Java堆內(nèi)可以用directByteBuffer對象直接引用并操作;
????2、這塊內(nèi)存不受Java堆大小限制,但受本機總內(nèi)存的限制,可以通過MaxDirectMemorySize來設(shè)置(默認與堆內(nèi)存最大值一樣),所以也會出現(xiàn)OOM異常;
9、擴展
一般重點會問你,類放在哪,常量,變量的位置,線程共享的內(nèi)存和獨有的內(nèi)存;
Java中new處的對象存放在Java堆中,而對象的引用存放在虛擬機棧中。
Java中的Class也是一個類,所以Class對象也存放在堆當(dāng)中,存放在方法區(qū)當(dāng)中的是類的元數(shù)據(jù),即類加載器從class文件中提取出來的類型信息、方法信息、字段信息等。
jdk1.7靜態(tài)變量存放在方法區(qū)中
jdk1.8以后類型信息,字段,方法,常量,保存在本地內(nèi)存的元空間(方法區(qū)),但字符串常量池,靜態(tài)變量仍在堆,new申請的內(nèi)存是在堆中。
1、堆和棧功能上的區(qū)別:
????以棧幀的方式存儲方法調(diào)用的過程,并存儲方法調(diào)用過程中基本數(shù)據(jù)類型的變量(int、short、long、byte、float、double、boolean、char等)以及對象的引用變量,其內(nèi)存分配在棧上,變量出了作用域就會自動釋放;
????而堆內(nèi)存用來存儲Java中的對象。無論是成員變量,局部變量,還是類變量,它們指向的對象都存儲在堆內(nèi)存中;
2、堆和棧在線程共享和線程私有區(qū)別
????棧內(nèi)存歸屬于單個線程,每個線程都會有一個棧內(nèi)存,其存儲的變量只能在其所屬線程中可見,即棧內(nèi)存可以理解成線程的私有內(nèi)存。
堆內(nèi)存中的對象對所有線程可見。堆內(nèi)存中的對象可以被所有線程訪問。
?????棧的內(nèi)存要遠遠小于堆內(nèi)存,棧的深度是有限制的,如果遞歸沒有及時跳出,很可能發(fā)生StackOverFlowError問題。
????可以通過-Xss選項設(shè)置棧內(nèi)存的大小( 這個參數(shù)是設(shè)定單個線程的??臻g)。-Xms選項可以設(shè)置堆的開始時的大小,-Xmx選項可以設(shè)置堆的最大值。
以上內(nèi)容僅供參考,請合理利用搜索引擎!