一文掌握 JVM 面試要點
之前發過一篇關於 JVM 面試知識點總結的文章。但是缺乏系統每個知識點的講解,於是我打算以那篇文章爲目錄根據每個知識點後面爲大家詳細講解,不過需要等喫透 MySQL 系列講解完。
今天,先公佈之前 JVM 面試總結的修訂版,並給大家預先宣傳一波,「關注公衆號,持續閱讀後續精彩好文」。
本文將作爲本專欄 「喫透 Redis 系列」 目錄,也是大廠面試標準回答,具體每個點的詳細解析會收錄於本專欄,關注【小龍 coding】,持續閱
讀後續精品文章!!
❝
本文收錄於【面試筆記】,更多付費文章,可以後臺回覆【面試筆記】獲取,【點擊此處試讀】。
❞
1、運行時數據區域
「堆」
對象實例、數組
-Xms 表示堆初始大小
-Xmx 表示堆最大大小
邏輯上連續,線程共享,虛擬機啓動時創建,最大
沒有內存完成實例分配,且無法擴展,OOM
「方法區」
存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存
被線程共享,不會頻繁 GC
「實現」:jdk7 把靜態變量和字符串常量池移到堆中,jdk8 移除永久代,把方法區移致元空間,它位於本地內存。
❝
注意:JDk6、JDk7 方法區即 PermGen(永久代),JDK8 方法區就是 MetaSpace(元空間)
❞
運行時常量池:
「Class 文件存放什麼:」
類的版本、字段、方法、接口
常量池表(Constant pool)(存放編譯期生成的各種**「字面量」**與**「符號引用」**)
-
「字面量」:字面量就是指由字母、數字等構成的字符串或者數值常量
-
「符號引用」:類和接口的全限定類名 + 字段的名稱與描述符 + 方法的名稱與描述符
int a = 1;//1、2、“abcdefg”就是字面量
int b = 2;//a b c d字段名就是符號引用,還有方法名、全限定類名等都屬於=符號引用。
String c = "abcdefg";
String d = "abcdefg";
Class 文件存放的常量池表的內容將在類加載後存放到方法區的運行時常量池(「符號引用轉爲直接引用」)
「與 Class 文件常量池區別」:動態性(可以在運行期間將常量放入池(String:intern()))
「OOM」:常量池無法申請到內存 JVM 常量池解析見下文
「Java 虛擬機棧(棧幀組成)」
局部變量表 (存放基本數據類型 + 對象引用+返回地址)
操作數棧
動態鏈接
方法出口
異常:
-
StackOverFlowError:線程請求的棧深度大於虛擬機允許的
-
OOM:棧容量動態擴展無足夠內存
-
命令參數:java -Xss2M stackjava
「本地方法棧」:
爲虛擬機使用到的 Native 方法服務
「程序計數器」:
當前執行的字節碼指令,「唯一沒有 OOM 的地方」 | 執行本地方法時本地方法計數器爲 NULL | 「線程私有」
「直接內存」
2、對象的創建五種方式
2.1、new - 構造函數
2.2、Class 類的 newInstance 方法 - 構造函數 「(相當於用無參構造」)
2.3、Constructor 類的 newInstance 方法 - 構造函數
(「bClass.getConstructors() 可以按順序獲取所有構造函數」)
2.4、反序列化
2.5、clone
「代碼:」
Constructor 相關
Class<B> bClass = B.class;
B b = bClass.newInstance();
System.out.println(b.getName());
Constructor<B> constructor[]= (Constructor<B>[]) bClass.getConstructors();
B b1 = constructor[0].newInstance("11",22);
System.out.println(b1.getName()+b1.getAge());
反序列化
//序列化過程 B需要實現序列化接口
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("b.txt"));
objectOutputStream.writeObject(new B("11",2));
objectOutputStream.close();
//反序列化
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("b.txt"));
B b = (B)objectInputStream.readObject();
System.out.println(b.getName()+b.getAge());
3、對象創建過程
1、當虛擬機遇到一條字節碼 new 指令時,首先去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,檢查這個符號引用代表的類是否被加載過,若沒有執行相關類加載過程。「// 類加載檢查」
2、類加載通過後分配內存,若堆內存規整,執行指針碰撞分配內存,否則使用空閒列表分配;「// 分配內存」
3、劃分內存還需要考慮併發問題,可以 CAS 同步處理,或則本地線程分配緩衝。「// 併發問題處理」
4、然後內存空間初始化操作(默認值,一) 些必要的對象設置(元信息、哈希碼),再行 init() 方法(按照程序員意願初始化)。
4、對象的訪問定位
「句柄:「指針的指針。堆劃分一塊內存作爲句柄池(句柄池 + 實例池),引用存儲句柄的地址,句柄中包含了」對象實例數據指針」(指向堆對象實例數據)+「對象實例類型指針」(指向方法區對象類型數據)
- 優點:穩定,對象移動時只改變句柄中對象實例數據指針,而引用本身不變指向句柄
「直接指針」:直接指向對象,保存對象內存起始地址的指針
- 優點:訪問速度快,一次定位
5、對象內存分配
「指針碰撞」
堆內存規整,將堆分爲空閒和使用過兩部分,空閒的放一邊,用過的放一邊,中間放一個指針指向分界處,分配對象內存時就將指針向空閒部分移動相應大小
「空閒列表」
堆內存不規整,需藉助列表存放可用空間,分配對象內存時查看列表找到足夠的空間分配給對象,並更新列表
6、對象併發安全問題
「分配內存需考慮併發問題」:可能正在給對象 A 分配內存,指針還沒來得及修改,對象 B 又同時使用了原來的指針來分配內存的情況
「CAS + 失敗重試保證更新原子性」:對分配內存空間動作進行同步處理
「本地線程分配緩衝」:每個線程在 Java 堆中預先分配一小塊內存(本地線程分配緩衝)哪個線程要分配內存,就在哪個線程的本地緩衝區中分配,本地緩衝區用完了分配新的緩衝區才需要同步鎖定
7、對象內存佈局
java 對象 = 對象頭 + 實例數據 + 對齊填充
對象頭 = Mark Word+ 對象所屬類 的指針組成 (如果是數組對象,還會包含長度)
Mark Word = 存儲對象自身的運行時數據,例如 hashCode,GC 分代年齡,鎖狀態標誌,線程持有的鎖等等
8、OOM 異常
JVM 堆,無法給實例分配內存,且無法擴展時,OOM。
方法區(以及運行時常量池)無法滿足內存分配需求時,OOM.
Java 虛擬機棧 + 本地方法棧,擴展時無法申請到足夠內存,OOM(線程請求深度超過 JVM 允許棧深度,StackOverflowException).
9、內存泄漏與內存溢出
無用內存得不到釋放。程序申請了內存,使用完後又不能歸還 JVM,造成內存泄漏,內存泄漏多了就造成內存溢出。內存溢出——》
OOM(內存滿了,沒有內存給實例分配空間,且無法擴展)
Student stu=new Student();
List<Student> stus=new ArrayList<>();
stus.add(stu);
stu=null;
stu佔用的內存得不到釋放,stus佔用着student. 發生內存泄漏
10、判斷對象是否是垃圾
「引用計數器法:」
對象添加一個引用計數器,每當有個引用就加一,引用失效減一,當爲減爲 0 對象不可用
缺點:相互引用,A<-->B,然後 A、B 已經沒有被其他有用對象引用,本視爲垃圾,但是由於互相引用不能被檢查回收。
「可達性分析法:」
從 GC Roots 到該對象可達
11、GC Roots
-
Java 虛擬機棧(棧幀中的本變量表)中引用的對象
-
方法區 「常量、靜態變量」 引用的對象
-
本地方法棧 JNI 引用對象
-
所有被同步鎖持有的對象
總結記憶口訣:兩棧一方法
局部變量表:存放方法參數和方法內部定義的局部變量
12、四種引用狀態(強軟弱虛)
「強引用」
Object obj=new Object(); StronglyReference ——不會被回收
「軟引用」
new SoftReference(obj); 描述有用但非必須的對象——內存不夠回收
「弱引用」
WeakReference 弱引用一定會被回收,下一次 GC 回收
「虛引用」
PhantomReference 爲了能在這個對象被收集器回收時收到一個系統通知
13、方法區的回收
「廢棄的常量」
沒有被引用
如:字面量回收
❝
一個字符串 “abc” 放入常量池, 現在沒有一個值爲 "abc" 的字符串對象,也就是 沒有任何字符串對象引用常量池中 “abc” 的常量,且虛擬機其他地方沒有引用這個字面量,如果發生垃圾回收且有必要時,常量會被系統清理出常量池 String s1="abc"; Strig s2=new String("abc"); 其他接口,方法,字段,符號引用類似
❞
「無用類的卸載」
-
該類的所有實例被回收,堆中不存在該類及其派生子類的實例
-
加載該類的 CassLoder 被回收
-
該類對應的 java.lang.Class 對象沒有在任何地方被引用
14、垃圾收集算法
「標記 - 清除」
-
先標記後清除,先標記垃圾對象,然後統一回收垃圾對象
-
缺點:要標記和清除,效率不高,還容易出現內存碎片化
「複製算法」(適用新生代—存活率低)
-
「對象存活率高時會有大量複製,效率低」 ,老年代存活率高,不適用 |(分配擔保)
-
將內存容量分爲大小相等兩部分,先使用一塊內存,用完了將還存活的對象複製到另一塊內存上。由於內存被分爲兩部分,使得 「只能用一半內存」
「標記 - 整理」
- 前面和標記清除一樣,先標記,但是不會立刻清除,先把 「存活的對象都移到一端」,然後直接清除掉邊界以外的對象。不會出現碎片化。
「分代收集」
- 堆分爲年輕代老年代,新生代存活率低使用複製算法,老年代存活率高使用標記清除 / 標記整理。
15、垃圾收集器
「注重低延遲」
「CMS」:基於標記清除的併發垃圾收集器
-
初始標記:標記 GC Roots 直達的對象(「stw」)
-
併發標記:跟蹤標記 GC Roots 所有可達對象
-
重新標記:重新標記那些由於併發標記中用戶程序跟到執行導致標記發生變化的對象(「stw」)
-
併發清除:清除標記垃圾
「優點」:支持併發,停頓時間短
「缺點」:使用標記清除算法,空間碎片。併發標記產生浮動垃圾。
「G1」:併發 + 並行(重新標記 + 篩選回收)
「弱化分代(老年代與年輕代一起回收),引入分區。將堆分爲多個大小相等區域分而治之。」
-
初始標記:標記 GC Roots 直達的對象,並且修改 TAMS(Next Top Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可以用的 Region 中創建新對象,「需要停頓線程,但耗時很短」
-
併發標記:跟蹤標記 GC Roots 所有可達對象
-
最終標記:修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄, 對象變化記錄在線程 Remenbered Set Logs 裏面,最終標記階段需要把 Remembered Set Logs 的數據合併到 Remembered Set 中,這階段 「需要停頓線程」,但可並行執行
-
篩選回收:對各個 region 區域進行回收價值與成本的排序,根據用戶期望的 GC 停頓時間來執行計劃(最少時間回收最多垃圾區域,停頓用戶線程)
「特點:」
「空間整合:「整體來看是基於 “標記 - 整理” 算法實現的收集器,從局部(兩個 Region 之間)上來看是基於 “複製” 算法實現的,這意味着運行期間不」會產生內存空間碎片」。
「可預測的停頓」:能讓使用者明確指定在一個長度爲 M 毫秒的時間片段內,消耗在 GC 上的時間不得超過 N 毫秒。
16、G1 與 CMS 區別
-
使用範圍:CMS 使用在老年代,G1 收集範圍新生代與老年代
-
STW 的時間:CMS 注重低延遲,G1 可預測的停頓
-
垃圾碎片:CMS 使用標記清除算法,造成內存空間碎片;G1 進行空間整合使用標記 - 整理,不會有內存空間碎片
-
垃圾回收過程
-
使用場景
「stw」:垃圾回收,暫停所用用戶線程執行,避免垃圾回收時產生新垃圾。
17、類加載過程
-
「加載」:根據類的全限定類名獲取二進制字節流,將字節流代表的靜態存儲結構 -> 運行時存儲結構,在內存生成 「Class 對象」,作爲方法區這個類數據訪問入口
-
「驗證」:檢驗加載的 class 文件正確性(修飾符、權限、)
-
「準備」:爲類的**「靜態變量」**分配內存,並賦默認值
-
「解析」:將常量池中符號引用轉爲直接引用(class 文件常量池轉至方法區運行時常量池)
-
「初始化」:(針對類變量初始化)對靜態變量和靜態代碼塊執行初始化工作
18、類加載器
-
啓動類加載器:加載核心類庫(JAVA_HOME/lib 如 rt.jar)
-
擴展類加載器:加載擴展類(JAVA_HOME/jre/lib/ext)
-
系統類加載器:加載用戶類路徑 ClassPath 下的類
-
用戶自定義加載器:繼承 java.lang.ClassLoader
19、類加載方式
-
隱式加載:當碰到通過 new 等方式生成對象時,隱式調用類裝載器加載對應的類到 jvm 中
-
顯示加載:通過 class.forname() 等方法,顯式加載需要的類
20、雙親委派模型
含義:類加載請求來了,類加載器自己先不加載,先讓父類加載器加載,父類不行自己再來
「怎樣打破雙親委派機制:」
-
自定義類加載器,重寫 loadClass 方法
-
線程上下文類加載器
Java 涉及 SPI 機制的都用線程上下文類加載器。父類加載器請求子類加載器完成加載動作
SPI 機制:爲接口找尋服務(jdbc)
❝
SPI 約定:服務提供者爲接口提供接口實現後,會在 jar 包的 META-INF/service / 目錄下創建一個以服務接口命名的文件。
❞
JDBC4.0 使用 SPI 機制,DriverManager 需要去 jar 包下的 META-INF/services/java.sql.Driver 目錄下去尋找對應的 Driver 加載,但是
DriverManager 在 rt.jar 中,使用啓動類加載器(BootStrapClassLoader), 它需要調用服務提供者放在 classpath 下的類,啓動類加載器無
法加載,就只得使用線程上下文加載器,讓父類加載器調用子類加載器完成,打破了雙親委派機制。
「好處」:避免類重複加載 + 防止核心類篡改 + 安全性
21、GC
Minor GC:回收年輕代
Major GC :回收老年代
Full GC:回收年輕代與老年代,方法區域
22、內存分配與回收策略
「對象優先在 Eden 區分配」
大多數情況下,對象在新生代 Eden 上分配,當 Eden 空間不夠時,發起 Minor GC。
「大對象直接進入老年代」
大對象指 「需要連續分配空間」 的對象,長字符串,數組。
對象過大,由於需要連續的內存空間,會導致提前進行垃圾回收以獲取足夠的連續空間 -XX:PretenureSizeThreshold,大於此值的對象直接在老年代分配,避免在 Eden 和 Survivor 之間的大量內存複製。
「長期存活的對象」
對象在 Eden 區出生,經過一次 Minor GC 存活下來,分代年齡就會加一,增加到一定年齡就會移向老年代。默認是 15. -XX:MaxTenuringThreshold 用來定義該年齡的閾值。
「動態對象年齡判定」
虛擬機並不是永遠要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升到老年代,當 「Survivor 區相同年齡的所有對象大小總和大於 Survivork 空間一半」,則年齡大於或等於該年齡的對象直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。
「空間分配擔保 (*)」
在發生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果條件成立的話,那麼 Minor GC 可以確認是安全的。
如果不成立的話虛擬機會查看 HandlePromotionFailure 的值是否允許擔保失敗,如果允許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小。
如果大於,將嘗試着進行一次 Minor GC;如果小於,或者 HandlePromotionFailure 的值不允許冒險,那麼就要進行一次 Full GC。
23、分代垃圾收集器是怎樣工作的
「分代回收器有兩個分區」:老生代和新生代,新生代默認的空間佔比總空間的 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)時,升級爲老生代。「大對象也會直接進入老生代。」
老生代當空間佔用到達某個值之後就會觸發全局垃圾收回,一般使用標記整理的執行算法。以上這些循環往復就構成了整個分代垃圾回收的整體執行流程。
24、Full GC 觸發條件
對於 Minor GC,其觸發條件非常簡單,當 Eden 空間滿時,就將觸發一次 Minor GC。而 Full GC 則相對複雜,有以下條件:
「調用 System.gc()」
只是建議虛擬機執行 Full GC,但是虛擬機不一定真正去執行。不建議使用這種方式,而是讓虛擬機管理內存。
「老年代空間不足」
「場景:」
大對象直接進入老年代(老年代空間足,但是沒有足夠的連續空間) 長期存活的對象直接進入老年代
「解決:」
1、儘量 「不要創建過大的對象」 以及數組 2、可以通過 - Xmn 「調大新生代大小」 ,讓 「對象儘量在新生代被回收」,不進老年代 3、可以通過 - XX:MaxTenuringThreshold 「調大分代年齡閾值」,讓對象得新生代多存活一段時間
「空間分配擔保失敗」
使用複製算法的 Minor GC 需要老年代的內存空間作擔保,如果擔保失敗會執行一次 Full GC。
「解釋一」
老年代最大可用的連續空間 < 新生代所有對象總空間 && HandlerPromotionFailure 設置不允許擔保失敗 full gc
老年代最大可用的連續空間 > 新生代所有對象總空間 && HandlerPromotionFailure 設置允許擔保失敗 && 通過 Minor GCJ 進入老年代的象平均大小 > 老年代最大連續空間大小 full gc
「解釋二」
Minor GC 前,先判斷老年代最大連續空間是否大於新生代所有對象總空間。
若大於,安全;若小於,查看 HandlerPromotionFailure 設置是否允許擔保失敗,允許,再看通過 Minor GCJ 進入老年代的對象平均大小 > 老年代最大連續
空間太小,則還是失敗,進行 Full GC。
「jdk1.7 以前的永久代空間不足」
永久代可能會被佔滿,在未配置爲採用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那麼虛擬機會拋出 java.lang.OutOfMemoryError。
爲避免以上原因引起的 Full GC,可採用的方法爲增大永久代空間或轉爲使用 CMS GC。
25、爲什麼有垃圾收集還會有內存泄漏問題?
「對象定義在錯誤的範圍」 如果長生命週期的對象持有短生命週期的引用,就很可能會出現內存泄露。「異常處理不當」 各種資源的關閉一定要放在 finally 裏面
26、堆與棧的區別?
「申請方式」:棧系統自動申請,堆需手動申請 c 語言 malloc(),java new Object();
棧系統分配速度快,堆慢,容易內部碎片;棧地址空間連續,堆是不連續的(鏈表存儲空閒內存地址);
「內容不一樣」(堆存對象實例與數組,關注存儲;棧存儲局部變量表,操作數棧,關注運行);
「大下限制」(棧預先設定好的,編譯器即可確定,堆取決有效虛擬內存,運行期間確定)
27、逃逸分析
概念: 當一個對象在方法中被定義後,它可能被方法外部其他對象所引用,則稱逃出方法(內存逃逸現象)
使用逃逸分析,編譯器優化
-
同步省略:對象沒有方法逃逸,只能被一個線程訪問到,可以不用同步
-
「將堆分配轉爲棧分配」
如果 JIT 經過逃逸分析,發現有些對象沒有逃逸出方法,那麼有可能堆內存分配會被優化成棧內存分配
28、JVM 參數
-
-Xmx3550:設置堆最大值
-
-Xms3660m:設置初始堆大小
-
-Xss128k:設置線程棧大小
-
-Xmn2g:設置年輕代大小
-
-XX:NewSize=1024m:設置年輕代初始值
-
-XX:MaxNewSize=1024m:設置年輕代最大值
-
-XX:SurvivorRatio=4:設置 Survivor 區與 Eden 區比值
-
-XX:MaxTenuringThreshold=15:設置分代年齡閾值,滿 15 就進入老年代
-
-XX:PretenureSizeThreshold:大對象直接進入老年代
29、內存持續上升,我該如何處理
1、啓動程序之前通過 HeapDumpOnOutOfMemoryError 和 HeapDumpPath 這兩個參數 「開啓堆內存異常日誌」
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=
2、從日誌從發現異常
3、通過 top 命令查看進程 cpu 使用率
4、再通過 top -Hp pid 查看進程下所有 「具體線程佔用系統資源情況」。
5、再通過 jstack pid 查看具體線程的 「堆棧信息」 (線程 ID、狀態(wait,sleep), 是否持有鎖)
6、再通過 jmap 查看 「堆內存的使用情況」 jmap -heap pid
7、通過以上命令分析基本可以看出什麼問題導致內存上升,現在分析問題產生的原因
8、我們在啓動時,已經設置了 dump 文件,通過 MAT 打開 dump 的內存日誌文件,分析即可。
須知
30、JVM 性能調優與故障處理
31、基本故障處理工具
32、可視化故障處理工具
最後三節,屬於進階內容,前面基礎一定要掌握好。由於篇幅有限,關注公衆號,後期會專門針對大廠面試常問性能調優與工具進行講解。
後記
關注我公衆號 “小龍 coding”,我們一起探討,幫助修改簡歷,回答疑問,項目分析,只爲幫助迷茫的你高效斬獲心儀 offer!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/S5Isd3E4CHnaSPTkbyRrQQ