數組和對象在堆內存分配;某些對象沒有逃逸出方法,可能被優化為在棧上分配
2、談談 JVM 中的常量池
JDK 1.8 開始
字符串常量池:存放在堆中,包括 String 對象執行 intern() 方法後存的地方、雙引號直接引用的字符串
運行時常量池:存放在方法區,屬於元空間,是類加載後的一些存儲區域,大多數是類中 constant_pool 的內容
類文件常量池:constant_pool,JVM 定義的概念
3、談談動態年齡判斷
這裡涉及到 -XX:TargetSurvivorRatio 參數,Survivor 區的目標使用率默認 50,即 Survivor 區對象目標使用率為 50%。
Survivor 區相同年齡所有對象大小的總和 > (Survivor 區內存大小 * 這個目標使用率)時,大於或等於該年齡的對象直接進入老年代。
當然,這裡還需要考慮參數 -XX:MaxTenuringThreshold 晉升年齡最大閾值
4、談談永久代
JDK 8 之前,Hotspot 中方法區的實現是永久代(Perm)
JDK 7 開始把原本放在永久代的字符串常量池、靜態變量等移出到堆,JDK 8 開始去除永久代,使用元空間(Metaspace),永久代剩餘內容移至元空間,元空間直接在本地內存分配。
5、JVM 有哪些運行時內存區域?
Java 8
The pc Register,程序計數器
Java Virtual Machine Stacks,Java 虛擬機棧
Heap,堆
Method Area,方法區
Run-Time Constant Pool,運行時常量池
Native Method Stacks,本地方法棧
摘自:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
6、運行時棧幀包含哪些結構?
7、JVM 的內存模型是什麼?
JVM 試圖定義一種統一的內存模型,能將各種底層硬體以及作業系統的內存訪問差異進行封裝,使 Java 程序在不同硬體以及作業系統上都能達到相同的並發效果。它分為工作內存和主內存,線程無法對主存儲器直接進行操作,如果一個線程要和另外一個線程通信,那麼只能通過主存進行交換。
8、JVM 如何確定垃圾對象?
JVM 採用的是可達性分析算法,通過 GC Roots 來判定對象是否存活,從 GC Roots 向下追溯、搜索,會產生 Reference Chain。當一個對象不能和任何一個 GC Root 產生關係時,就判定為垃圾。
軟引用和弱引用,也會影響對象的回收。內存不足時會回收軟引用對象;GC 時會回收弱引用對象。
9、哪些是 GC Roots?
在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
在方法區中常量引用的對象,譬如字符串常量池(String Table)裡的引用。
在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
所有被同步鎖(synchronized關鍵字)持有的對象。
反映 Java 虛擬機內部情況的 JMXBean、JVMTI中註冊的回調、本地代碼緩存等。
10、被引用的對象就一定能存活嗎?
不一定,看 Reference 類型,弱引用在 GC 時會被回收,軟引用在內存不足的時候,即 OOM 前會被回收,但如果沒有在 Reference Chain 中的對象就一定會被回收。
11、強引用、軟引用、弱引用、虛引用是什麼,有什麼區別?
強引用,就是普通的對象引用關係,如 String s = new String("ConstXiong")
軟引用,用於維護一些可有可無的對象。只有在內存不足時,系統則會回收軟引用對象,如果回收了軟引用對象之後仍然沒有足夠的內存,才會拋出內存溢出異常。SoftReference 實現
弱引用,相比軟引用來說,要更加無用一些,它擁有更短的生命周期,當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。WeakReference 實現
虛引用是一種形同虛設的引用,在現實場景中用的不是很多,它主要用來跟蹤對象被垃圾回收的活動。PhantomReference 實現
12、你做過 JVM 調優,說說如何查看 JVM 參數默認值?
13、工作中常用的 JVM 配置參數有哪些?
Java 8 為例
日誌
-XX:+PrintFlagsFinal,列印JVM所有參數的值
-XX:+PrintGC,列印GC信息
-XX:+PrintGCDetails,列印GC詳細信息
-XX:+PrintGCTimeStamps,列印GC的時間戳
-Xloggc:filename,設置GC log文件的位置
-XX:+PrintTenuringDistribution,查看熬過收集後剩餘對象的年齡分布信息
內存設置
-Xms,設置堆的初始化內存大小
-Xmx,設置堆的最大內存
-Xmn,設置新生代內存大小
-Xss,設置線程棧大小
-XX:NewRatio,新生代與老年代比值
-XX:SurvivorRatio,新生代中Eden區與兩個Survivor區的比值,默認為8,即Eden:Survivor:Survivor=8:1:1
-XX:MaxTenuringThreshold,從年輕代到老年代,最大晉升年齡。CMS 下默認為 6,G1 下默認為 15
-XX:MetaspaceSize,設置元空間的大小,第一次超過將觸發 GC
-XX:MaxMetaspaceSize,元空間最大值
-XX:MaxDirectMemorySize,用於設置直接內存的最大值,限制通過 DirectByteBuffer 申請的內存
-XX:ReservedCodeCacheSize,用於設置 JIT 編譯後的代碼存放區大小,如果觀察到這個值有限制,可以適當調大,一般夠用即可
設置垃圾收集相關
-XX:+UseSerialGC,設置串行收集器
-XX:+UseParallelGC,設置並行收集器
-XX:+UseConcMarkSweepGC,使用CMS收集器
-XX:ParallelGCThreads,設置Parallel GC的線程數
-XX:MaxGCPauseMillis,GC最大暫停時間 ms
-XX:+UseG1GC,使用G1垃圾收集器
CMS 垃圾回收器相關
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction,與前者配合使用,指定MajorGC的發生時機
-XX:+ExplicitGCInvokesConcurrent,代碼調用 System.gc() 開始並行 FullGC,建議加上這個參數
-XX:+CMSScavengeBeforeRemark,表示開啟或關閉在 CMS 重新標記階段之前的清除(YGC)嘗試,它可以降低 remark 時間,建議加上
-XX:+ParallelRefProcEnabled,可以用來並行處理 Reference,以加快處理速度,縮短耗時
G1 垃圾回收器相關
-XX:MaxGCPauseMillis,用於設置目標停頓時間,G1 會盡力達成
-XX:G1HeapRegionSize,用於設置小堆區大小,建議保持默認
-XX:InitiatingHeapOccupancyPercent,表示當整個堆內存使用達到一定比例(默認是 45%),並發標記階段就會被啟動
-XX:ConcGCThreads,表示並發垃圾收集器使用的線程數量,默認值隨 JVM 運行的平臺不同而變動,不建議修改
參數查詢官網地址:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
建議面試時最好能記住 CMS 和 G1的參數,特點突出使用較多,被問的概率大
14、談談對 OOM 的認識
除了程序計數器,其他內存區域都有 OOM 的風險。
棧一般經常會發生 StackOverflowError,比如 32 位的 windows 系統單進程限制 2G 內存,無限創建線程就會發生棧的 OOM
Java 8 常量池移到堆中,溢出會出 java.lang.OutOfMemoryError: Java heap space,設置最大元空間大小參數無效
堆內存溢出,報錯同上,這種比較好理解,GC 之後無法在堆中申請內存創建對象就會報錯
方法區 OOM,經常會遇到的是動態生成大量的類、jsp 等
直接內存 OOM,涉及到 -XX:MaxDirectMemorySize 參數和 Unsafe 對象對內存的申請
15、什麼情況發生棧溢出?
-Xss可以設置線程棧的大小,當線程方法遞歸調用層次太深或者棧幀中的局部變量過多時,會出現棧溢出錯誤 java.lang.StackOverflowError
16、你有哪些手段來排查 OOM 的問題?
增加兩個參數 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,當 OOM 發生時自動 dump 堆內存信息到指定目錄
同時 jstat 查看監控 JVM 的內存和 GC 情況,先觀察問題大概出在什麼區域
使用 MAT 工具載入到 dump 文件,分析大對象的佔用情況,比如 HashMap 做緩存未清理,時間長了就會內存溢出,可以把改為弱引用
17、遇到過元空間溢出嗎?
元空間在本地內存上,默認是沒有上限的,不加限制出了問題會影響整個伺服器的,所以也是比較危險的。-XX:MaxMetaspaceSize 可以指定最大值。
一般使用動態代理的框架會生成很多 Java 類,如果佔用空間超出了我們的設定最大值,會發生元空間溢出。
Unsafe 類申請內存、JNI 對內存進行操作、Netty 調用作業系統的 malloc 函數的直接內存,這些內存是不受 JVM 控制的,不加限制的使用,很容易發生溢出。這種情況有個顯著特點,dump 的堆文件信息正常甚至很小。
-XX:MaxDirectMemorySize 可以指定最大直接內存,但限制不住所有堆外內存的使用。
19、談談你知道的垃圾回收算法
判斷對象是否可回收的算法有兩種:
JVM 各廠商基本都是用的 Tracing GC 實現
大部分垃圾收集器遵從了分代收集(Generational Collection)理論。
針對新生代與老年代回收垃圾內存的特點,提出了 3 種不同的算法:
1、標記-清除算法(Mark-Sweep)
標記需回收對象,統一回收;或標記存活對象,回收未標記對象。
缺點:
2、標記-複製算法(Mark-Copy)
可用內存等分兩塊,使用其中一塊 A,用完將存活的對象複製到另外一塊 B,一次性清空 A,然後改分配新對象到 B,如此循環。
缺點:
3、標記-整理算法(Mark-Compact)
標記存活的對象,統一移到內存區域的一邊,清空佔用內存邊界以外的內存。
缺點:
20、談談你知道的垃圾收集器
Serial
特點:
JDK 1.3 開始提供
新生代收集器
無線程交互開銷,單線程收集效率最高
進行垃圾收集時需要暫停用戶線程
適用於客戶端,小內存堆的回收
ParNew
特點:
Parallel Scavenge
特點:
Serial Old
特點:
Parallel Old
特點:
CMS
特點:
標記-清除算法
追求最短回收停頓時間
多應用於關注響應時間的 B/S 架構的服務端
並發收集、低停頓
佔用一部分線程資源,應用程式變慢,吞吐量下降
無法處理浮動垃圾,可能導致 Full GC
內存碎片化問題
G1
特點:
JDK 6 開始實驗,JDK 7 商用
面向服務端,JDK 9 取代 Parallel Scavenge + Parallel Old
結合標記-整理、標記-複製算法
首創局部內存回收設計思路
基於 Region 內存布局,採用不同策略實現分代
不再使用固定大小、固定數量的堆內存分代區域劃分
優先回收價收益最大的 Region
單個或多個 Humongous 區域存放大對象
使用記憶集解決跨 Region 引用問題
複雜的卡表實現,導致更高的內存佔用,堆的 10%~20%
全功能垃圾收集器
追求有限的時間內最高收集效率、延遲可控的情況下最高吞吐量
追求應付內存分配速率,而非一次性清掉所有垃圾內存
適用於大內存堆
Shenandoah
特點:
ZGC
特點:
21、生產環境用的什麼JDK?如何配置的垃圾收集器?
Oracle JDK 1.8
JDK 1.8 中有 Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,默認使用 Parallel Scavenge + Parallel Old。
Serial 系列是單線程垃圾收集器,處理效率很高,適合小內存、客戶端場景使用,使用參數 -XX:+UseSerialGC 顯式啟用。
Parallel 系列相當於並發版的 Serial,追求高吞吐量,適用於較大內存並且有多核CPU的環境,默認或顯式使用參數 -XX:+UseParallelGC 啟用。可以使用 -XX:MaxGCPauseMillis 參數指定最大垃圾收集暫停毫秒數,收集器會儘量達到目標;使用 -XX:GCTimeRatio 指定期望吞吐量大小,默認 99,用戶代碼運行時間:垃圾收集時間=99:1。
CMS,追求垃圾收集暫停時間儘可能短,適用於服務端較大內存且多 CPU 的應用,使用參數 -XX:+UseConcMarkSweepGC 顯式開啟,會同時作用年輕代與老年代,但有浮動垃圾和內存碎片化的問題。
G1,主要面向服務端應用的垃圾收集器,適用於具有大內存的多核 CPU 的伺服器,追求較小的垃圾收集暫停時間和較高的吞吐量。首創局部內存回收設計思路,採用不同策略實現分代,不再使用固定大小、固定數量的堆內存分代區域劃分,而是基於 Region 內存布局,優先回收價收益最大的 Region。使用參數 -XX:+UseG1GC 開啟。
我們生產環境使用了 G1 收集器,相關配置如下
-Xmx12g
-Xms12g
-XX:+UseG1GC
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
-XX:G1HeapRegionSize 未指定
核心思路:
每個內存區域設置上限,避免溢出
堆設置為作業系統的 70%左右,超過 8 G,首選 G1
根據老年代對象提升速度,調整新生代與老年代之間的內存比例
等過 GC 信息,針對項目敏感指標優化,比如訪問延遲、吞吐量等
22、如何查看 JVM 當前使用的是什麼垃圾收集器?
-XX:+PrintCommandLineFlags 參數可以列印出所選垃圾收集器和堆空間大小等設置
如果開啟了 GC 日誌詳細信息,裡面也會包含各代使用的垃圾收集器的簡稱
23、如何開啟和查看 GC 日誌?
常見的 GC 日誌開啟參數包括:
-Xloggc:filename,指定日誌文件路徑
-XX:+PrintGC,列印 GC 基本信息
-XX:+PrintGCDetails,列印 GC 詳細信息
-XX:+PrintGCTimeStamps,列印 GC 時間戳
-XX:+PrintGCDateStamps,列印 GC 日期與時間
-XX:+PrintHeapAtGC,列印 GC 前後的堆、方法區、元空間可用容量變化
-XX:+PrintTenuringDistribution,列印熬過收集後剩餘對象的年齡分布信息,有助於 MaxTenuringThreshold 參數調優設置
-XX:+PrintAdaptiveSizePolicy,列印收集器自動設置堆空間各分代區域大小、收集目標等自動調節的相關信息
-XX:+PrintGCApplicationConcurrentTime,列印 GC 過程中用戶線程並發時間
-XX:+PrintGCApplicationStoppedTime,列印 GC 過程中用戶線程停頓時間
-XX:+HeapDumpOnOutOfMemoryError,堆 oom 時自動 dump
-XX:HeapDumpPath,堆 oom 時 dump 文件路徑
Java 9 JVM 日誌模塊進行了重構,參數格式發生變化,這個需要知道。
GC 日誌輸出的格式,會隨著上面的參數不同而發生變化。關注各個分代的內存使用情況、垃圾回收次數、垃圾回收的原因、垃圾回收佔用的時間、吞吐量、用戶線程停頓時間。
藉助工具可視化工具可以更方便的分析,在線工具 GCeasy;離線版可以使用 GCViewer。
如果現場環境不允許,可以使用 JDK 自帶的 jstat 工具監控觀察 GC 情況。
24、JVM 監控與分析工具你用過哪些?介紹一下。
jps,顯示系統所有虛擬機進程信息的命令行工具
jstat,監視分析虛擬機運行狀態的命令行工具
jinfo,查看和調整虛擬機參數的命令行工具
jmap,生成虛擬機堆內存轉儲快照的命令行工具
jhat,顯示和分析虛擬機的轉儲快照文件的命令行工具
jstack,生成虛擬機的線程快照的命令行工具
jcmd,虛擬機診斷工具,JDK 7 提供
jhsdb,基於服務性代理實現的進程外可視化調試工具,JDK 9 提供
JConsole,基於JMX的可視化監視和管理工具
jvisualvm,圖形化虛擬機使用情況的分析工具
Java Mission Control,監控和管理 Java 應用程式的工具
MAT,Memory Analyzer Tool,虛擬機內存分析工具
vjtools,唯品會的包含核心類庫與問題分析工具
arthas,阿里開源的 Java 診斷工具
greys,JVM進程執行過程中的異常診斷工具
GCHisto,GC 分析工具
GCViewer,GC 日誌文件分析工具
GCeasy,在線版 GC 日誌文件分析工具
JProfiler,檢查、監控、追蹤 Java 性能的工具
BTrace,基於動態字節碼修改技術(Hotswap)實現的Java程序追蹤與分析工具
下面可以重點體驗下:
JDK 自帶的命令行工具方便快捷,不是特別複雜的問題可以快速定位;
阿里的 arthas 命令行也不錯;
可視化工具 MAT、JProfiler 比較強大。
25、JIT 是什麼?
Just In Time Compiler 的簡稱,即時編譯器。為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器就是 JIT。
26、談談雙親委派模型
Parents Delegation Model,這裡的 Parents 翻譯成雙親有點不妥,類加載向上傳遞的過程中只有單親;parents 更多的是多級向上的意思。
除了頂層的啟動類加載器,其他的類加載器在加載之前,都會委派給它的父加載器進行加載,一層層向上傳遞,直到所有父類加載器都無法加載,自己才會加載該類。
雙親委派模型,更好地解決了各個類加載器協作時基礎類的一致性問題,避免類的重複加載;防止核心API庫被隨意篡改。
JDK 9 之前
啟動類加載器(Bootstrp ClassLoader),加載 /lib/rt.jar、-Xbootclasspath
擴展類加載器(Extension ClassLoader)sun.misc.Launcher$ExtClassLoader,加載 /lib/ext、java.ext.dirs
應用程式類加載器(Application ClassLoader,sun.misc.Launcher$AppClassLoader),加載 CLASSPTH、-classpath、-cp、Manifest
自定義類加載器
JDK 9 開始 Extension ClassLoader 被 Platform ClassLoader 取代,啟動類加載器、平臺類加載器、應用程式類加載器全都繼承於 jdk.internal.loader.BuiltinClassLoader
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,檢查請求的類是否已經被加載過了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父類加載器拋出ClassNotFoundException
// 說明父類加載器無法完成加載請求
}
if (c == null) {
// 在父類加載器無法加載時
// 再調用本身的findClass方法來進行類加載
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
27、列舉一些你知道的打破雙親委派機制的例子。為什麼要打破?
JNDI 通過引入線程上下文類加載器,可以在 Thread.setContextClassLoader 方法設置,默認是應用程式類加載器,來加載 SPI 的代碼。有了線程上下文類加載器,就可以完成父類加載器請求子類加載器完成類加載的行為。打破的原因,是為了 JNDI 服務的類加載器是啟動器類加載,為了完成高級類加載器請求子類加載器(即上文中的線程上下文加載器)加載類。
Tomcat,應用的類加載器優先自行加載應用目錄下的 class,並不是先委派給父加載器,加載不了才委派給父加載器。打破的目的是為了完成應用間的類隔離。
OSGi,實現模塊化熱部署,為每個模塊都自定義了類加載器,需要更換模塊時,模塊與類加載器一起更換。其類加載的過程中,有平級的類加載器加載行為。打破的原因是為了實現模塊熱替換。
JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,當平臺及應用程式類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統模塊中,如果可以找到這樣的歸屬關係,就要優先委派給負責那個模塊的加載器完成加載。打破的原因,是為了添加模塊化的特性。
28、說一下垃圾分代收集的過程
分為新生代和老年代,新生代默認佔總空間的 1/3,老年代默認佔 2/3。
新生代使用複製算法,有 3 個分區:Eden、To Survivor、From Survivor,它們的默認佔比是 8:1:1。
當新生代中的 Eden 區內存不足時,就會觸發 Minor GC,過程如下:
在 Eden 區執行了第一次 GC 之後,存活的對象會被移動到其中一個 Survivor 分區;
Eden 區再次 GC,這時會採用複製算法,將 Eden 和 from 區一起清理,存活的對象會被複製到 to 區;
移動一次,對象年齡加 1,對象年齡大於一定閥值會直接移動到老年代
Survivor 區相同年齡所有對象大小的總和 > (Survivor 區內存大小 * 這個目標使用率)時,大於或等於該年齡的對象直接進入老年代。其中這個使用率通過 -XX:TargetSurvivorRatio 指定,默認為 50%
Survivor 區內存不足會發生擔保分配
超過指定大小的對象可以直接進入老年代
Major GC,指的是老年代的垃圾清理,但並未找到明確說明何時在進行Major GC
FullGC,整個堆的垃圾收集,觸發條件:
1.每次晉升到老年代的對象平均大小>老年代剩餘空間
2.MinorGC後存活的對象超過了老年代剩餘空間
3.元空間不足
4.System.gc() 可能會引起
5.CMS GC異常,promotion failed:MinorGC時,survivor空間放不下,對象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC時,同時有對象要放入老年代,而老年代空間不足造成
6.堆內存分配很大的對象
29、如何找到死鎖的線程?
死鎖的線程可以使用 jstack 指令 dump 出 JVM 的線程信息。
jstack -l <pid> > threads.txt
有時候需要dump出現異常,可以加上 -F 指令,強制導出
jstack -F -l <pid> > threads.txt
如果存在死鎖,一般在文件最後會提示找到 deadlock 的數量與線程信息
30、invokedynamic 指令是幹什麼的?
Java 7 開始,新引入的字節碼指令,可以實現一些動態類型語言的功能。Java 8 的 Lambda 表達式就是通過 invokedynamic 指令實現,使用方法句柄實現。
31、什麼是方法內聯?
為了減少方法調用的開銷,可以把一些短小的方法,納入到目標方法的調用範圍之內,這樣就少了一次方法調用,提升速度
32、什麼是逃逸分析?
分析對象動態作用域
如果能證明一個對象不會逃逸到方法或線程之外,或者逃逸程度比較低(只逃逸出方法而不會逃逸出線程),則可能為這個對象實例採取不同程度的優化,如棧上分配、標量替換、同步消除。
33、描述一下什麼情況下,對象會從年輕代進入老年代
對象的年齡超過一定閥值,-XX:MaxTenuringThreshold 可以指定該閥值
動態對象年齡判定,有的垃圾回收算法,比如 G1,並不要求 age 必須達到 15 才能晉升到老年代,它會使用一些動態的計算方法
大小超出某個閥值的對象將直接在老年代上分配,值默認為 0,意思是全部首選 Eden 區進行分配,-XX:PretenureSizeThreshold 可以指定該閥值,部分收集器不支持
分配擔保,當 Survivor 空間不夠的時候,則需要依賴其他內存(指老年代)進行分配擔保,這個時候,對象也會直接在老年代上分配
34、safepoint 是什麼?
為了減少對象引用的掃描,使用 OopMap 的數據結構在特定的位置記錄下棧裡和寄存器裡哪些位置是引用;
但為了避免給每條指令都生成 OopMap 記錄佔用大量內存的問題,只在特定位置記錄這些信息。
安全點的選定既不能太少以至於讓收集器等待時間過長,也不能太過頻繁以至於過分增大運行時的內存負荷。安全點位置的選取基本上是以「是否具有讓程序長時間執行的特徵」為標準進行選定的,如方法調用、循環跳轉、異常跳轉等都屬於指令序列復用。
35、MinorGC、MajorGC、FullGC 什麼時候發生?
36、說說類加載的過程
加載(Loading),通過一個類的全限定名來獲取定義此類的二進位字節流;將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。
驗證(Verification),確保Class文件的字節流中包含的信息符合《Java虛擬機規範》的全部約束要求,保證這些信息被當作代碼運行後不會危害虛擬機自身的安全。
準備(Preparation),正式為類中定義的變量(即靜態變量,被static修飾的變量)分配內存並設置類變量初始值。
解析(Resolution),是 JVM 將常量池內的符號引用替換為直接引用的過程。
初始化(Initialization),執行類構造器 <clinit> 方法的過程,執行所有類變量的賦值動作和靜態語句塊(static{}塊)。
其中驗證、準備、解析統稱為稱為連接(Linking)
37、可以描述一下 class 文件的結構嗎?
Class 文件包含了 Java 虛擬機的指令集、符號表、輔助信息的字節碼(Byte Code),是實現跨作業系統和語言無關性的基石之一。
一個 Class 文件定義了一個類或接口的信息,是以 8 個字節為單位,沒有分隔符,按順序緊湊排在一起的二進位流。
用 "無符號數" 和 "表" 組成的偽結構來存儲數據。
組成部分
1、魔數 Magic Number
2、版本號
3、常量池
4、訪問標誌
5、類索引、父類索引、接口索引集合
6、欄位表(field_info)集合
7、方法表(method_info)集合
8、屬性表(attribute_info)集合
38、說說 JVM 如何執行 class 中的字節碼。
JVM 先加載包含字節碼的 class 文件,存放在方法區,實際運行時,虛擬機會執行方法區內的代碼。Java 虛擬機在內存中劃分出棧和堆來存儲運行時的數據。
運行過程中,每當調用進入 Java 方法,都會在 Java 方法棧中生成一個棧幀,用來支持虛擬機進行方法的調用與執行,包含了局部變量表、操作數棧、動態連結、方法返回地址等信息。
當退出當前執行的方法時,不管正常返回還是異常返回,Java 虛擬機均會彈出當前線程的當前棧幀,並將之捨棄。
方法的調用,需要通過解析完成符號引用到直接引用;通過分派完成動態找到被調用的方法。
從硬體角度來看,Java 字節碼無法直接執行。因此,Java 虛擬機需要將字節碼翻譯成機器碼。翻譯過程由兩種形式:第一種是解釋執行,即將遇到的字節一邊碼翻譯成機器碼一邊執行;第二種是即時編譯(Just-In-Time compilation,JIT),即將一個方法中包含的所有字節碼編譯成機器碼後再執行。在 HotSpot 裡兩者都有,解釋執行在啟動時節約編譯時間執行速度較快;隨著時間的推移,編譯器逐漸會返回作用,把越來越多的代碼編譯成本地代碼後,可以獲取更高的執行效率。
39、生產環境 CPU 佔用過高,你如何解決?
top + H 指令找出佔用 CPU 最高的進程的 pid
top -H -p
jstack -l
將 tid 轉換為十六進位,在 threads.txt 中搜索,查到對應的線程代碼執行棧,在代碼中查找佔 CPU 比較高的原因。其中 tid 轉十六進位,可以藉助 Linux 的 printf "%x" tid 指令
我用上述方法查到過,jvm 多條線程瘋狂 full gc 導致的CPU 100% 的問題和 JDK1.6 HashMap 並發 put 導致線程 CPU 100% 的問題
40、生產環境伺服器變慢,如何診斷處理?
使用 top 指令,伺服器中 CPU 和 內存的使用情況,-H 可以按 CPU 使用率降序,-M 內存使用率降序。排除其他進程佔用過高的硬體資源,對 Java 服務造成影響。
如果發現 CPU 使用過高,可以使用 top 指令查出 JVM 中佔用 CPU 過高的線程,通過 jstack 找到對應的線程代碼調用,排查出問題代碼。
如果發現內存使用率比較高,可以 dump 出 JVM 堆內存,然後藉助 MAT 進行分析,查出大對象或者佔用最多的對象來自哪裡,為什麼會長時間佔用這麼多;如果 dump 出的堆內存文件正常,此時可以考慮堆外內存被大量使用導致出現問題,需要藉助作業系統指令 pmap 查出進程的內存分配情況、gdb dump 出具體內存信息、perf 查看本地函數調用等。
如果 CPU 和 內存使用率都很正常,那就需要進一步開啟 GC 日誌,分析用戶線程暫停的時間、各部分內存區域 GC 次數和時間等指標,可以藉助 jstat 或可視化工具 GCeasy 等,如果問題出在 GC 上面的話,考慮是否是內存不夠、根據垃圾對象的特點進行參數調優、使用更適合的垃圾收集器;分析 jstack 出來的各個線程狀態。如果問題實在比較隱蔽,考慮是否可以開啟 jmx,使用 visualmv 等可視化工具遠程監控與分析。