JVM面试题与答案
JVM內存布局
JVM在內存布局上可以分為哪些區域?
- 堆(線程共享):GC的主要回收地,包含幾乎所有的實例對象、字符串常量池;
- 元空間(線程共享):在本地內存分配,包含類元信息、字段、靜態屬性、方法、常量等;
- 虛擬機棧(線程私有):是描述Java方法執行的內存區域;
- 本地方法棧(線程私有):本地方法棧為Native方法服務,線程調用本地方法時,會進入一個不再受JVM約束的世界;
- 程序計數器(線程私有):程序計數器用來存放執行指令的偏移量和行號指示器等,線程執行或恢復都要依賴程序計數器。
垃圾回收
什么樣的對象會被回收?
- 通過一系列稱為GC Roots 的對象作為起始點,從這些節點開始向下搜索,搜索所走的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,該對象將被回收,這個過程又稱為可達性分析算法。
- 簡單來說就是,沒有被GC Roots引用的對象會被回收。
什么是GC Roots?
- 類靜態屬性或常量中引用的對象;
- 虛擬機棧中引用的對象;
- 本地方法棧中引用的對象。
什么是Stop The World?
- 可達性分析的執行需要一個確保一致性的快照下進行,即對象引用關系達到穩定,因此必須停頓所有java線程,這種操作稱為Stop The World,簡稱 STW。
什么是安全點和安全區域?
- Stop The World 需要java線程到達安全點或安全區域后執行;
- 安全點(Safe Point)可以是方法調用、循環跳轉、異常跳轉等;
- 安全區域指一段代碼片段中,引用關系不會發生變化。
被GC Roots引用的對象就一定不會回收嗎?
- 不一定
- 強引用:始終不會被回收;
- 軟引用:會在OOM前被回收;
- 弱引用:會在下一次YGC被回收;
- 虛引用:無法通過該引用指向對象,為一個對象設置虛引用的唯一目的就是希望能在這個對象被回收時收到一個系統通知。
常見的GC回收算法有哪些?
- 復制算法:將內存分為大小相等的兩塊,每次只使用其中一塊,回收完把存活的復制到另一塊,當前區域清空,這樣回收的效率會很高,不會產生空間碎片,缺點是內存的使用上限變低了,而且不適合存放長期生存的對象,適合新生代;
- 標記-清除:先根據GC Roots標記可達對象,再清除不可達的對象,缺點是回收的效率比復制算法低,回收完會產生內存空間碎片;
- 標記-整理:在標記清除基礎上增加了對象整理過程,避免空間碎片,適合老年代使用。
一個對象從出生到被回收的過程是什么?
- 絕大部分對象在Eden區生成,當Eden區滿了的時候,會觸發YGC。沒有被引用的對象直接回收,依然存活的對象被移送到Survivor區。
- Survivor區分為S0和S1兩塊內存空間,每次YGC時,它們將存活的對象復制到未使用的那塊空間,然后將當前正在使用的空間完全清除,交換兩塊空間的使用狀態。另外每次GC后,存活的對象年齡會+1,當達到一個閾值的時候直接移至老年代,這個閾值默認是15。
- 如果YGC要移送的對象大于Survivor區容量的上限,則直接移至老年代。如果老年代也無法放下,則會觸發FGC。如果依然無法放下,則拋出OOM。
CMS的工作原理是什么?
- cms的特點
- 針對老年代;
- 采用標記清除算法(為了節省時間,不進行整理,會產生內存碎片,可配置成周期性整理);
- 并發收集,低停頓;
- 以獲得最短停頓時間為目標。
- 回收過程
- 初始標記:僅標記一下GC Roots能直接關聯到的對象,不用向下追溯,速度很快,但需要STW;
- 并發標記:進行GC Roots Tracing的過程, 這個過程相對耗時,但卻可以和用戶線程并行;
- 重新標記:將并發標記期間發生變化的對象進行重新標記,需要STW;
- 并發清除:回收所有的垃圾對象,整個過程中耗時最長的并發標記和并發清除都可以與用戶線程一起工作。
G1的工作原理是什么?
- g1的特點
- 并行與并發:STW時間短。
- 分代收集,收集范圍包括新生代和老年代 ,而不需要與其他收集器搭配。將整個堆劃分為多個大小相等的獨立區域(Region),新生代和老年代不再是物理隔離,它們都是一部分Region的集合。
- 結合多種垃圾收集算法,空間整合,不產生碎片。從整體看,是基于標記-整理算法,從局部(兩個Region間)看,是基于復制算法。
- 可局部收集,可預測停頓:每個Region都一個Remembered Set,記錄引用其它區域對象的指針,這樣便可以局部回收避免全局掃描。因此可以做到可預測停頓的時間。
- 垃圾最多的Region,會被優先收集,這也是 G1 名字的由來。
- 回收過程
- 初始標記:僅標記一下GC Roots能直接關聯到的對象,用戶程序能在正確可用的Region中創建新對象。需要STW,但速度很快。
- 并發標記:進行GC Roots Tracing的過程, 這個過程相對耗時,但卻可以和用戶線程并行;
- 最終標記:將并發標記期間發生變化的對象進行重新標記,需要STW;
- 篩選回收:首先排序各個Region的回收價值和成本,然后根據用戶期望的GC停頓時間來制定回收計劃,最后按計劃回收一些價值高的Region中垃圾對象。 回收時采用復制算法,從一個或多個Region復制存活對象到堆上的另一個空的Region,并且在此過程中壓縮和釋放內存。可以并發進行,降低停頓時間,并增加吞吐量。
什么時候才應該考慮使用G1?
- 堆空間在6G以上;
- 存活數據占50%以上堆內存;
- 大對象比較多或晉升比較快的情況;
- GC 停頓在0.5~1s之間。
是否聽說過ZGC?
- ZGC是JDK11新加入的低延遲收集器,它立下了三個令人期待的flag:
- 停頓時間不會超過 10ms;
- 停頓時間不會隨著堆的增大而增大(不管多大的堆都能保持在 10ms 以下);
- 可支持幾百 M,甚至幾 T 的堆大小(最大支持 4T)。
常用命令與參數
如何查看jvm進程參數?
jinfo -flags 1280如何查看gc回收情況?
##1280是pid,每1000ms 打印一次 jstat -gcutil 1280 1000如何導出dump文件?
jmap -dump:format=b,file=/usr/temp/heap.hprof 1280CPU飆升如何定位問題?
## 找到占用cpu最多的進程,記錄pid top ## -Hp參數查看進程中占用cpu最多的線程信息,記錄tid top -Hp 7083 ## 將十進制的tid轉換成十六進制 printf %x 32468 ## jstack將進程的線程棧信息打印,最后再根據十六進制tid找到問題線程的上下文 jstack 7083 >stack.logfree命令的作用是什么?
- 用于查看當前內存使用情況,主要包括的字段有total、used、free、shared、buff/cache、available;
- available(可用內存)= free + buffer/cache - 不可被回收內存。
netstat命令的作用是什么?
- 用于查看當前的網絡信息匯總;
iostat命令的作用是什么?
- 用于對磁盤IO進行監控:iostat -x(顯示擴展報告);
- 重點關注:
- await字段:IO響應時間,一般小于5ms;
- %util:顯示了IO設備的繁忙程度,100%表示滿負荷運行;
常用的JVM參數與推薦值
https://hujinyang.blog.csdn.net/article/details/103655911
類加載原理
一個類被加載的全流程是怎樣的?
什么是雙親委派模型?工作流程是什么?有什么優點?
- 是一種遞歸上級優先加載的工作模型;
- 工作過程:
- 優點:Java類伴隨其類加載器具備了帶有優先級的層次關系,確保了在各種環境下的加載順序。 保證了運行的安全性,防止不可信類扮演可信任的角色。
如何自定義類加載器?
- 正常方式:繼承ClassLoader類并實現findClass方法;
- 破壞雙親委派:繼承ClassLoader類重寫loadClass方法。
自定義類加載器有哪些使用場景?
- 隔離加載類。例如在某些框架內進行中間件與應用的模塊隔離,把類加載到不同環境。
- 修改類加載方式。類的加載模型并非強制,除了Bootstrap ClassLoader外,其他的加載并非一定要引入。可以按需動態加載。
- 擴展加載源。例如從數據庫、網絡,甚至電視機機頂盒進行加載。
- 防止源碼泄漏。Java代碼容易被編譯和篡改,可以通過自定義類加載器進行編譯加密。
有哪些開源實現破壞了雙親委派?
- tomcat
- tomcat是可以部署多個項目的,那么每個項目的公共的基礎類只需要加載一次,而自定義類則需要隔離加載;
- 對于一些需要加載的非基礎類,會由一個叫作 WebAppClassLoader 的類加載器優先加載。等它加載不到的時候,再交給上層的 ClassLoader 進行加載。這個加載器用來隔絕不同應用的 .class 文件,比如你的兩個應用,可能會依賴同一個第三方的不同版本,它們是相互沒有影響的。
- SPI
- 典型的例子就是JDBC加載數據庫驅動;
- JDK提供了統一的JDBC驅動接口Driver,各種數據庫廠商(MySQL、Oracle等)會根據SPI規范,以jar包形式提供自己的實現。對于實現類的查找與加載本就屬于JDK的職責范疇,但是這些實現類都存在于classpath路徑下,頂層的雙親類加載器是無法找到的,需要借助子加載器AppClassLoader才能加載,但這又違背雙親委派原則。
- 為了解決這個問題,Java設計師引入了一種線程上下文類加載器(Thread Context ClassLoader),可以通過Thread類的setContextClassLoader()方法進行設置,如果沒有設置,默認就是AppClassLoader。這樣便可以可以利用線程上下文加載器去加載SPI的實現類,實現一種父類加載器請求子類加載器完成類加載的行為。雖然這樣違背了雙親委派原則,但也解決了一些特殊需求。
能否自定義一個java.lang.Object類?
-
不能:
https://hujinyang.blog.csdn.net/article/details/104113847
- 正常情況下類加載過程會遵循雙親委派機制,依次向上級類加載器委托加載,上級都加載不了,才會自行加載。
- 如果想繞過雙親委派機制,需要覆寫ClassLoader類的loadClass方法,一般不推薦這么做。
- 由于final方法defineClass的限制,正常情況下我們無法加載以“java.”開頭的系統類。
- 一般自定義類加載器只需實現ClassLoader的findClass方法來加載自定義路徑下的類,而不是覆寫loadClass破壞雙親委派,避免帶來系統安全隱患。
編譯原理
什么是解釋執行?什么是編譯執行?
- 解釋執行:解釋器對程序逐條翻譯成機器語言,然后再由計算機執行,優點就是可以立刻執行,無需預熱,不會產生新代碼;
- 編譯執行:編譯器將程序一次性翻譯成機器語言,后續可以直接執行,集成更多優化,有效提升執行效率,缺點是需要預熱等待生成機器碼。
JIT即時編譯器的作用是什么?
- 虛擬機將熱點代碼編譯成與本地平臺相關的機器碼,從而提高熱點代碼的執行效率;
- 同時JIT還能做一些代碼級的優化,如方法內聯、棧上分配、鎖消除、鎖粗化等。
什么是方法內聯?
- 方法內聯指的是,把一些短小的方法體,直接納入目標方法的作用范圍之內,就像是直接在代碼塊中追加代碼。這樣,就少了一次方法調用,執行速度就能夠得到提升。
除了基本數據類型,一定是在堆上分配的嗎?
- 不一定,通過逃逸分析,JVM 能夠分析出一個新的對象的使用范圍,從而決定是否要將這個對象分配到堆上。
什么是逃逸分析?
- 對象被賦值給成員變量或者靜態變量,或者作為返回結果返回,可能被外部使用,變量就發生了逃逸。
什么是棧上分配?
- 如果一個對象在子程序中被分配,指向該對象的指針永遠不會逃逸,對象有可能會被優化為棧分配。棧分配可以快速地在棧幀上創建和銷毀對象,不用再分配到堆空間,可以有效地減少 GC 的壓力;
- JIT可以將某些沒有逃逸出方法的對象優化成棧上分配。
總結
- 上一篇: python源码精要(2)-C代码规范
- 下一篇: access开发精要(2)-参照完整性