前言:

你可能花過很多精力學習 JVM 的知識,但在面對真實生產環境產生的問題,依舊會束手無策:
- 正在運行的 Java 程式,突然 OOM
- 程式例外卡頓,CPU 瘋狂運轉,GC 時間飆升
- 面對一堆 JVM 引數無從下手,甚至錯誤配置某個引數而產生負面效果
- 一頭霧水,不知如何了解線上應用的垃圾回收狀況
不論是在問題現場還是跳槽面試,面對這些問題,如何快速定位和解決問題,需要你對 Java虛擬機的實作和優化,有極為深刻的理解,所以我在這里整理了一下 JVM 的知識點,內容有點長,但是都是十足的干貨,請各位看官耐心批閱!
1.什么是 Java 虛擬機?為什么 Java 被稱作是“平臺無關的編程語言”?
Java 虛擬機是一個可以執行 Java 位元組碼的虛擬機行程,Java 源檔案被編譯成能被 Java 虛擬機執行的位元組碼檔案,Java 被設計成允許應用程式可以運行在任意的平臺,而不需要程式員為每一個平臺單獨重寫或者是重新編譯,Java 虛擬機讓這個變為可能,因為它知道底層硬體平臺的指令長度和其他特性,
2.Java 記憶體結構?

方法區和對是所有執行緒共享的記憶體區域;而 java 堆疊、本地方法堆疊和程式員計數器是運行是執行緒私有的記憶體區域,
3.記憶體模型以及磁區,需要詳細到每個區放什么?
JVM 分為堆區和堆疊區,還有方法區,初始化的物件放在堆里面,參考放在堆疊里面,class 類資訊常量池(static 常量和 static 變數)等放在方法區, new:
- 方法區:主要是存盤類資訊,常量池(static 常量和 static 變數),編譯后的代碼(位元組碼)等資料
- 堆:初始化的物件,成員變數 (那種非 static 的變數),所有的物件實體和陣列都要在堆上分配
- 堆疊:堆疊的結構是堆疊幀組成的,呼叫一個方法就壓入一幀,幀上面存盤區域變數表,運算元堆疊,方法出口等資訊,區域變數表存放的是 8大基礎型別加上一個應用型別,所以還是一個指向地址的指標
- 本地方法堆疊:主要為 Native 方法服務
- 程式計數器:記錄當前執行緒執行的行號
另外本人整理收藏了20年多家公司面試知識點整理 ,以及各種Java核心知識點免費分享給大家,想要資料的話請點795983544暗號CSDN,

4.堆里面的磁區:Eden,survival (from+ to),老年代,各自的特點?
堆里面分為新生代和老生代(java8 取消了永久代,采用了 Metaspace),新生代包含 Eden+Survivor 區,survivor 區里面分為 from 和 to 區,記憶體回收時,如果用的是復制演算法,從 from 復制到 to,當經過一次或者多次 GC 之后,存活下來的物件會被移動到老年區,當 JVM 記憶體不夠用的時候,會觸發 Full GC,清理 JVM 老年區當新生區滿了之后會觸發 YGC,先把存活的物件放到其中一個 Survice 區,然后進行垃圾清理,
因為如果僅僅清理需要洗掉的物件,這樣會導致記憶體碎片,因此一般會把 Eden 進行完全的清理,然后整理記憶體,
那么下次 GC 的時候,就會使用下一個 Survive,這樣回圈使用,
如果有特別大的物件,新生代放不下,就會使用老年代的擔保,直接放到老年代里面,
因為 JVM 認為,一般大物件的存活時間一般比較久遠,
5 .解釋記憶體中的堆疊(stack)、堆(heap)和方法區(method area)的用法
通常我們定義一個基本資料型別的變數,一個物件的參考,還有就是函式呼叫的現場保存都使用 JVM 中的堆疊空間;而通過 new 關鍵字和構造器創建的物件則放在堆空間,堆是垃圾收集器管理的主要區域,由于現在的垃圾收集器都采用分代收集演算法,所以堆空間還可以細分為新生代和老生代,再具體一點可以分為 Eden、Survivor(又可分為 From Survivor 和 To Survivor)、Tenured;方法區和堆都是各個執行緒共享的記憶體區域,用于存盤已經被 JVM 加載的類資訊、常量、靜態變數、JIT 編譯器編譯后的代碼等資料;程式中的字面量(literal)如直接書寫的 100、”hello”和常量都是放在常量池中,常量池是方法區的一部分,,堆疊空間操作起來最快但是堆疊很小,通常大量的物件都是放在堆空間,堆疊和堆的大小都可以通過 JVM 的啟動引數來進行調整,堆疊空間用光了會引發 StackOverflowError,而堆和常量池空間不足則會引發 OutOfMemoryError,
String str = new String("hello");
上面的陳述句中變數 str 放在堆疊上,用 new 創建出來的字串物件放在堆上,而”hello”這個字面量是放在方法區的,
補充 1:較新版本的 Java(從 Java 6 的某個更新開始)中,由于 JIT 編譯器的發展和”逃逸分析”技術的逐漸成熟,堆疊上分配、標量替換等優化技術使得物件一定分配在堆上這件事情已經變得不那么絕對了,
補充 2:運行時常量池相當于 Class 檔案常量池具有動態性,Java 語言并不要求常量一定只有編譯期間才能產生,運行期間也可以將新的常量放入池中,String 類的 intern()方法就是這樣的,看看下面代碼的執行結果是什么并且比較一下 Java 7 以前和以后的運行結果是否一致,
String s1 = new StringBuilder("go")
.append("od").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja")
.append("va").toString();
System.out.println(s2.intern() == s2);
6.GC 的兩種判定方法?
參考計數法:指的是如果某個地方參考了這個物件就+1,如果失效了就-1,當為 0 就 會回收但是 JVM 沒有用這種方式,因為無法判定相互回圈參考(A 參考 B,B 參考 A) 的情況,
參考鏈法:通過一種 GC ROOT 的物件(方法區中靜態變數參考的物件等-static 變 量)來判斷,如果有一條鏈能夠到達 GC ROOT 就說明,不能到達 GC ROOT 就說明 可以回收
7.SafePoint 是什么?
比如 GC 的時候必須要等到 Java 執行緒都進入到 safepoint 的時候 VMThread 才能開始執行 GC
1.回圈的末尾 (防止大回圈的時候一直不進入 safepoint,而其他執行緒在等待它進入 safepoint)
2.方法回傳前
3.呼叫方法的 call 之后
4.拋出例外的位置
8.GC 的三種收集方法:標記清除、標記整理、復制演算法的原理與特點,分別用在什么地方,如果讓你優化收集方法,有什么思路?
先標記,標記完畢之后再清除,效率不高,會產生碎片
復制演算法:分為 8:1 的 Eden 區和 survivor 區,就是上面談到的 YGC
標記整理:標記完畢之后,讓所有存活的物件向一端移動
9.GC 收集器有哪些?CMS 收集器與 G1 收集器的特點?
并行收集器:串行收集器使用一個單獨的執行緒進行收集,GC 時服務有停頓時間
串行收集器:次要回收中使用多執行緒來執行
CMS 收集器是基于“標記—清除”演算法實作的,經過多次標記才會被清除
G1 從整體來看是基于“標記—整理”演算法實作的收集器,從區域(兩個 Region 之間)上來看是基于“復制”演算法實作的
10.Minor GC 與 Full GC 分別在什么時候發生?
新生代記憶體不夠用時候發生 MGC 也叫 YGC,JVM 記憶體不夠的時候發生 FGC
11. 幾種常用的記憶體除錯工具:jmap、jstack、jconsole、jhat?
jstack 可以看當前堆疊的情況,jmap 查看記憶體,jhat 進行 dump 堆的資訊 mat(eclipse 的也要了解一下)
12.什么是類的加載
類的加載指的是將類的.class 檔案中的二進制資料讀入到記憶體中,將其放在運行時資料區的方法區內,然后在堆區創建一個 java.lang.Class 物件,用來封裝類在方法區內的資料結構,類的加載的最終產品是位于堆區中的 Class 物件,Class 物件封裝了類在方法區內的資料結構,并且向 Java 程式員提供了訪問方法區內的資料結構的介面,
13.類加載器

- 啟動類加載器:Bootstrap ClassLoader,負責加載存放在 JDK\jre\lib(JDK 代表 JDK
的安裝目錄,下同)下,或被-Xbootclasspath 引數指定的路徑中的,并且能被虛擬機識別的類別庫 - 擴展類加載:Extension ClassLoader,該加載器由 sun.misc.Launcher$ExtClassLoader
實作,它負責加載 DK\jre\lib\ext 目錄中,或者由 java.ext.dirs 系統變數指定的路徑中的所有類別庫(如javax.*開頭的類),開發者可以直接使用擴展類加載器, - 應用程式類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader
來實作,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器
14.Java 物件創建程序
1.JVM 遇到一條新建物件的指令時首先去檢查這個指令的引數是否能在常量池中定義到一個類的符號參考,然后加載這個類(類加載程序在后邊講)
2.為物件分配記憶體,一種辦法“指標碰撞”、一種辦法“空閑串列”,最終常用的辦法“本地執行緒緩沖分配(TLAB)”
3.將除物件頭外的物件記憶體空間初始化為 0
4.對物件頭進行必要設定
15.類的生命周期
類的生命周期包括這幾個部分,加載、連接、初始化、使用和卸載,其中前三部是類的加載的程序,如下圖;

java 類加載需要經歷以下 幾個程序:
- 加載
加載時類加載的第一個程序,在這個階段,將完成以下三件事情:
1.通過一個類的全限定名獲取該類的二進制流,
2.將該二進制流中的靜態存盤結構轉化為方法去運行時資料結構,
3.在記憶體中生成該類的 Class 物件,作為該類的資料訪問入口,
- 驗證
驗證的目的是為了確保 Class 檔案的位元組流中的資訊不回危害到虛擬機.在該階段主要完成以下四鐘驗證:
檔案格式驗證:驗證位元組流是否符合 Class 檔案的規范,如主次版本號是否在當前虛擬機范圍內,常量池中的常量是否有不被支持的型別.
元資料驗證:對位元組碼描述的資訊進行語意分析,如這個類是否有父類,是否集成了不被繼承的類等,
位元組碼驗證:是整個驗證程序中最復雜的一個階段,通過驗證資料流和控制流的分析,確定程式語意是否正確,主要針對方法體的驗證, 如: 方法中的型別轉換是否正確,跳轉指令是否正確等,
符號參考驗證: 這個動作在后面的決議程序中發生,主要是為了確保決議動作能正確執行,
- 準備
準備階段是為類的靜態變數分配記憶體并將其初始化為默認值,這些記憶體都將在方法區中進行分配,準備階段不分配類中的實體變數的記憶體,實體變數將會在物件實體化時隨著物件一起分配在 Java 堆中,
public static int value=123;
//在準備階段 value 初始值為 0 ,
在初始化階段才會變為 123,
-
決議
該階段主要完成符號參考到直接參考的轉換動作,決議動作并不一定在初始化動作完成之前,也有可能在初始化之后, -
初始化
初始化時類加載的最后一步,前面的類加載程序,除了在加載階段用戶應用程式可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制, 到了初始化階段,才真正開始執行類中定義的 Java 程式,
16.簡述 java 類加載機制?
虛擬機把描述類的資料從 Class 檔案加載到記憶體,并對資料進行校驗,決議和初始化,最終形成可以被虛擬機直接使用的 java 型別,
17.Java 物件結構
Java 物件由三個部分組成:物件頭、實體資料、對齊填充,
物件頭由兩部分組成,第一部分存盤物件自身的運行時資料:哈希碼、GC 分代年齡、鎖標識狀態、執行緒持有的鎖、偏向執行緒 ID(一般占 32/64 bit),
第二部分是指標型別,指向物件的類元資料型別(即物件代表哪個類),如果是陣列物件,則物件頭中還有一部分用來記錄陣列長度,
實體資料用來存盤物件真正的有效資訊(包括父類繼承下來的和自己定義的)
對齊填充:JVM 要求物件起始地址必須是 8 位元組的整數倍(8 位元組對齊)
18.如和判斷一個物件是否存活?(或者 GC 物件的判定方法)
判斷一個物件是否存活有兩種方法:
1.參考計數法
所謂參考計數法就是給每一個物件設定一個參考計數器,每當有一個地方參考這個物件時,就將計數器加一,參考失效時,計數器就減一,當一個物件的參考計數器為零時,說明此物件沒有被參考,也就是“死物件”,將會被垃圾回收, 參考計數法有一個缺陷就是無法解決回圈參考問題,也就是說當物件 A 參考物件 B,物件 B 又參考者物件 A,那么此時 A,B 物件的參考計數器都不為零,也就造成無法完成垃圾回收,所以主流的虛擬機都沒有采用這種演算法,
2.可達性演算法(參考鏈法)
該演算法的思想是:從一個被稱為 GC Roots 的物件開始向下搜索,如果一個物件到 GC Roots 沒有任何參考鏈相連時,則說明此物件不可用,
在 java 中可以作為 GC Roots 的物件有以下幾種:
-
虛擬機堆疊中參考的物件
-
方法區類靜態屬性參考的物件
-
方法區常量池參考的物件
-
本地方法堆疊 JNI 參考的物件
雖然這些演算法可以判定一個物件是否能被回收,但是當滿足上述條件時,一個物件比不一定會被回收,當一個物件不可達 GC Root 時,這個物件并不會立馬被回收,而是出于一個死緩的階段,若要被真正的回收需要經歷兩次標記如果物件在可達性分析中沒有與 GC Root 的參考鏈,那么此時就會被第一次標記并且進行一次篩選,篩選的條件是是否有必要執行 finalize()方法,當物件沒有覆寫 finalize()方法或者已被虛擬機呼叫過,那么就認為是沒必要的,
如果該物件有必要執行 finalize()方法,那么這個物件將會放在一個稱為 F-Queue 的對佇列中,虛擬機會觸發一個 Finalize()執行緒去執行,此執行緒是低優先級的,并且虛擬機不會承諾一直等待它運行完,這是因為如果 finalize()執行緩慢或者發生了死鎖,那么就會造成 FQueue 佇列一直等待,造成了記憶體回收系統的崩潰, GC 對處于 F-Queue 中的物件進行第二次被標記,這時,該物件將被移除”即將回收”集合,等待回收,
19.JVM 的永久代中會發生垃圾回收么?
垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC),如果你仔細查看垃圾收集器的輸出資訊,就會發現永久代也是被回收的,這就是為什么正確的永久代大小對避免 Full GC 是非常重要的原因,請參考下 Java8:從永久代到元資料區 (注:Java8 中已經移除了永久代,新加了一個叫做元資料區的 native 記憶體區)
20.簡述 java 記憶體分配與回收策率以及 Minor GC 和 Major GC?
1.物件優先在堆的 Eden 區分配,
2.大物件直接進入老年代.
3.長期存活的物件將直接進入老年代.,當 Eden 區沒有足夠的空間進行分配時,虛擬機會執行一次 Minor GC.Minor Gc 通常發生在新生代的 Eden 區,在這個區的物件生存期短,往往發生 Gc 的頻率較高, 回收速度比較快;Full Gc/Major GC 發生在老年代,一般情況下,觸發老年代 GC 的時候不會觸發 Minor GC,但是通過配置,可以在 Full GC 之前進行一次 MinorGC 這樣可以加快老年代的回收速度,
總結:
JVM在一些互聯網大廠是面試必問的一個技術點,所以在面試時一定要注重重點,想一些高并發高可用的技術,面試時要掌握節奏,說一些讓面試官眼前一亮的技術,有些基礎的東西能少說就少說,畢竟面試官面了這么多早就聽夠了,越是稀少的越是能激發面試官的興趣,然后掌握在自己的節奏中,
另外本人整理收藏了20年多家公司面試知識點整理 ,以及各種Java核心知識點免費分享給大家,想要資料的話請點795983544暗號CSDN,


轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/45799.html
標籤:其他
下一篇:多執行緒小案例
