推薦使用小程式閱讀
為了能讓您更加方便的閱讀
本文所有的面試題目均已整理至小程式《面試手冊》
可以通過微信掃描(或長按)下圖的二維碼享受更好的閱讀體驗!

目錄
- 推薦使用小程式閱讀
- 1. JVM 基礎
- 1.1 JVM 記憶體分哪幾個區,每個區的作用是什么?
- 方法區
- 虛擬機堆疊:
- 本地方法堆疊
- 堆
- 程式計數器
- 1.2 物件的訪問定位有幾種方式?
- 句柄訪問
- 直接指標訪問
- 1.3 JVM記憶體模型是什么?
- 1.4 finalize()方法什么時候被呼叫?解構式(finalization)的目的是什么?
- 1.5 什么是深拷貝和淺拷貝?什么是深復制和淺復制?
- 1.6 說一下堆疊的區別?
- 1.7 佇列和堆疊是什么?有什么區別?
- 1.8 Java會存在記憶體泄漏嗎?請簡單描述
- 1.9 Java物件結構是什么?
- 1.10 參考的分類有幾種?
- 2. JVM 垃圾回收
- 2.1 如和判斷一個物件是否存活?(或者 GC 物件的判定方法)
- 參考計數法
- 可達性演算法(參考鏈法)
- 2.2 簡述 java 垃圾回識訓制?
- 2.3 垃圾回收有什么目的?什么時候進行垃圾回收?
- 2.4 如果物件的參考被置為null,垃圾收集器是否會立即釋放物件占用的記憶體?
- 2.5 GC是什么?為什么要GC?
- 2.6 垃圾回收的優點有那些?
- 2.7 垃圾回收器的基本原理是什么?
- 2.8 垃圾回收器可以馬上回收記憶體嗎?
- 2.9 有什么辦法主動通知虛擬機進行垃圾回收?
- 2.10 Java 中都有哪些參考型別?
- 2.11 JVM中的永久代中會發生垃圾回收嗎?
- 2.12 JVM 有哪些垃圾回收演算法?
- 2.13 簡述一下標記-清除演算法
- 2.14 簡述一下復制演算法
- 2.15 簡述一下標記-整理演算法
- 2.16 簡述一下分代收集演算法
- 2.17 JVM 有哪些垃圾回收器?
- 2.18 詳細介紹一下 CMS 垃圾回收器?
- 2.19 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么區別?
- 2.20 簡述分代垃圾回收器是怎么作業的?
- 3. JVM 類加載
- 3.1 JVM類加載的時機?
- 3.2 JVM類加載程序?
- 加載
- 驗證
- 準備
- 決議
- 初始化
- 卸載
- 3.3 JVM加載Class檔案的原理機制是什么?
- 3.4 什么是類加載器,類加載器有哪些?
- 3.5 什么是雙親委派模型?
- 4. JVM調優
- 4.1 用過那些JVM 調優的工具?
- 4.2 常用的 JVM 調優的引數都有哪些?
- 4.3 如何分析GC日志?
- 4.4 JVM調優命令有那些?
- 4.5 你知道哪些JVM性能調優方式有那些?
1. JVM 基礎
1.1 JVM 記憶體分哪幾個區,每個區的作用是什么?
方法區
1. 有時候也成為永久代,在該區內很少發生垃圾回收,但是并不代表不發生 GC,在這里進行的 GC 主要是對方法區里的常量池和對型別的卸載
2. 方法區主要用來存盤已被虛擬機加載的類的資訊、常量、靜態變數和即時編譯器編譯后的代碼等資料,
3. 該區域是被執行緒共享的,
4. 方法區里有一個運行時常量池,用于存放靜態編譯產生的字面量和符號參考,該常量池具有動態性,也就是說常量并不一定是編譯時確定,運行時生成的常量也會存在這個常量池中,
虛擬機堆疊:
-
虛擬機堆疊也就是我們平常所稱的堆疊記憶體,它為 java 方法服務,每個方法在執行的時候都會創建一個堆疊幀,用于存盤區域變數表、運算元堆疊、動態鏈接和方法出口等資訊,
-
虛擬機堆疊是執行緒私有的,它的生命周期與執行緒相同,
-
區域變數表里存盤的是基本資料型別、returnAddress 型別(指向一條位元組碼指令的地址)和物件參考,這個物件參考有可能是指向物件起始地址的一個指標,也有可能是代表物件的句柄或者與物件相關聯的位置,區域變數所需的記憶體空間在編譯器間確定
-
運算元堆疊的作用主要用來存盤運算結果以及運算的運算元,它不同于區域變數表通過索引來訪問,而是壓堆疊和出堆疊的方式
-
每個堆疊幀都包含一個指向運行時常量池中該堆疊幀所屬方法的參考,持有這個參考是為了支持方法呼叫程序中的動態連接.動態鏈接就是將常量池中的符號參考在運行期轉化為直接參考,
本地方法堆疊
本地方法堆疊和虛擬機堆疊類似,只不過本地方法堆疊為 Native 方法服務,
堆
java 堆是所有執行緒所共享的一塊記憶體,在虛擬機啟動時創建,幾乎所有的物件實體都在這里創建,因此該區域經常發生垃圾回收操作,
程式計數器
記憶體空間小,位元組碼解釋器作業時通過改變這個計數值可以選取下一條需要執行的位元組碼指令,分支、回圈、跳轉、例外處理和執行緒恢復等功能都需要依賴這個計數器完成,該記憶體區域是唯一一個 java 虛擬機規范沒有規定任何 OOM 情況的區域,
1.2 物件的訪問定位有幾種方式?
Java程式需要通過 JVM 堆疊上的參考訪問堆中的具體物件,物件的訪問方式取決于 JVM 虛擬機的實作,目前主流的訪問方式有 句柄 和 直接指標 兩種方式,
- 句柄:
可以理解為指向指標的指標,維護著物件的指標,句柄不直接指向物件,而是指向物件的指標(句柄不發生變化,指向固定記憶體地址),再由物件的指標指向物件的真實記憶體地址, - 直接指標:
指向物件,代表一個物件在記憶體中的起始地址,
句柄訪問
Java堆中劃分出一塊記憶體來作為句柄池,參考中存盤物件的句柄地址,而句柄中包含了物件實體資料與物件型別資料各自的具體地址資訊,具體構造如下圖所示:

優勢:參考中存盤的是穩定的句柄地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變句柄中的實體資料指標,而參考本身不需要修改,
直接指標訪問
如果使用直接指標訪問,參考 中存盤的直接就是物件地址,那么Java堆物件內部的布局中就必須考慮如何放置訪問型別資料的相關資訊,

優勢:速度更快,節省了一次指標定位的時間開銷,由于物件的訪問在Java中非常頻繁,因此這類開銷積少成多后也是非常可觀的執行成本,HotSpot 中采用的就是這種方式,
1.3 JVM記憶體模型是什么?
java 記憶體模型(JMM)是執行緒間通信的控制機制.JMM 定義了主記憶體和執行緒之間抽象關系,執行緒之間的共享變數存盤在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中存盤了該執行緒以讀/寫共享變數的副本,本地記憶體是 JMM 的一個抽象概念,并不真實存在,它涵蓋了快取,寫緩沖區,暫存器以及其他的硬體和編譯器優化,Java 記憶體模型的抽象示意圖如下:
執行緒 A 與執行緒 B 之間如要通信的話,必須要經歷下面 2 個步驟:
- 首先,執行緒 A 把本地記憶體 A 中更新過的共享變數重繪到主記憶體中去,
- 然后,執行緒 B 到主記憶體中去讀取執行緒 A 之前已更新過的共享變數,
1.4 finalize()方法什么時候被呼叫?解構式(finalization)的目的是什么?
垃圾回收器(garbage colector)決定回收某物件時,就會運行該物件的finalize()方法;
finalize是Object類的一個方法,該方法在Object類中的宣告protected void finalize() throws Throwable { }
在垃圾回收器執行時會呼叫被回收物件的finalize()方法,可以覆寫此方法來實作對其資源的回收,注意:一旦垃圾回收器準備釋放物件占用的記憶體,將首先呼叫該物件的finalize()方法,并且下一次垃圾回收動作發生時,才真正回收物件占用的記憶體空間
GC本來就是記憶體回收了,應用還需要在finalization做什么呢? 答案是大部分時候,什么都不用做(也就是不需要多載),只有在某些很特殊的情況下,比如你呼叫了一些native的方法(一般是C寫的),可以要在finaliztion里去呼叫C的釋放函式,
1.5 什么是深拷貝和淺拷貝?什么是深復制和淺復制?
淺拷貝(shallowCopy)只是增加了一個指標指向已存在的記憶體地址,
深拷貝(deepCopy)是增加了一個指標并且申請了一個新的記憶體,使這個增加的指標指向這個新的記憶體,
使用深拷貝的情況下,釋放記憶體的時候不會因為出現淺拷貝時釋放同一個記憶體的錯誤,
淺復制:僅僅是指向被復制的記憶體地址,如果原地址發生改變,那么淺復制出來的物件也會相應的改變,
深復制:在計算機中開辟一塊新的記憶體地址用于存放復制的物件,
1.6 說一下堆疊的區別?
-
物理地址
-
堆的物理地址分配對物件是不連續的,因此性能慢些,在GC的時候也要考慮到不連續的分配,所以有各種演算法,比如,
標記-消除,復制,標記-壓縮,分代(即新生代使用復制演算法,老年代使用標記——壓縮) -
堆疊使用的是資料結構中的堆疊,先進后出的原則,物理地址分配是連續的,所以性能快,
-
-
記憶體分別
- 堆因為是不連續的,所以分配的記憶體是在
運行期確認的,因此大小不固定,一般堆大小遠遠大于堆疊, - 堆疊是連續的,所以分配的記憶體大小要在
編譯期就確認,大小是固定的,
- 堆因為是不連續的,所以分配的記憶體是在
-
存放的內容
- 堆存放的是物件的實體和陣列,因此該區更關注的是資料的存盤
- 堆疊存放:區域變數,運算元堆疊,回傳結果,該區更關注的是程式方法的執行,
PS:- 靜態變數放在方法區
- 靜態的物件還是放在堆,
-
程式的可見度
- 堆對于整個應用程式都是共享、可見的,
- 堆疊只對于執行緒是可見的,所以也是執行緒私有,他的生命周期和執行緒相同,
1.7 佇列和堆疊是什么?有什么區別?
佇列和堆疊都是被用來預存盤資料的,
- 操作的名稱不同,佇列的插入稱為入隊,佇列的洗掉稱為出隊,堆疊的插入稱為進堆疊,堆疊的洗掉稱為出堆疊,
- 可操作的方式不同,佇列是在隊尾入隊,隊頭出隊,即兩邊都可操作,而堆疊的進堆疊和出堆疊都是在堆疊頂進行的,無法對堆疊底直接進行操作,
- 操作的方法不同,佇列是先進先出(FIFO),即佇列的修改是依先進先出的原則進行的,新來的成員總是加入隊尾(不能從中間插入),每次離開的成員總是佇列頭上(不允許中途離隊),而堆疊為后進先出(LIFO),即每次洗掉(出堆疊)的總是當前堆疊中最新的元素,即最后插入(進堆疊)的元素,而最先插入的被放在堆疊的底部,要到最后才能洗掉,
1.8 Java會存在記憶體泄漏嗎?請簡單描述
記憶體泄漏是指不再被使用的物件或者變數一直被占據在記憶體中,理論上來說,Java是有GC垃圾回識訓制的,也就是說,不再被使用的物件,會被GC自動回收掉,自動從記憶體中清除,
但是,即使這樣,Java也還是存在著記憶體泄漏的情況,java導致記憶體泄露的原因很明確:長生命周期的物件持有短生命周期物件的參考就很可能發生記憶體泄露,盡管短生命周期物件已經不再需要,但是因為長生命周期物件持有它的參考而導致不能被回收,這就是java中記憶體泄露的發生場景,
1.9 Java物件結構是什么?
Java物件由三個部分組成:物件頭、實體資料、對齊填充,
-
物件頭由兩部分組成
- 第一部分存盤物件自身的運行時資料:
- 哈希碼;
- GC分代年齡;
- 鎖標識狀態;
- 執行緒持有的鎖;
- 偏向執行緒ID(一般占32/64 bit),
- 第二部分是指標型別,指向物件的類元資料型別(即物件代表哪個類),如果是陣列物件,則物件頭中還有一部分用來記錄陣列長度,
- 第一部分存盤物件自身的運行時資料:
-
實體資料用來存盤物件真正的有效資訊(包括父類繼承下來的和自己定義的)
-
對齊填充:JVM要求物件起始地址必須是8位元組的整數倍(8位元組對齊)
1.10 參考的分類有幾種?
- 強參考:GC時不會被回收
- 軟參考:描述有用但不是必須的物件,在發生記憶體溢位例外之前被回收
- 弱參考:描述有用但不是必須的物件,在下一次GC時被回收
- 虛參考(幽靈參考/幻影參考):無法通過虛參考獲得物件,用PhantomReference實作虛參考,虛參考用來在GC時回傳一個通知,
2. JVM 垃圾回收
2.1 如和判斷一個物件是否存活?(或者 GC 物件的判定方法)
判斷一個物件是否存活有兩種方法:
參考計數法
所謂參考計數法就是給每一個物件設定一個參考計數器,每當有一個地方參考這個物件時,就將計數器加一,參考失效時,計數器就減一,當一個物件的參考計數器為零時,說明此物件沒有被參考,也就是“死物件”,將會被垃圾回收.
參考計數法有一個缺陷就是無法解決回圈參考問題,也就是說當物件 A 參考物件 B,物件 B 又參考者物件 A,那么此時 A,B 物件的參考計數器都不為零,也就造成無法完成垃圾回收,所以主流的虛擬機都沒有采用這種演算法,
可達性演算法(參考鏈法)
該演算法的思想是:從一個被稱為 GC Roots 的物件開始向下搜索,如果一個物件到 GC Roots 沒有任何參考鏈相連時,則說明此物件不可用,在 java 中可以作為 GC Roots 的物件有以下幾種:
- 虛擬機堆疊中參考的物件
- 方法區類靜態屬性參考的物件
- 方法區常量池參考的物件
- 本地方法堆疊 JNI 參考的物件
雖然這些演算法可以判定一個物件是否能被回收,但是當滿足上述條件時,一個物件不一定會被回收,當一個物件不可達 GC Root 時,這個物件并不會立馬被回收,而是出于一個死緩的階段,若要被真正的回收需要經歷兩次標記
如果物件在可達性分析中沒有與 GC Root 的參考鏈,那么此時就會被第一次標記并且進行一次篩選,篩選的條件是是否有必要執行 finalize()方法,當物件沒有覆寫 finalize()方法或者已被虛擬機呼叫過,那么就認為是沒必要的,
如果該物件有必要執行 finalize()方法,那么這個物件將會放在一個稱為 F-Queue 的對佇列中,虛擬機會觸發一個 Finalize()執行緒去執行,此執行緒是低優先級的,并且虛擬機不會承諾一直等待它運行完,這是因為如果 finalize()執行緩慢或者發生了死鎖,那么就會造成 F-Queue 佇列一直等待,造成了記憶體回收系統的崩潰,GC 對處于 F-Queue 中的物件進行第二次被標記,這時,該物件將被移除”即將回收”集合,等待回收,
2.2 簡述 java 垃圾回識訓制?
在 java 中,程式員是不需要顯示的去釋放一個物件的記憶體的,而是由虛擬機自行執行,在JVM 中,有一個垃圾回收執行緒,它是低優先級的,在正常情況下是不會執行的,只有在虛擬機空閑或者當前堆記憶體不足時,才會觸發執行,掃面那些沒有被任何參考的物件,并將它們添加到要回收的集合中,進行回收,
2.3 垃圾回收有什么目的?什么時候進行垃圾回收?
垃圾回收是在記憶體中存在沒有參考的物件或超過作用域的物件時進行的,
垃圾回收的目的是識別并且丟棄應用不再使用的物件來釋放和重用資源,
2.4 如果物件的參考被置為null,垃圾收集器是否會立即釋放物件占用的記憶體?
不會,在下一個垃圾回呼周期中,這個物件將是被可回收的,
也就是說并不會立即被垃圾收集器立刻回收,而是在下一次垃圾回收時才會釋放其占用的記憶體,
2.5 GC是什么?為什么要GC?
GC是垃圾收集的意思,記憶體處理是編程人員容易出現問題的地方,忘記或者錯誤的記憶體回識訓導致程式或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測物件是否超過作用域從而達到自動回收記憶體的目的,Java語言沒有提供釋放已分配記憶體的顯示操作方法,Java程式員不用擔心記憶體管理,因為垃圾收集器會自動進行管理,要請求垃圾收集,可以呼叫下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉顯示的垃圾回收呼叫, 垃圾回收可以有效的防止記憶體泄露,有效的使用可以使用的記憶體,垃圾回收器通常是作為一個單獨的低優先級的執行緒運行,不可預知的情況下對記憶體堆中已經死亡的或者長時間沒有使用的物件進行清除和回收,程式員不能實時的呼叫垃圾回收器對某個物件或所有物件進行垃圾回收,在Java誕生初期,垃圾回收是Java最大的亮點之一,因為服務器端的編程需要有效的防止記憶體泄露問題,然而時過境遷,如今Java的垃圾回識訓制已經成為被詬病的東西,移動智能終端用戶通常覺得iOS的系統比Android系統有更好的用戶體驗,其中一個深層次的原因就在于android系統中垃圾回收的不可預知性,
補充:垃圾回識訓制有很多種,包括:分代復制垃圾回收、標記垃圾回收、增量垃圾回收等方式,標準的Java行程既有堆疊又有堆,堆疊保存了原始型區域變數,堆保存了要創建的物件,Java平臺對堆記憶體回收和再利用的基本演算法被稱為標記和清除,但是Java對其進行了改進,采用“分代式垃圾收集”,這種方法會跟Java物件的生命周期將堆記憶體劃分為不同的區域,在垃圾收集程序中,可能會將物件移動到不同區域:
- 伊甸園(Eden):這是物件最初誕生的區域,并且對大多數物件來說,這里是它們唯一存在過的區域,
- 幸存者樂園(Survivor):從伊甸園幸存下來的物件會被挪到這里,
- 終身頤養園(Tenured):這是足夠老的幸存物件的歸宿,年輕代收集(Minor-GC)程序是不會觸及這個地方的,當年輕代收集不能把物件放進終身頤養園時,就會觸發一次完全收集(Major-GC),這里可能還會牽扯到壓縮,以便為大物件騰出足夠的空間, 與垃圾回收相關的JVM引數:
-Xms / -Xmx:堆的初始大小 / 堆的最大大小
-Xmn:堆中年輕代的大小
-XX:-DisableExplicitGC:讓System.gc()不產生任何作用
-XX:+PrintGCDetails:列印GC的細節
-XX:+PrintGCDateStamps:列印GC操作的時間戳
-XX:NewSize / XX:MaxNewSize: 設定新生代大小/新生代最大大小
-XX:NewRatio :可以設定老生代和新生代的比例
-XX:PrintTenuringDistribution :設定每次新生代GC后輸出幸存者樂園中物件年齡的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:設定老年代閥值的初始值和最大值
-XX:TargetSurvivorRatio:設定幸存區的目標使用率
2.6 垃圾回收的優點有那些?
- java語言最顯著的特點就是引入了垃圾回識訓制,它使java程式員在撰寫程式時不再考慮記憶體管理的問題,
- 由于有這個垃圾回識訓制,java中的物件不再有“作用域”的概念,只有參考的物件才有“作用域”,
- 垃圾回識訓制有效的防止了記憶體泄露,可以有效的使用可使用的記憶體,
- 垃圾回收器通常作為一個單獨的低級別的執行緒運行,在不可預知的情況下對記憶體堆中已經死亡的或很長時間沒有用過的物件進行清除和回收,
2.7 垃圾回收器的基本原理是什么?
對于GC來說,當程式員創建物件時,GC就開始監控這個物件的地址、大小以及使用情況,
2.8 垃圾回收器可以馬上回收記憶體嗎?
不會;通常,GC采用有向圖的方式記錄和管理堆(heap)中的所有物件,通過這種方式確定哪些物件是"可達的",哪些物件是"不可達的",當GC確定一些物件為"不可達"時,GC就有責任回收這些記憶體空間,而這個回收操作時達到一定閾值或者條件之后才會觸發回收;并不是實時的,
2.9 有什么辦法主動通知虛擬機進行垃圾回收?
可以,程式員可以手動執行System.gc(),通知GC運行,但是Java語言規范并不保證GC一定會執行,
2.10 Java 中都有哪些參考型別?
- 強參考:發生 gc 的時候不會被回收,
- 軟參考:有用但不是必須的物件,在發生記憶體溢位之前會被回收,
- 弱參考:有用但不是必須的物件,在下一次GC時會被回收,
- 虛參考(幽靈參考/幻影參考):無法通過虛參考獲得物件,用 PhantomReference 實作虛參考,虛參考的用途是在 gc 時回傳一個通知,
2.11 JVM中的永久代中會發生垃圾回收嗎?
垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC),如果你仔細查看垃圾收集器的輸出資訊,就會發現永久代也是被回收的,這就是為什么正確的永久代大小對避免Full GC是非常重要的原因,
java 8中,永久代被移除,取而代之的為元空間,
2.12 JVM 有哪些垃圾回收演算法?
-
標記-清除:
這是垃圾收集演算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的物件,然后統一回收,這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的記憶體碎片,導致以后程式在分配較大的物件時,由于沒有充足的連續記憶體而提前觸發一次 GC 動作, -
復制演算法:
為了解決效率問題,復制演算法將可用記憶體按容量劃分為相等的兩部分,然后每次只使用其中的一塊,當一塊記憶體用完時,就將還存活的物件復制到第二塊記憶體上,然后一次性清楚完第一塊記憶體,再將第二塊上的物件復制到第一塊,但是這種方式,記憶體的代價太高,每次基本上都要浪費一般的記憶體,
于是將該演算法進行了改進,記憶體區域不再是按照 1:1 去劃分,而是將記憶體劃分為 8:1:1 三部分,較大那份記憶體交 Eden 區,其余是兩塊較小的記憶體區叫 Survior 區,每次都會優先使用 Eden 區,若 Eden 區滿,就將物件復制到第二塊記憶體區上,然后清除 Eden 區,如果此時存活的物件太多,以至于 Survivor 不夠時,會將這些物件通過分配擔保機制復制到老年代中,(java 堆又分為新生代和老年代) -
標記-整理
該演算法主要是為了解決標記-清除,產生大量記憶體碎片的問題;當物件存活率較高時,也解決了復制演算法的效率問題,它的不同之處就是在清除物件的時候現將可回收物件移動到一端,然后清除掉端邊界以外的物件,這樣就不會產生記憶體碎片了, -
分代收集
現在的虛擬機垃圾收集大多采用這種方式,它根據物件的生存周期,將堆分為新生代和老年代,在新生代中,由于物件生存期短,每次回收都會有大量物件死去,那么這時就采用復制演算法,老年代里的物件存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除,
2.13 簡述一下標記-清除演算法
標記無用物件,然后進行清除回收,
標記-清除演算法(Mark-Sweep)是一種常見的基礎垃圾收集演算法,它將垃圾收集分為兩個階段:
- 標記階段:標記出可以回收的物件,
- 清除階段:回收被標記的物件所占用的空間,
標記-清除演算法之所以是基礎的,是因為后面講到的垃圾收集演算法都是在此演算法的基礎上進行改進的,
優點:實作簡單,不需要物件進行移動,
缺點:標記、清除程序效率低,產生大量不連續的記憶體碎片,提高了垃圾回收的頻率,
標記-清除演算法的執行的程序如下圖所示

2.14 簡述一下復制演算法
為了解決標記-清除演算法的效率不高的問題,產生了復制演算法,它把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域,垃圾收集時,遍歷當前使用的區域,把存活物件復制到另外一個區域中,最后將當前使用的區域的可回收的物件進行回收,
-
優點:
按順序分配記憶體即可,實作簡單、運行高效,不用考慮記憶體碎片, -
缺點:
可用的記憶體大小縮小為原來的一半,物件存活率高時會頻繁進行復制,
復制演算法的執行程序如下圖所示

2.15 簡述一下標記-整理演算法
在新生代中可以使用復制演算法,但是在老年代就不能選擇復制演算法了,因為老年代的物件存活率會較高,這樣會有較多的復制操作,導致效率變低,標記-清除演算法可以應用在老年代中,但是它效率不高,在記憶體回收后容易產生大量記憶體碎片,因此就出現了一種標記-整理演算法(Mark-Compact)演算法,與標記-整理演算法不同的是,在標記可回收的物件后將所有存活的物件壓縮到記憶體的一端,使他們緊湊的排列在一起,然后對端邊界以外的記憶體進行回收,回收后,已用和未用的記憶體都各自一邊,
-
優點:
解決了標記-清理演算法存在的記憶體碎片問題, -
缺點:
仍需要進行區域物件移動,一定程度上降低了效率,
標記-整理演算法的執行程序如下圖所示

2.16 簡述一下分代收集演算法
當前商業虛擬機都采用分代收集的垃圾收集演算法,分代收集演算法,顧名思義是根據物件的存活周期將記憶體劃分為幾塊,一般包括年輕代、老年代 和 永久代
如圖所示:

2.17 JVM 有哪些垃圾回收器?
如果說垃圾收集演算法是記憶體回收的方法論,那么垃圾收集器就是記憶體回收的具體實作,下圖展示了7種作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用于回收整個Java堆的G1收集器,不同收集器之間的連線表示它們可以搭配使用,

- Serial收集器(復制演算法):
新生代單執行緒收集器,標記和清理都是單執行緒,優點是簡單高效; - ParNew收集器 (復制演算法):
新生代收并行集器,實際上是Serial收集器的多執行緒版本,在多核CPU環境下有著比Serial更好的表現; - Parallel Scavenge收集器 (復制演算法):
新生代并行收集器,追求高吞吐量,高效利用 CPU,吞吐量 = 用戶執行緒時間/(用戶執行緒時間+GC執行緒時間),高吞吐量可以高效率的利用CPU時間,盡快完成程式的運算任務,適合后臺應用等對互動相應要求不高的場景; - Serial Old收集器 (標記-整理演算法):
老年代單執行緒收集器,Serial收集器的老年代版本; - Parallel Old收集器 (標記-整理演算法):
老年代并行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本; - CMS(Concurrent Mark Sweep)收集器(標記-清除演算法):
老年代并行收集器,以獲取最短回收停頓時間為目標的收集器,具有高并發、低停頓的特點,追求最短GC回收停頓時間, - G1(Garbage First)收集器 (標記-整理演算法):
Java堆并行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基于“標記-整理”演算法實作,也就是說不會產生記憶體碎片,此外,G1收集器不同于之前的收集器的一個重要特點是:G1回收的范圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的范圍僅限于新生代或老年代,
2.18 詳細介紹一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器,對于要求服務器回應速度的應用上,這種垃圾回收器非常適合,在啟動 JVM 的引數加上“-XX:+UseConcMarkSweepGC”來指定使用 CMS 垃圾回收器,
CMS 使用的是標記-清除的演算法實作的,所以在 gc 的時候回產生大量的記憶體碎片,當剩余記憶體不能滿足程式運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會采用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低,
2.19 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么區別?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
新生代垃圾回收器一般采用的是復制演算法,復制演算法的優點是效率高,缺點是記憶體利用率低;老年代回收器一般采用的是標記-整理的演算法進行垃圾回收,
2.20 簡述分代垃圾回收器是怎么作業的?
分代回收器有兩個磁區:老生代和新生代,新生代默認的空間占比總空間的 1/3,老生代的默認占比是 2/3,
新生代使用的是復制演算法,新生代里有 3 個磁區:Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1,它的執行流程如下:
- 把 Eden + From Survivor 存活的物件放入 To Survivor 區;
- 清空 Eden 和 From Survivor 磁區;
- From Survivor 和 To Survivor 磁區交換,From Survivor 變 To Survivor,To Survivor 變 From Survivor,
每次在 From Survivor 到 To Survivor 移動時都存活的物件,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級為老生代,大物件也會直接進入老生代,
老生代當空間占用到達某個值之后就會觸發全域垃圾識訓,一般使用標記整理的執行演算法,以上這些回圈往復就構成了整個分代垃圾回收的整體執行流程,
3. JVM 類加載
3.1 JVM類加載的時機?
5種場景會觸發類加載:
-
遇到
new,getstatic,putstatic或invokestatic這四條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發初始化 -
使用
java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化, -
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
-
當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個類
-
當時用,JDK1.7的動態語言支持時,如果一個
java.lang.invoke.MethodHandle實體后的決議結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄對應的類還沒有進行過初始化,則需要先觸發其初始化
3.2 JVM類加載程序?
類從被加載到虛擬機記憶體開始,到卸載出記憶體為止,整個生命周期包括:加載,驗證,準備,決議,初始化,使用和卸載
加載
加載是類加載程序的一個階段,在加載階段虛擬機需要完成三件事
- 通過一個類的全限定名來獲取定義此類的輔而進之位元組流
- 將位元組流所代表的的靜態存盤結構轉化為方法區的運行時資料結構
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口
驗證
驗證就是確保Class檔案的位元組流中包含的資訊符合當前虛擬機的要求,并且不會危害虛擬機自身的安全
- 檔案格式驗證
驗證位元組流是否符合Class檔案格式的規范 - 元資料驗證
對位元組碼描述的資訊進行語意分析(注意:對比javac編譯階段的語意分析),以保證其描述的資訊符合Java語言規范的要求 - 位元組碼驗證
通過資料流和控制流分析,確定程式語意是合法的、符合邏輯的 - 符號參考驗證
確保決議動作能正確執行,
準備
準備階段是正式為類靜態變數分配記憶體并設定類變數初始值的階段,這些變數所使用的記憶體都在方法區中進行分配
public static int value = https://www.cnblogs.com/pengfeilu/p/123 //在準備階段 value的值是 0 并不是123
public static final int value = 123 // 準備階段value 的值為123
如果屬性有Constant Value 屬性,那么在準備階段變數就會被初始化為所指定的值
這時候進行記憶體分配的僅包括類變數(static),而不包括實體變數,實體變數會在物件實體化時隨著物件一塊分配在Java堆中
這里所設定的初始值通常情況下是資料型別默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值
決議
決議階段是虛擬機將常量池內的符號參考替換為直接參考的程序
-
直接參考
直接參考可以使直接指向目標的指標,相對偏移量或是一個能間接定位到目標的句柄 -
符號參考
符號參考以一組符號來描述所參考的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可
主要包含:
-
類或介面的決議
-
欄位決議
-
類方法決議
-
介面方法決議
初始化
初始化,為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化,在Java中對類變數進行初始值設定有兩種方式:
- 宣告類變數是指定初始值
- 使用靜態代碼塊為類變數指定初始值
JVM初始化步驟:
- 假如這個類還沒有被加載和連接,則程式先加載并連接該類
- 假如該類的直接父類還沒有被初始化,則先初始化其直接父類
- 假如類中有初始化陳述句,則系統依次執行這些初始化陳述句
類的初始化
- 創建類的實體,也就是new的方式
- 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
- 呼叫類的靜態方法
- 反射(如Class.forName(“com.shengsiyuan.Test”))
- 初始化某個類的子類,則其父類也會被初始化
- Java虛擬機啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來運行某個主類
卸載
- 執行了 System.exit()方法
- 程式正常執行結束
- 程式在執行程序中遇到了例外或錯誤而例外終止
- 由于作業系統出現錯誤而導致Java虛擬機行程終止
3.3 JVM加載Class檔案的原理機制是什么?
Java中的所有類,都需要由類加載器裝載到JVM中才能運行,類加載器本身也是一個類,而它的作業就是把class檔案從硬碟讀取到記憶體中,在寫程式的時候,我們幾乎不需要關心類的加載,因為這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類,
類裝載方式,有兩種 :
-
隱式裝載
程式在運行程序中當碰到通過new 等方式生成物件時,隱式呼叫類裝載器加載對應的類到jvm中, -
顯式裝載,
通過class.forname()等方法,顯式加載需要的類
Java類的加載是動態的,它并不會一次性將所有類全部加載后再運行,而是保證程式運行的基礎類(像是基類)完全加載到jvm中,至于其他類,則在需要的時候才加載,這當然就是為了節省記憶體開銷,
3.4 什么是類加載器,類加載器有哪些?
實作通過類的權限定名獲取該類的二進制位元組流的代碼塊叫做類加載器,
主要有一下四種類加載器:
- 啟動類加載器(Bootstrap ClassLoader)用來加載java核心類別庫,無法被java程式直接參考,
- 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫,Java 虛擬機的實作會提供一個擴展庫目錄,該類加載器在此目錄里面查找并加載 Java 類,
- 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類,一般來說,Java 應用的類都是由它來完成加載的,可以通過 ClassLoader.getSystemClassLoader()來獲取它,
- 用戶自定義類加載器,通過繼承 java.lang.ClassLoader類的方式實作,
3.5 什么是雙親委派模型?
在介紹雙親委派模型之前先說下類加載器,對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立在 JVM 中的唯一性,每一個類加載器,都有一個獨立的類名稱空間,類加載器就是根據指定全限定名稱將 class 檔案加載到 JVM 記憶體,然后再轉化為 class 物件,

類加載器分類:
- 啟動類加載器(Bootstrap ClassLoader),是虛擬機自身的一部分,用來加載Java_HOME/lib/目錄中的,或者被 -Xbootclasspath 引數所指定的路徑中并且被虛擬機識別的類別庫;
- 其他類加載器:
- 擴展類加載器(Extension ClassLoader):負責加載\lib\ext目錄或Java. ext. dirs系統變數指定的路徑中的所有類別庫;
- 應用程式類加載器(Application ClassLoader),負責加載用戶類路徑(classpath)上的指定類別庫,我們可以直接使用這個類加載器,一般情況,如果我們沒有自定義類加載器默認就是用這個加載器,
雙親委派模型:如果一個類加載器收到了類加載的請求,它首先不會自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣所有的加載請求都會被傳送到頂層的啟動類加載器中,只有當父加載無法完成加載請求(它的搜索范圍中沒找到所需的類)時,子加載器才會嘗試去加載類,
當一個類收到了類加載請求時,不會自己先去加載這個類,而是將其委派給父類,由父類去加載,如果此時父類不能加載,反饋給子類,由子類去完成類的加載,
4. JVM調優
4.1 用過那些JVM 調優的工具?
常用調優工具分為兩類,jdk自帶監控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto,
- jconsole:
Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制臺,用于對JVM中記憶體,執行緒和類等的監控 - jvisualvm:
jdk自帶全能工具,可以分析記憶體快照、執行緒快照;監控記憶體變化、GC變化等, - MAT:
Memory Analyzer Tool,一個基于Eclipse的記憶體分析工具,是一個快速、功能豐富的Java heap分析工具,它可以幫助我們查找記憶體泄漏和減少記憶體消耗 - GChisto:
一款專業分析gc日志的工具
4.2 常用的 JVM 調優的引數都有哪些?
- -Xms2g:
初始化推大小為 2g; - -Xmx2g:
堆最大記憶體為 2g; - -XX:NewRatio=4:
設定年輕的和老年代的記憶體比例為 1:4; - -XX:SurvivorRatio=8:
設定新生代 Eden 和 Survivor 比例為 8:2; - –XX:+UseParNewGC:
指定使用 ParNew + Serial Old 垃圾回收器組合; - -XX:+UseParallelOldGC:
指定使用 ParNew + ParNew Old 垃圾回收器組合; - -XX:+UseConcMarkSweepGC:
指定使用 CMS + Serial Old 垃圾回收器組合; - -XX:+PrintGC:
開啟列印 gc 資訊; - -XX:+PrintGCDetails:
列印 gc 詳細資訊,
4.3 如何分析GC日志?
摘錄GC日志一部分(前部分為年輕代gc回收;后部分為full gc回收):
20xx-0x-0xT10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]
20xx-0x-0xT10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]
通過上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen屬于Parallel收集器,其中PSYoungGen表示gc回收前后年輕代的記憶體變化;ParOldGen表示gc回收前后老年代的記憶體變化;PSPermGen表示gc回收前后永久區的記憶體變化,young gc 主要是針對年輕代進行記憶體回收比較頻繁,耗時短;full gc 會對整個堆記憶體進行回城,耗時長,因此一般盡量減少full gc的次數
4.4 JVM調優命令有那些?
Sun JDK監控和故障處理命令有jps jstat jmap jhat jstack jinfo
- jps:
JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機行程, - jstat:
JVM statistics Monitoring是用于監視虛擬機運行時狀態資訊的命令,它可以顯示出虛擬機行程中的類裝載、記憶體、垃圾收集、JIT編譯等運行資料, - jmap:
JVM Memory Map命令用于生成heap dump檔案 - jhat:
JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內置了一個微型的HTTP/HTML服務器,生成dump的分析結果后,可以在瀏覽器中查看 - jstack:
用于生成java虛擬機當前時刻的執行緒快照, - jinfo:
JVM Configuration info 這個命令作用是實時查看和調整虛擬機運行引數,
4.5 你知道哪些JVM性能調優方式有那些?
-
設定堆記憶體大小
-Xmx:堆記憶體最大限制, -
設定新生代大小, 新生代不宜太小,否則會有大量物件涌入老年代
-XX:NewSize:新生代大小-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸園空間和幸存者空間的占比
-
設定垃圾回收器
年輕代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
感謝您的點贊、評論、關注;
您還可以掃碼關注“公眾號”獲取粉絲福利,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/254674.html
標籤:Java
