面試半年,憑藉這份JVM面試題,我終於拿到了字節跳動的offer!

內存區域

虛擬機棧
生命週期與線程相同,描述的是Java 方法執行的內存模型,每一個方法在執行的時候都會建立一個棧幀,用於存取局部變量表、操做數棧、動態連接、方法出口等信息
本地方法棧
與虛擬機棧做用類似,只不過本地方法棧是爲虛擬機使用到的Native方法服務
程序計數器
內存空間較小,能夠看作是當前線程所執行的字節碼的行號指示器。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域
若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Native方法,這個計數器值爲空(Undefined)

內存區域最大的一塊,此內存區域的惟一目的就是存放對象實例,基本上全部的對象實例分配都是由其分配內存。Java堆是垃圾收集器管理的區主要區域,所以有時也成爲GC堆
方法區
也稱爲非堆,主要用來存取已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據java

對象內存佈局

對象頭(Header)
用於存儲對象自身的運行時數據,如HashCode、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳
類型指針
實例數據(Instance Data)
對象真正存儲的有效信息,也是在程序代碼中所定義的各類類型的字段內容
對齊補充(Padding)面試

僅僅起到佔位符的做用算法

對象訪問定位

句柄訪問
Java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息
直接指針訪問
Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址數據庫

虛擬機棧和本地方法棧異常

若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError異常
若是虛擬機在拓展時沒法申請到足夠的內存空間,則拋出OutOfMemoryError(OOM)異常數組

對象已死

引用計數法
對象每引用一次就加1,引用失效則減1,當引用次數爲0的時候將進行回收,會出現循環依賴問題,所以虛擬機沒有使用此算法
可達性分析
使用GC ROOTS來判斷一個對象是否可達,不可達將其判斷爲不可達的對象瀏覽器

回收算法

標記-清除算法
將要回收的對象進行標記,回收的時候直接將已標記的對象進行回收,可是很容易產生內存碎片
標記-整理算法
將要回收的對象進行標記並移動到內存區域的一端,減小內存碎片的產生,可是這很影響效率
複製算法
新生代的對象大部分都是朝夕生死的,使用複製算法將不須要回收的對象移動到Survivor區,爲Eden區騰出空間,由於對象優先在Eden分配,年輕代中默認爲Eden:Survivor爲8:1,其中Survivor有兩個
分代收集算法
只是根據對象存活週期的不一樣將內存劃分爲幾塊,通常是將java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法
在新生代中,每次垃圾回收都有大批對象死去,只有少許存活,那就選中複製算法,只須要少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高,沒有額外空間對它進行分配擔保,就必須使用標記–清理或標記–整理算法安全

HotSpot算法實現

枚舉根節點
安全點(Safepoint)
安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的——由於每條指令執行的時間都是很是短暫,程序不太可能由於指令長度太長這個緣由而過長時間運行,「長時間運行」的最明顯的特徵就是指令序列複用,如方法調用、循環跳轉、異常跳轉
產生安全點
方法調用
循環跳轉
異常跳轉
安全區域(Safe Region)
Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。可是,程序「不執行」的時候呢?所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處於Sleep狀態或者Blocked狀態,這時候線程沒法響應JVM的中斷請求,「走」到安全的地方掛起,JVM也顯然不太可能等待線程從新被分配CPU時間。對於這種狀況,須要安全區域來解決
在一段代碼片斷中,引用關係不會發生變化,在這個區域中的任意地方開始GC都是安全的,能夠把Safe Region當作是被拓展了的Safepoint服務器

垃圾收集器

並行與併發的概念
並行(Parallel):指多條垃圾收集器線程並行工做,但此時用戶線程仍然處於等待狀態
併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續執行,而垃圾回收器程序運行於另外一個CPU上
新生代
Serial(JDK1.3.1以前)
單線程收集器,進行垃圾回收時必須暫停其餘全部的工做線程,知道它收集結束。優勢是簡單高效(與其餘收集器的單線程相比),沒有線程交互開銷
ParNew(JDK1.3)
Serial的多線程版本,除了多線程收集以外,其餘與Serial收集器相比並無太多創新之處
Parallel Scavenge(JDK1.4)
使用複製算法的收集器,使用並行的多線程收集器,爲了達到一個可控制的吞吐量,即CPU用於執行用戶代碼的時間與CPU總消耗時間的比值(吞吐量=用戶代碼執行時間/(用戶代碼執行時間+垃圾收集時間)),也稱爲吞吐量優先收集器
老年代
Serial Old
Serial收集器的老年代版本,單線程收集器,使用標記——整理算法 。主要意義也是在於給Client模式下的虛擬機使用,GC時須要STW
Parallel Old(JDK1.6)
Parallel Scavenge收集器的老年代版本,使用多線程和標記——整理算法
CMS(JDK1.5,Concurrent Mark Sweep)
使用標記——清除算法,以獲取最短回收停頓時間爲目標的收集器。優勢:併發收集,低停頓,Sun公司也稱之爲併發低停頓收集器(Concurrent Low Pause Collector)
運行步驟
初始標記(須要STW)
標記一下GC Roots能直接關聯到的對象,速度很快
併發標記
進行GC Roots Tracing的過程
從新標記(須要STW)
爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一點,但遠比並發標記的事件短
併發清除
缺點
對CPU資源很是敏感
沒法處理浮動垃圾
因爲採用了標記——清除算法,因此這很容易致使產生大量空間碎片
G1(JDK1.7,Garbage First)
運行步驟
初始標記
併發標記
最終標記
篩選回收
面向服務端應用
特色
並行與併發
充分利用多CPU、多核環境下的硬件優點,使用多個CPU來縮短STW(Stop The World,GC進行時需停頓全部的Java執行進程)停頓的時間
分代收集
空間整合
可預測的停頓網絡

內存分配與回收策略

新生代GC與老年代GC
新生代GC(Minor GC)
指發生在新生代的垃圾回收動做,由於Java對象大多都具有朝生夕滅的特性,因此Minor GC很是頻繁,通常回收速度也比較快
老年代(Major GC / Full GC)
指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行進行Major GC的策略選擇過程)。Major GG的速度通常會比Minor GC慢10倍以上
對象優先在Eden分配
對象主要分配在新生代的Eden區上,若是啓動了本地線程分配緩衝,將按線程優先在TLAB(Thread Local Allocation Buffer 本地線程分配緩衝區)上分配
大對象直接放入老年代
大對象指的是須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串和數組,儘可能避免出現朝生夕滅的大對象
長期存活的對象將進入老年代
虛擬機爲每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden出生並通過一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,而且對象年齡設爲1。對象在Survivor區中每熬過一次Minor GC,年齡就增長1歲,當其年齡增長到必定程度(默認爲15歲),就會晉升到老年代。對象晉升老年代的年齡閾值,能夠經過參數-XX:MaxTenuringThreshold設置
空間分配擔保
在發生Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則虛擬機會查看HandlerPromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小;若是大於,將嘗試進行一次Minor GC,儘管這個Minor GC是有風險的;若是小於,或者HandlerPromotionFailure設置不容許冒險,那這時也要改成進行一次Full GC數據結構

JVM經常使用命令

jps
相似於Linux中的ps命令,列出正在執行的虛擬機進程,並顯示虛擬機執行主類(Main Class,main()函數所在的類)名稱以及這些進程的本地虛擬機惟一ID(Local Virtual Machine Identifier,LVMID),是使用頻率最高的JDK命令行工具,由於其餘的JDK工具大多須要輸入它查詢到的LVMID來肯定要監控的是哪個虛擬機進程
jps [options] [hostid]
options
-q
只輸出LVMID,省略主類的名稱
-m
輸出虛擬機進程啓動時傳遞給主類main()函數的參數
-l
輸入主類的全名,若是進程執行的是Jar包,輸出Jar包路徑
-v
輸出虛擬機進程啓動時的JVM參數
hostid
RMI註冊表中註冊的主機名
jstat
虛擬機統計信息監視工具,能夠顯示本地或遠程虛擬機中的類加載、內存、垃圾收集、JIT編譯等運行數據
jstat [option vmid [interval[s | ms] [count]] ]
options 列舉2個
-class
監視類裝載、卸載數量、總空間以及類裝載所耗費的時間
-gc
監視Java情況,包括Eden區、兩個Survivor區、老年代、永久代等的容量、已用空間、GC時間合計等信息
interval和count表明查詢間隔和次數,若是省略這兩個參數,說明只查詢一次;eg:
jstat -gc 2764 250 20
須要每250ms查詢一次進程2764垃圾收集狀況,一次查詢20次
jinfo
Java配置信息工具,實時查看和調整虛擬機各項參數
jinfo [options] pid
options
jinfo對於Windows平臺功能仍然有較大限制,只提供了最基本的-flag選項
eg:查詢CMSInitiatingOccupancyFraction參數值
jinfo -flag CMSInitiatingOccupancyFraction 1444
jmap
Java內存影像工具(Memory Map for Java),jmap命令用於生成堆轉儲快照(通常稱爲headdump或dump文件)。和jinfo命令同樣,jmap有很多功能在Windows平臺都是受限的
jmap [ option ] vmid
options列舉4個
-dump
用於生成Java堆轉儲快照
-finalizerinfo
顯示在F-Queue中等待Finalizer線程執行finalize方法的對象。只在Linux/Solaris平臺纔有效
-head
顯示Java堆詳細信息,如使用哪一種回收器、參數配置、分代情況等。只在Linux/Solaris平臺下有效
-histo
顯示堆中對象統計信息,包括類、實例數量、合計容量
jhat
虛擬機堆轉儲快照分析工具,Sun JDK提供jhat(JVM Heap Analysis Tool)命令與jmap搭配使用,來分析jmap生成的堆轉儲快照,jhat內置了一個微型的HTTP/HTML服務器,生成dump文件的分析結果後,能夠在瀏覽器中查看。
jstack
Java堆棧跟蹤工具,jstack(Stack Trace for Java)命令用於生成虛擬機當前時刻的線程快照(通常稱爲threaddump或者javacore文件)。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的緣由。如線程間死鎖、死循環、請求外部資源致使的長時間等待等都是致使線程長時間停頓的常見緣由。線程出現停頓的時候經過jstack來查看各個線程的調用堆棧,就能夠知道沒有響應的線程到底在後臺作些什麼事情,或者等待些什麼資源
jstack [option] vmid
-F
當正常輸出的請求不被響應時,強制輸出線程堆棧
-l
除堆棧外,顯示關於鎖的附加信息
-m

若是調用到本地方法的話,能夠顯示C/C++的堆棧

類加載

加載
經過一個類的全限定名來獲取定義此類的二進制字節流
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口
(Class對象比較特殊,存放在方法區裏)
二進制流獲取路徑
從ZIP包中獲取,這很常見,最終成爲JAR、EAR、WAR格式的基礎
從網絡中獲取,如從Applet應用中獲取
運行時計算生成,如動態代理技術(JDK動態代理或cglib),在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來爲特定接口生成形式爲」*$Proxy「的代理類的二進制流
由其餘文件生成,如JSP應用,由JSP文件生成對應的Class類
從數據庫中讀取,比較少見,有些中間件服務器(如SAP Netweaver)能夠選擇把程序安裝到數據庫中來完成程序代碼在集羣間的分發
驗證
文件格式驗證
驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處
是否以魔數0xCAFEBABE開頭
主、次版本號是否在當前虛擬機處理範圍以內
常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)
指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量
CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數據
Class文件中各個部分及文件自己是否有被刪除的或附加的其餘信息
元數據驗證
對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求
這個類是否有父類(除了java.lang.Object以外,全部的類應當有父類)
這個類的父類是否繼承了不容許繼承的類,如被final修飾的類
若是這個類是抽象類,是否實現了其父類或接口之中要求實現的全部方法
類中的字段、方法是否與父類產生矛盾(如覆蓋了父類的final字段、或者出現不符合規則的方法重載、重寫)
字節碼驗證
驗證過程最爲複雜,主要目的是經過數據流和控制流分析,以肯定程序語義是合法的、符合邏輯的
保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似這樣的狀況:在操做數棧放置了一個int類型的數據,使用時卻按long類型來加載入本地變量表中
保證跳轉指令不會調轉到方法體之外的字節碼指令上
保證方法體中的類型轉換是有效的,例如能夠把一個子類對象賦值給父類數據類型,這是安全的,可是把父類對象賦值給子類數據類型,甚至把對象賦值給與它毫無繼承關係、徹底不相干的一個數據類型,則是危險和不合法的
符號引用驗證
符號引用中經過字符串描述的全限定名是否能找到對應的類
在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段
符號引用中的類、字段、方法的訪問性
(private、protected、public、default)是否能夠被當前類訪問
準備
爲類變量(被static修飾的變量,不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中)分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區進行分配
初始值一般狀況下是數據類型的零值
若是類字段的字段屬性表中存在ConstantValue屬性,那麼在準備階段變量value就會被初始化爲ConstantValue屬性所指定的值(被final修飾的類變量在編譯時會生成ConstantValue屬性)
eg:public static final int value = 123;
在準備階段虛擬機就會根據ConstantValue的設置將value賦值給123
解析
解析階段是虛擬機將常量池內的符號引用(在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量)替換成直接引用的過程
符號引用(Symbolic References)
符號引用以一組符號來描述所引用的目標,符號能夠是任意形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中
直接引用(Direct References)
直接引用能夠是直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那引用的目標一定已經在內存中存在
類或接口的解析
字段解析
類方法解析
接口方法解析
初始化
必須當即對類進行初始化的5種狀況
遇到new(實例化一個對象)、getstatic(讀取一個類的靜態字段【被final修飾、已在編譯期把結果放入常量池的靜態字段除外】)、putstatic(設置一個類的靜態字段【被final修飾、已在編譯期把結果放入常量池的靜態字段除外】)或invokestatic(調用一個類的靜態方法)字節碼指令時
使用java.lang.reflect包的方法對類進行反射調用的時候
當初始化一個類的時候,若是發現其父類尚未進行初始化,則須要先觸發其父類的初始化
包含main方法的類(執行的主類)
當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的的類沒有進行過初始化,則需先觸發其初始化
主動引用
上述五種狀況均爲主動引用
被動引用
全部引用類的方法都不會觸發初始化
經過子類引用父類的靜態字段,不會致使子類初始化
經過數組定義來引用類,不會觸發此類的初始化
常量在譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化
類構造器()

可由類中的static{}語句塊產生
也可由接口中的定義的常量產生,接口中不能定義static{}語句塊,須要注意的是,執行接口的()方法不須要先執行父接口的()方法,只有當父接口的常量使用時,父接口才會初始化
它不要顯示調用地父類構造器,虛擬機會保證在子類的()方法執行以前,父類的()方法已經執行完畢。能夠得出java.lang.Object是第一個先執行()方法的類
虛擬機會保證一個類的()方法在多線程環境中被正確加鎖、同步,如若多個線程同時去初始化一個類,那麼只有一個線程執行這個類的()方法,其餘線程阻塞等待,直至這個線程執行完()方法。其餘線程喚醒以後不會再次進入()方法
()方法不是必須的,若是類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成()方法
實例構造器()
使用
卸載

##類加載器
類加載器分類
從JVM的角度上看
啓動類加載器 Bootstrap ClassLoader
由C++實現,是虛擬機的一部分
全部其餘的類加載器
由Java實現,獨立於虛擬機外部,而且所有繼承自抽象類ClassLoader
從Java開發人員角度上看
啓動類加載器 Boostrap ClassLoader
拓展類加載器 Extension ClassLoader
應用程序類加載器 Application ClassLoader
自定義類加載器 User ClassLoader
雙親委派模型
若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都是應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載

下面附上我本身整理的路線圖:

面試半年,憑藉這份JVM面試題,我終於拿到了字節跳動的offer!
面試半年,憑藉這份JVM面試題,我終於拿到了字節跳動的offer!

最後:

上面的路線圖只是一部分,歡迎你們關注個人公衆號:前程有光,路線圖都放在個人公衆號裏面了,另外整理了1000多道將近500多頁pdf文檔的Java面試題資料關注後回覆領取資料便可領取到,文章都會在裏面更新,整理的資料也會放在裏面。

相關文章
相關標籤/搜索