1. 程式人生 > >【面試題】JVM相關

【面試題】JVM相關

1.堆和棧的區別

  • 棧記憶體是儲存方法幀和區域性變數(基本型別的變數、物件的引用變數),方法呼叫完後會釋放該棧及棧中變數。存取速度比堆要快,僅次於暫存器,棧資料可以共享,多個引用可以指向同一個地址,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。

  • 堆記憶體用於存放由new建立的物件和陣列,由JVM管理,由於要在執行時動態分配記憶體,存取速度較慢,

  • 棧中的變數指向堆記憶體中的變數,這就是 Java 中的指標

2.執行時資料區域有有哪些?

  • 程式計數器:記錄當前執行緒所執行到的位元組碼的行號,若執行緒中執行的是一個Java方法時,程式計數器中記錄的是正在執行的執行緒的虛擬機器位元組碼指令的地址,若是native方法,則計數器值為空。是JVM中唯一一個不會發生OOM的區域。

  • 虛擬機器棧: 每個方法被執行的時候都會建立一個棧幀,一個棧幀包含:區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法被呼叫到執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

  • 本地方法棧:為JVM所呼叫到的Nativa(本地方法)服務的,

  • 堆:儲存物件例項,更好地分配記憶體。堆是GC管理的主要區域。為了更好地回收記憶體。在物理上可以不連續,只要邏輯上連續即可。分配堆記憶體的方法有指標碰撞法和空閒列表法。

  • 方法區:堆的一個邏輯部分,但卻是非堆,儲存執行時常量池(具有動態性)、已被虛擬機器載入的類資訊、常量、靜態變數、JIT編譯後的程式碼等資訊。

  • 執行時常量池:存放編譯期生成的各種字面量和符號引用,執行期間也可以將新的常量放入池中,例如String類的intern()方法,JDK7中若常量池中無資料,則會從堆中引用。

  • 堆和方法區是執行緒共享的,而棧是執行緒獨享的。OOM:給一個物件一直分配記憶體,超過最大大小限制或本地直接記憶體已滿,會丟擲OOM,SOF:遞迴呼叫方法,

  • 記憶體洩露:一個不再使用的物件或變數卻被引用而一直被佔據在記憶體中,如當 o物件的引用被置空後,若發生 GC,o物件不能被 GC 回收,因為 GC 在跟蹤程式碼棧中的引用時,會發現 v 引用,儘管o 引用已經被置空,但是 Object 物件仍然存在其他的引用,是可以被訪問到的,所以 GC 無法將其釋放掉。從而導致記憶體洩露,還有資料庫的連線、網路連線、IO連線等沒有顯示呼叫關閉,也會造成記憶體洩露。

3.堆記憶體的劃分?

  • JDK8之前堆的記憶體區域分為新生代、老年代 和永久區,

    • 新生代:所有新生成的物件首先都是放在新生代的。以儘可能快速的收集掉那些生命週期短的物件,它被細分為 Eden 、 Survivor from 和Survivor to。預設Edem : from : to = 8 : 1 : 1 ( 可以通過引數 –XX:SurvivorRatio 來設定 ),而JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為物件服務,總有一塊 Survivor 區域空閒。因此,新生代實際可用的記憶體空間為90% 的新生代空間。
    • 老年代:在新生代中經歷了N次(預設15次)垃圾回收後仍然存活的物件,就會被放到老年代中。因此,老年代中存放的都是一些生命週期較長的物件。預設老年代與新生代的比例的值為 2:1 ( 通過引數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。
    • 永久區:用於存放Class和Meta的資訊,Class在被 Load的時候被放入永久區,它和存放Instance的Heap區域不同,
  • JDK8中移除永久區,引入元空間,元資料(類資訊、常量、靜態變數)都在本地記憶體中分配,元資料由Klass (class檔案在jvm裡的執行時資料結構)和NoKlass Metaspace(常量池)組成,類載入器用SpaceManager管理元空間。用元資料替代永久區的原因是永久代中容易出現效能問題和記憶體溢位,並且指定大小困難。

  • 使用Runtime.freeMemory() 方法可知剩餘空間的位元組數

4.GC演算法有哪些?

  • 判斷物件是否存活

    • 引用計數法:引用時,計數器值就加1;當引用失效時,計數器值就減1,計數器為0時回收該物件,不能解決迴圈引用問題
    • 可達性分析法:當物件到GC Root沒有任何引用鏈相連時,回收該物件。GC Root包含java虛擬機器棧幀中的本地變量表中的引用物件、方法區中的類靜態屬性引用物件、常量引用物件、本地方法的引用物件。
    • 四種引用:強引用:寧願OOM也不回收記憶體;軟引用:記憶體不夠就回收;弱引用:每次GC都回收;虛引用:GC時發出一個系統通知。
    • finalize方法:只會被系統自動呼叫一次,如果下一次回收,該方法不會被執行,因此儘量避免使用finalize方法。
  • GC演算法

    • 標記-清除:效率低,產生大量的不連續的記憶體碎片
    • 複製演算法:可使用記憶體變為原來的一半,當物件存活率高時,效率低。
    • 標記-整理:標記出要清除的物件,往一端移動,過程中清除物件。
    • 分代收集:新生代採用複製演算法,而老年代採用標記整理演算法,永久代採用標記-整理演算法(JDK8之前)
    • 分割槽演算法:將整個記憶體分為N個小的獨立空間,每個獨立空間都可以獨立使用或進行GC,減少了GC的停頓時間。

5.minor GC和Full GC的觸發時機

  • 物件在記憶體中的初始化過程:首先載入該物件所對應的class檔案,然後為將要初始化的物件分配記憶體空間,優先在TLAB分配大小,如果空間不足,再到eden中進行記憶體分配.

  • Minor GC
    觸發條件:當新物件生成,並且在Eden申請空間失敗時,就會觸發Minor GC,查詢GC Roots,拷貝所引用的物件到 to 區;會發現晉升的情況,然後清理Eden 以及 Survivor from 區,並且將這些物件的年齡設定為1,以後物件在 Survivor 區每熬過一次 Minor GC,就將物件的年齡 + 1,當物件的年齡達到某個值時 ( 預設是 15 歲,可以通過引數 -XX:MaxTenuringThreshold 來設定 ),這些物件就會成為老年代。若出現較大的物件 , 則直接進入到老年代。Eden區的GC會頻繁進行。一般使用複製演算法,使Eden去能儘快空閒出來。

  • Full GC
    對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆對進行回收,所以比Minor GC要慢,儘可能減少Full GC的次數。採用的是標記-清除演算法,會產生許多的記憶體碎片,此後需要為較大的物件分配記憶體空間時,若無法找到足夠的連續的記憶體空間,就會提
    前觸發一次 GC 的收集動作。

  • 可能導致Full GC的原因

    • 老年代被寫滿,或空間不足;永久代被寫滿
    • System.gc()被顯示呼叫,底層呼叫Runtime.getRuntime().gc()這個本地方法
    • Minor GC晉升到老年代的平均大小大於老年代的剩餘空間
    • 堆中分配很大的物件
    • CMS GC時出現promotion failed(新生代老年代都放不下)和concurrent mode failure(同時有物件要放入老年代,而此時老年代空間不足)
  • TLAB:JVM在記憶體新生代Eden Space中開闢了一小塊執行緒私有的區域。預設設定為佔用Eden Space的1%。小物件通常JVM會優先分配在TLAB上,(主要就是三個指標:start,top 和 end ),TLAB滿後,再分配一塊TLAB,不管之前的。

6.你所知道的GC收集器

  • 新生代收集器

    • serial:是一個單執行緒收集器,並且在它進行垃圾收集時,必須STW。採用複製演算法
    • ParNew:是Serial的多執行緒版本,只有serial和ParNew能與CMS配合工作
    • Parallel Scavenge:為了達到一個可控吞吐量的並行收集器,不需要STW,採用複製演算法
  • 老年代收集器

    • Serial Old:採用單執行緒、標記-整理演算法
    • Parallel Old:採用並行收集、標記-整理演算法。
    • CMS:以獲取最短回收停頓時間為目標的併發收集器,採用的是標記-清除演算法。四個步驟:初始標記(STW)、併發標記、重新標記(STW)、併發清除。缺點是對CPU資源敏感(影響系統性能)、無法處理浮動垃圾、會產生大量空間碎片。
  • G1:JDK1.7提供的一個新收集器

    • 執行步驟:開始標記(STW)、併發標記、最終標記(STW)、篩選回收。
    • 優點:並行與併發,採用“標記 - 整理”演算法,避免產生碎片,可預測的停頓(衰減標準偏差),
    • 分代收集:G1將整個Java堆劃分為多個大小固定的獨立區域,並且跟蹤這些區域,在後臺維護一個優先列表,Young GC模式:回收年輕代裡的Region,Mixed GC:回收老年代Region,而G1無Full GC
  • 常用引數:Xms:初始化堆大小;Xmx:最大堆大小,Xmn:新生代堆大小,Xss:棧大小,-XX:+PrintGCDetails:輸出GC詳細日誌;UseSerialGC:使用Serial + Serial Old的收集器組合進行記憶體回收;UseTLAB:開啟TLAB分配,而命令java –verbose:gc :輸出虛擬機發生記憶體回收時在輸出裝置顯示資訊

7.類載入機制(位元組碼的編譯過程)

  • 類的生命週期是分為載入、驗證、準備、解析、初始化、使用、解除安裝七個過程

  • 載入:載入類的二進位制資料。通過類名獲取二進位制位元組流。將該流轉化為方法區的執行時資料結構。在堆中生成一個代.Class物件,作為對方法區中這些資料的訪問入口

  • 驗證:確保載入的類資訊符合JVM規範,分為檔案格式的驗證:是否以0xCAFEBABE開頭、版本號是否合理;元資料驗證:是否有父類、繼承了final類;位元組碼驗證:執行檢查、棧資料型別和操作碼引數吻合、跳轉指令到合理的位置;符號引用驗證:確保解析動作能正確執行。

  • 準備:為類變數或static變數分配記憶體並儲存設定類變數初始值的階段,這些值都在方法區中進行分配。一般情況(int)準備階段為0,在初始化時才賦值,但對static final型別的資料,在準備階段就會被賦值。

  • 解析:將虛擬機器常量池內的符號引用替換為直接引用的過程

  • 初始化:是執行類構造器()方法的過程,虛擬機器會保證一個類的< clinit>()方法在多執行緒環境中被正確加鎖和同步。類初始化時機:只有當對類的主動使用的時候才會導致類的初始化。

  • 類的主動引用(一定會發生類的初始化)

    • New 一個類的物件
    • 呼叫類的靜態成員(除了final常量)和靜態方法
    • 使用java.lang.reflect包的方法對類進行反射呼叫
    • 當虛擬機器啟動時,先啟動main方法所在的類
    • 若父類沒有初始化則先會初始化父類
  • 類的被動引用

    • 當訪問一個靜態域時,通過子類引用父類的靜態變數,不會導致子類的初始化
    • 通過資料定義類的引用(定義物件陣列),不會觸發此類的初始化,
    • 引用常量不會觸發此類的初始化(常量在編譯階段就存入常量池中了)
    • 通過類名獲取Class物件,不會觸發類的初始化,
    • 通過ClassLoader預設的loadClass方法,也不會觸發初始化動作
  • 結束生命週期的情況:執行System.exit()方法、程式正常執行結束、遇到了異常或錯誤而異常終止、由於作業系統出現錯誤而導致Java虛擬機器程序終止

  • Dakvik虛擬機器不能執行class檔案,因為它並不是Java虛擬機器,使用的是暫存器架構,但它執行的dex檔案可以通過class檔案轉化而來。

8.雙親委派模型

  • 啟動類載入器bootstrap:載入Java的核心庫,是用c/c++實現的,載入擴充套件、應用程式類載入器,並指定它們的父類載入器 rt.jar 引數:-Xbootclasspath

  • 擴充套件類載器:它負責載入JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充套件類載入器

  • 應用程式類載入器:它負責載入使用者類路徑所指定的類,開發者可以直接使用該類載入器,

  • 自定義類載入器:通過繼承java.long.Classloader類,來實現自己的類載入器。若沒有被類載入器載入到名稱空間,委派類載入器請求給父類載入器,載入完成返回class例項,呼叫本類載入器的findClass方法,獲取到對應的位元組碼,呼叫defineClass方法匯入型別到方法區,而loadClass方法是載入指定的類,

  • JVM類載入機制:全盤負責、父類委託、快取機制:所有載入過的Class都會被快取,需要時再尋找。

  • 雙親委託機制:只有父類載入器無法完成此載入任務時,自己才去載入,是代理模式的一種,父類載入器採用組合實現

  • 熱替換:當一個class被替換後,系統無需重啟,替換的類立即生效。OSGI:動態模組系統,有多個類載入器,誰的元件誰來載入

  • Class.forName可以進行初始化,而ClassLoader不能,只是將.class檔案載入到JVM中

  • JDK命令列工具:jps:檢視虛擬機器程序資訊、jinfo:用於檢視和調整虛擬機器的配置引數、jstack:虛擬機器堆疊跟蹤、jstat:檢視虛擬機器統計資訊監視工具、jmap:生成Java應用程式的堆快照和物件的統計資訊、 jhat:分析虛擬機器轉儲快照資訊。記憶體dump後的分析工具:MAT

9.Java的記憶體模型

  • Java記憶體模型中規定了所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了該執行緒使用到的變數到主記憶體副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同執行緒之間無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要在主記憶體來完成

  • 記憶體間互動操作

    • lock:作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
    • unlock:作用主存,把一個處於鎖定狀態的變數釋放出來
    • read:作用主存,把變數值從主記憶體傳輸到工作記憶體中,以便之後load
    • load:作用工作記憶體,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中
    • use:作用工作記憶體,把工作記憶體中的一個變數值傳遞給執行引擎,
    • assign:作用工作記憶體,它把一個從執行引擎接收到的值賦值給工作記憶體的變數
    • store:作用工作記憶體,把工作記憶體中變數的值傳送到主記憶體中,以便之後write
    • write:作用主存,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中
    • 讀寫操作必須按順序執行、lock和unlock必須成對出現、不允許read和load、store和write操作之一單獨出現、對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中
  • 執行程式時為了提高效能,編譯器和處理器經常會對指令進行重排序

    • as-if-serial:不管怎麼重排序,程式的執行結果不能被改變
    • 為了保證記憶體的可見性,編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止處理器重排序
    • 當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體

10.類檔案結構

  • class檔案主要由魔數、Class檔案的版本號、常量池、訪問標誌、類索引(還包括父類索引和介面索引集合)、欄位表集合、方法表集合、屬性表集合組成。

  • 魔數:每個Class檔案的頭4個位元組,用於確定Class檔案是否能被虛擬機器接受

  • 版本號:前2位元組表示次版本號,後2個位元組表示主版本號。從45開始,JDK8是50
  • 常量池:存放常量和符號引用。constant_pool_count:佔2位元組,表示常量池中有幾個常量,constant_pool:表型別資料集合,即常量池中每一項常量都是一個表
  • 訪問標誌:用於識別類或介面的訪問資訊
  • 類索引、父類索引和介面索引集合:用於確定這個類的繼承關係。
  • 欄位表集合:用於描述介面或類中宣告的變數
  • 方法表集合和屬性表集合。

11.位元組碼執行引擎

  • 每一個棧幀都包含了區域性變量表、運算元棧、動態連線、方法返回地址等

  • 區域性變量表:是存放一組變數的儲存空間。存放方法引數和方法內部定義的區域性變量表

  • 運算元棧:用來存放運算元的棧結構

  • 動態連線:每個棧幀都包含一個指向執行時常量池的引用,持有這個引用是為了支援動態連線

  • 方法返回地址:當執行引擎遇到方法返回的位元組碼指令,或出現異常,則退出該方法,即把當前棧幀出棧

  • 常用位元組碼:Xload_n:將第幾個區域性變數的值壓棧,Xstore_n(n為0 1 2 3):出棧,將值存入第n個區域性變數;i2l表示將int轉為long;方法呼叫:Invokevirtual;Iconst_m1 int:常量-1入棧

12.一次JVM調優的經歷

  • JVM引數:-Xmn=40M,-Xmx=100M,-Xms=100M,-XX:+UseConcMarkSweepGC,-XX:CMSInitiatingOccupancyFraction=80 ,(記憶體碎片,預設68)-XX:+UseCMSInitiatingOccupancyOnly,

  • 程式碼:用ArrayList集合,list.add(byte[]=5M),出現了OOM,

  • 解決思路:通過檢視GC日誌,dump之後,發現這是我的低階失誤,給陣列分配記憶體太多了,但我意外的發現雖然一直在CMS GC ,但老年代一直很大的,我試著主動觸發一次full gc之後老年代卻下降了,我檢視ArrayList擴容原始碼後發現,是因為擴容的System.arrayCopy方法是native且是淺拷貝,我認為複製的新陣列是在新生代分配的,而通過老年代使用率達到了閾值觸發的CMS GC,會把新生代裡的物件作為GC ROOT的一部分,從而阻止了老年代byte陣列被回收。增加了-XX:+CMSScavengeBeforeRemark引數可以解決這個問題(remark前觸發YGC)。我之後查詢資料驗證了我的解決方法。其實主要就是新生代指向老生代的跨代引用問題。



本人才疏學淺,若有錯,請指出,謝謝!
如果你有更好的建議,可以留言我們一起討論,共同進步!
衷心的感謝您能耐心的讀完本篇博文!