JVM相關面試題

一、什麼是JVM

JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。

Java語言的一個非常重要的特點就是與平臺的無關性。而使用Java虛擬機是實現這一特點的關鍵。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入Java語言虛擬機後,Java語言在不同平臺 上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。這就是Java的能夠“一次編譯,到處運行”的原因。


java平臺

java程序執行圖

JAVA代碼編譯和執行過程

class文件由以下部分組成:

結構信息:包括class文件格式版本號及各部分的數量與大小的信息

元數據:對應於Java源碼中聲明與常量的信息。包含類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池

方法信息:對應Java源碼中語句和表達式對應的信息。包含字節碼、異常處理器表、求值棧與局部變量區大小、求值棧的類型記錄、調試符號信息


類加載機制



1). Bootstrap ClassLoader

  負責加載$JAVA_HOME中jre/lib/rt.jar裡所有的class,由C++實現,不是ClassLoader子類

2). Extension ClassLoader

  負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包

3). App ClassLoader

  負責記載classpath中指定的jar包及目錄中class

4). Custom ClassLoader(自定義加載器)

  屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。

雙親委派模型

雙親委派模型要求除了頂層的啟動類加載器之外,其餘的類加載器都應當有自己的父類加載器。這裡的類加載器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是使用組合(Composition)關係來複用父加載器的代碼。

雙親委派模型類加載過程

1. 當前ClassLoader首先從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類。

每個類加載器都有自己的加載緩存,當一個類被加載了以後就會放入緩存,等下次加載的時候就可以直接返回了。

2. 當前classLoader的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用同樣的策略,首先查看自己的緩存,然後委託父類的父類去加載,一直到bootstrp ClassLoader.

3. 當所有的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。

作用

防止重複加載相同類名的類,保證系統庫的安全

解釋:這種設計有個好處是,如果有人想替換系統級別的類:String.java。篡改它的實現,但是在這種機制下這些系統的類已經被Bootstrap classLoader加載過了,所以並不會再去加載,從一定程度上防止了危險代碼的植入。

內存模型

程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,是線程私有的,它的作用可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裡(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

Java 虛擬機棧(Java Virtual Machine Stacks)是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java 方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame ①)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程

本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬棧為虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native 方法服務,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。

Java 堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裡分配內存。Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”(Garbage Collected Heap,幸好國內沒翻譯成“垃圾堆”)。如果從內存回收的角度看,由於現在收集器基本都是採用的分代收集算法,所以Java 堆中還可以細分為:新生代和老年代;再細緻一點的有Eden 空間、From Survivor 空間、To Survivor 空間等

方法區

方法區(Method Area)與Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java 虛擬機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java 堆區分開來。

垃圾回收

判斷對象是否存活一般有兩種方式:

l 引用計數:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決對象相互循環引用的問題。

l 可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的,不可達對象。

GC Root

常說的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的對象,GC會收集那些不是GC roots且沒有被GC roots引用的對象。

一個對象可以屬於多個root,GC root有幾下種:

l Class - 由系統類加載器(system class loader)加載的對象,這些類是不能夠被回收的,他們可以以靜態字段的方式保存持有其它對象。我們需要注意的一點就是,通過用戶自定義的類加載器加載的類,除非相應的java.lang.Class實例以其它的某種(或多種)方式成為roots,否則它們並不是roots,.

l Thread - 活著的線程

l Stack Local - Java方法的local變量或參數

l JNI Local - JNI方法的local變量或參數

l JNI Global - 全局JNI引用

l Monitor Used - 用於同步的監控對象

l Held by JVM - 用於JVM特殊目的由GC保留的對象,但實際上這個與JVM的實現是有關的。可能已知的一些類型是:系統類加載器、一些JVM知道的重要的異常類、一些用於處理異常的預分配對象以及一些自定義的類加載器等。然而,JVM並沒有為這些對象提供其它的信息,因此需要去確定哪些是屬於"JVM持有"的了。

垃圾回收算法

標記-清除(Mark-Sweep)

“標記-清除”算法,如它的名字一樣,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。

它的主要缺點有兩個:

(1)效率問題:標記和清除過程的效率都不高;

(2)空間問題:標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致,碎片過多會導致大對象無法分配到足夠的連續內存,從而不得不提前觸發GC,甚至Stop The World。


複製(Copying)算法

為解決效率問題,“複製”收集算法出現了。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。

它的主要缺點有兩個:

(1)效率問題:在對象存活率較高時,複製操作次數多,效率降低;

(2)空間問題:內存縮小了一半;需要額外空間做分配擔保(老年代)




標記-整理(Mark-Compact)

  複製收集算法在對象存活率較高時就要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。

根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。


分代法(Java堆採用)

主要思想是根據對象的生命週期長短特點將其進行分塊,根據每塊內存區間的特點,使用不同的回收算法,從而提高垃圾回收的效率。比如Java虛擬機中的堆就採用了這種方法分成了新生代和老年代。然後對於不同的代採用不同的垃圾回收算法。新生代使用了複製算法,老年代使用了標記壓縮清除算法。

JVM中的堆,一般分為三大部分:新生代、老年代、永久代:



Minor GC和Full

(A)、Minor GC

又稱新生代GC,指發生在新生代的垃圾收集動作;

因為Java對象大多是朝生夕滅,所以Minor GC非常頻繁,一般回收速度也比較快;

(B)、Full GC

又稱Major GC或老年代GC,指發生在老年代的GC;

出現Full GC經常會伴隨至少一次的Minor GC(不是絕對,Parallel Sacvenge收集器就可以選擇設置Major GC策略);

Major GC速度一般比Minor GC慢10倍以上;


新生代區、老年代、永久代

一:新生代:主要是用來存放新生的對象。一般佔據堆的1/3空間。由於頻繁創建對象,所以新生代會頻繁觸發MinorGC進行垃圾回收。

新生代又分為 Eden區、ServivorFrom、ServivorTo三個區。

Eden區:Java新對象的出生地(如果新創建的對象佔用內存很大,則直接分配到老年代)。當Eden區內存不夠的時候就會觸發MinorGC,對新生代區進行一次垃圾回收。

ServivorTo:保留了一次MinorGC過程中的倖存者。

ServivorFrom:上一次GC的倖存者,作為這一次GC的被掃描者。

MinorGC的過程:MinorGC採用複製算法。首先,把Eden和ServivorFrom區域中存活的對象複製到ServicorTo區域(如果有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(如果ServicorTo不夠位置了就放到老年區);然後,清空Eden和ServicorFrom中的對象;最後,ServicorTo和ServicorFrom互換,原ServicorTo成為下一次GC時的ServicorFrom區。

二:老年代:主要存放應用程序中生命週期長的內存對象。

老年代的對象比較穩定,所以MajorGC不會頻繁執行。在進行MajorGC前一般都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次MajorGC進行垃圾回收騰出空間。

MajorGC採用標記—清除算法:首先掃描一次所有老年代,標記出存活的對象,然後回收沒有標記的對象。MajorGC的耗時比較長,因為要掃描再回收。MajorGC會產生內存碎片,為了減少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。

當老年代也滿了裝不下的時候,就會拋出OOM(Out of Memory)異常。

三:永久代

指內存的永久保存區域,主要存放Class和Meta(元數據)的信息,Class在被加載的時候被放入永久區域. 它和和存放實例的區域不同,GC不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨著加載的Class的增多而脹滿,最終拋出OOM異常。

在Java8中,永久代已經被移除,被一個稱為“元數據區”(元空間)的區域所取代。

  元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:

  -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。

  -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

採用元空間而不用永久代的幾點原因:

  1、為了解決永久代的OOM問題,元數據和class對象存在永久代中,容易出現性能問題和內存溢出。

  2、類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出(因為堆空間有限,此消彼長)。

  3、永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。

併發垃圾收集和並行垃圾收集的區別(瞭解)

(A)、並行(Parallel)

指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態;

如ParNew、Parallel Scavenge、Parallel Old;

(B)、併發(Concurrent)

指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行);

用戶程序在繼續運行,而垃圾收集程序線程運行於另一個CPU上;

如CMS、G1(也有並行);


垃圾回收器

 1.Serial/Serial Old

  Serial/Serial Old收集器是最基本最古老的收集器,它是一個單線程收集器,並且在它進行垃圾收集時,必須暫停所有用戶線程。Serial收集器是針對新生代的收集器,採用的是Copying算法,Serial Old收集器是針對老年代的收集器,採用的是Mark-Compact算法。它的優點是實現簡單高效,但是缺點是會給用戶帶來停頓。

  2.ParNew

  ParNew收集器是Serial收集器的多線程版本,使用多個線程進行垃圾收集。

  3.Parallel Scavenge

  Parallel Scavenge收集器是一個新生代的多線程收集器(並行收集器),它在回收期間不需要暫停其他用戶線程,其採用的是Copying算法,該收集器與前兩個收集器有所不同,它主要是為了達到一個可控的吞吐量。

  4.Parallel Old

  Parallel Old是Parallel Scavenge收集器的老年代版本(並行收集器),使用多線程和Mark-Compact算法。

  5.CMS

  CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,它是一種併發收集器,採用的是Mark-Sweep算法。(老年代收集器)

  6.G1

  G1收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多CPU、多核環境。因此它是一款並行與併發收集器,並且它能建立可預測的停頓時間模型。

詳細查看該篇博文cnblogs.com/cxxjohnson/p/8625713.html

重點掌握CMS和G1

java -XX:+PrintCommandLineFlags -version

jdk1.7 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默認垃圾收集器G1


JVM調優

 對JVM內存的系統級的調優主要的目的是減少GC的頻率和Full GC的次數,過多的GC和Full GC是會佔用很多的系統資源(主要是CPU),影響系統的吞吐量。特別要關注Full GC,因為它會對整個堆進行整理,導致Full GC一般由於以下幾種情況:

1)年老代(Tenured)被寫滿

調優時儘量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象 。

2)持久代Pemanet Generation空間不足(jdk8已經沒有持久代)

增大Perm Gen空間,避免太多靜態對象 , 控制好新生代和舊生代的比例

3)System.gc()被顯示調用

垃圾回收不要手動觸發,儘量依靠JVM自身的機制

調優手段主要是通過控制堆內存的各個部分的比例和GC策略來實現,下面來看看各部分比例不良設置會導致什麼後果

1). 新生代設置過小

  一是新生代GC次數非常頻繁,增大系統消耗;二是導致大對象直接進入舊生代,佔據了舊生代剩餘空間,誘發Full GC

2). 新生代設置過大

  一是新生代設置過大會導致舊生代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加

  一般說來新生代佔整個堆1/3比較合適

3). Survivor設置過小

  導致對象從eden直接到達舊生代,降低了在新生代的存活時間

4). Survivor設置過大

  導致eden過小,增加了GC頻率

  另外,通過-XX:MaxTenuringThreshold=n來控制新生代存活時間,儘量讓對象在新生代被回收

由內存管理和垃圾回收可知新生代和舊生代都有多種GC策略和組合搭配,選擇這些策略對於我們這些開發人員是個難題,JVM提供兩種較為簡單的GC策略的設置方式

分析結果,判斷是否需要優化

如果各項參數設置合理,系統沒有超時日誌出現,GC頻率不高,GC耗時不高,那麼沒有必要進行GC優化,如果GC時間超過1-3秒,或者頻繁GC,則必須優化。

注:如果滿足下面的指標,則一般不需要進行優化:

l Minor GC執行時間不到50ms;

l Minor GC執行不頻繁,約10秒一次;

l Full GC執行時間不到1s;

l Full GC執行頻率不算頻繁,不低於10分鐘1次;

JVM調優參數參考

1.針對JVM堆的設置,一般可以通過-Xms -Xmx限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而產生額外的時間,通常把最大、最小設置為相同的值;

2.年輕代和年老代將根據默認的比例(1:2)分配堆內存, 可以通過調整二者之間的比率NewRadio來調整二者之間的大小,也可以針對回收代。

比如年輕代,通過 -XX:newSize -XX:MaxNewSize來設置其絕對大小。同樣,為了防止年輕代的堆收縮,我們通常會把-XX:newSize -XX:MaxNewSize設置為同樣大小。

3.年輕代和年老代設置多大才算合理

1)更大的年輕代必然導致更小的年老代,大的年輕代會延長普通GC的週期,但會增加每次GC的時間;小的年老代會導致更頻繁的Full GC

2)更小的年輕代必然導致更大年老代,小的年輕代會導致普通GC很頻繁,但每次的GC時間會更短;大的年老代會減少Full GC的頻率

如何選擇應該依賴應用程序對象生命週期的分佈情況: 如果應用存在大量的臨時對象,應該選擇更大的年輕代;如果存在相對較多的持久對象,年老代應該適當增大。但很多應用都沒有這樣明顯的特性。

在抉擇時應該根 據以下兩點:

(1)本著Full GC儘量少的原則,讓年老代儘量緩存常用對象,JVM的默認比例1:2也是這個道理 。

(2)通過觀察應用一段時間,看其他在峰值時年老代會佔多少內存,在不影響Full GC的前提下,根據實際情況加大年輕代,比如可以把比例控制在1:1。但應該給年老代至少預留1/3的增長空間。

4.在配置較好的機器上(比如多核、大內存),可以為年老代選擇並行收集算法: -XX:+UseParallelOldGC 。

5.線程堆棧的設置:每個線程默認會開啟1M的堆棧,用於存放棧幀、調用參數、局部變量等,對大多數應用而言這個默認值太了,一般256K就足用。

理論上,在內存不變的情況下,減少每個線程的堆棧,可以產生更多的線程,但這實際上還受限於操作系統。

調優命令

jstack

觀察jvm中當前所有線程的運行情況和線程當前狀態

Jps查看當前機器運行的java程序

jstack -l 52676 > ./mainStack.txt

查找java佔用高cpu的線程

cnblogs.com/tiancai/p/9252780.html

csdn.net/jijianshuai/article/details/79128014

查看進程堆內存使用情況,包括使用的GC算法、堆配置參數和各代中堆內存使用情況

jmap -heap 52676

cnblogs.com/kongzhongqijing/articles/3621163.html

使用jmap -histo[:live] pid查看堆內存中的對象數目、大小統計直方圖,如果帶上live則只統計活對象

jmap -histo:live 52676

用jmap把進程內存使用情況dump到文件中

jmap -dump:format=b,file=./dump.dat 49364

dump出來的文件可以用MAT、VisualVM、jhat等工具查看

jhat -port 9998 ./dump.dat

jstat

cnblogs.com/lizhonghua34/p/7307139.html

jstat -gc 49364 250 4

S0C、S1C、S0U、S1U:Survivor 0/1區容量(Capacity)和使用量(Used)

EC、EU:Eden區 容量和使用量

OC、OU:年老代容量和使用量

PC、PU:永久代容量和使用量

YGC、YGT:年輕代GC次數和GC耗時

FGC、FGCT:Full GC次數和Full GC耗時

GCT:GC總耗時

JVM調參

最後彙總一下JVM常見配置

堆設置

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:設置年輕代大小

-XX:NewRatio=n:設置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代佔整個年輕代年老代和的1/4

-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5

-XX:MaxPermSize=n:設置持久代大小

收集器設置

-XX:+UseSerialGC:設置串行收集器

-XX:+UseParallelGC:設置並行收集器

-XX:+UseParalledlOldGC:設置並行年老代收集器

-XX:+UseConcMarkSweepGC:設置併發收集器

垃圾回收統計信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

並行收集器設置

-XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。

-XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間

-XX:GCTimeRatio=n:設置垃圾回收時間佔程序運行時間的百分比。公式為1/(1+n)

併發收集器設置

-XX:+CMSIncrementalMode:設置為增量模式。適用於單CPU情況。

-XX:ParallelGCThreads=n:設置併發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。

jianshu.com/p/fff649585b78

版权声明:JVM相關面試題内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请联系 删除。

本文链接:https://www.fcdong.com/f/9ff1edad89fc27d4893488fcfaa60f5a.html