面試題——Java虛擬機
一、運行時數據區域
Java虛擬機在執行Java程序的時候會把它所管理的內存劃分為若幹個不同的數據區域,這些區域各有用途:
-
程序計數器:(線程私有的)
程序計數器是一塊較小的內存,可以看作是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時通過改變這個計數器的值來選取下一條指令。
-
Java虛擬機棧:(線程私有)
Java虛擬機棧描述的是Java方法執行的內存模型,每個方法執行的同時會創建一個棧幀,用來存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。
-
本地方法棧:(線程私有):
本地方法棧為虛擬機使用到Native方法服務。
-
Java堆:(線程共享)
Java堆存放對象的實例惡,幾乎所有對象的實例都在這裏分配。Java堆可以細分為新生代和老生代;又可以劃分為Eden空間,From Survivor空間和To Survivor空間。
-
方法區:(線程共享)
方法區用於存儲已經被虛擬機加載的類信息,常量,靜態變量,編譯器編譯後的代碼等數據。
運行時常量池是方法區的一部分,用來存放編譯期生成的各種字面量和符號引用。
二、對象在虛擬機中創建的過程
虛擬機遇到一條new指令,首先將去檢查這個指令的參數能否在常量池中定位到一個類的符號引用,並且檢查這個符號應用代表的類是否已被加載,解析和初始化過,否則執行相應的類加載過程;
在類加載檢查通過之後,虛擬機將為新生的對象分配內存,選擇恰當的堆內存分配方式(需要考慮到垃圾收集器和並發分配內存);
內存分配完成後,虛擬機將分配到的內存空間都初始化為0後,虛擬機對對象進行必要的設置,如這個對象是那個類的實例,類的元數據信息,對象的哈希碼,GC
之後Java程序將進行<init>方法進行初始化,但與虛擬機初始化無關。
三、判斷對象是否存在的方法
一般來說,通過引用計數算法,給對象中添加一個引用計數器,每一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1.任何時候計數器為0時對象就是不能再被使用的。
但是並不能解決循環引用的問題,需要使用可達性分析,通過一系列稱為“GC Root”對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個鍍錫那個到GC Root沒有任何引用鏈,則證明此對象是不可用的。
在Java語言中作為GC Root的對象包括:
-
虛擬機棧中的引用對象;
-
方法區中類靜態屬性引用的對象;
-
方法區中常量引用的對象;
-
本地方法棧中JNI引用的對象。
引用強度:強引用,軟引用,弱引用,虛引用。
-
強引用:最普通的引用,一般new出來的對象都是強引用類型,如果是一個強引用,那麽GC不會進行回收,當出現內存不足的時候會拋出OutOfMemoryError錯誤,除非人為將對象設置為null;
-
軟引用:通過SoftReference進行聲明,如果一個對象是軟引用類型的話,當出現內存不足的時候就會進行GC,通常軟引用類型對內存敏感,可以作為高速緩存;
-
弱引用:通過WeakrReference進行聲明,具有弱引用的對象生命周期更短,GC一旦發現了弱引用就會回收內存,但由於垃圾回收線程優先級較低,不容易發現這樣的弱引用對象;
-
虛引用:通過PhantomReference進行聲明,聲明為虛引用的對象在任何時候都會被回收,通常用來進行垃圾回收的過程。
四、對象的死亡需要經過的過程:
如果對象在進行可達性分析後發現沒有與GC Root相連接將會被第一次標記並進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法,當對象沒有覆蓋finalize ()方法或者finalize()方法已經被虛擬機調用過,那麽這兩種情況都視為沒有必要執行。
如果對象被判定為有必要執行finalize()方法,那麽對象會放置在一個F-Queue隊列中由之後的finalize線程執行。
五、垃圾收集算法:
- 標記-清除:
這是垃圾收集算法中最基礎的,它的思想就是標記哪些要被回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,導致以後程序在分配較大的對象時,由於沒有充足的連續內存而提前觸發一次GC動作。
- 復制算法:
為了解決效率問題,復制算法將可用內存按容量劃分為相等的兩部分,然後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象復制到第二塊內存上,然後一次性清楚完第一塊內存,再將第二塊上的對象復制到第一塊。但是這種方式,內存的代價太高,每次基本上都要浪費一半的內存。
於是將該算法進行了改進,內存區域不再是按照1:1去劃分,而是將內存劃分為8:1:1三部分,較大那份內存交Eden區,其余是兩塊較小的內存區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象復制到第二塊內存區上,然後清除Eden區,如果此時存活的對象太多,以至於Survivor不夠時,會將這些對象通過分配擔保機制復制到老年代中。
- 標記-整理
該算法主要是為了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了復制算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生內存碎片了。
- 分代收集
現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存周期,將堆分為新生代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那麽這時就采用復制算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。
六、垃圾收集器
-
新生代收集器:Serial、ParNew、Parallel Scavenge;
-
老年代收集器:Serial Old、Parallel Old、CMS;
-
整堆收集器:G1;
(A)、並行(Parallel)指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態;如ParNew、Parallel Scavenge、Parallel Old;
(B)、並發(Concurrent)指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序線程運行於另一個CPU上; 如CMS、G1(也有並行);
-
Serial收集器
Serial(串行)垃圾收集器是最基本、發展歷史最悠久的收集器;
針對新生代;采用復制算法;單線程收集;進行垃圾收集時,必須暫停所有工作線程,直到完成;
-
ParNew收集器
ParNew垃圾收集器是Serial收集器的多線程版本,除了多線程外,其余的行為、特點和Serial收集器一樣。
-
Parallel Scavenge收集器
新生代收集器;采用復制算法;多線程收集;
CMS等收集器的關註點是盡可能地縮短垃圾收集時用戶線程的停頓時間;而Parallel Scavenge收集器的目標則是達一個可控制的吞吐量(Throughput);
-
Serial Old收集器
Serial Old是 Serial收集器的老年代版本;針對老年代;采用"標記-整理"算法(還有壓縮,Mark-Sweep-Compact);單線程收集;
-
Parallel Old收集器
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本; JDK1.6中才開始提供;針對老年代; 采用"標記-整理"算法;多線程收集;
-
CMS收集器
並發標記清理(Concurrent Mark Sweep,CMS)收集器也稱為並發低停頓收集器(Concurrent Low Pause Collector)或低延遲(low-latency)垃圾收集器;
針對老年代;基於"標記-清除"算法(不進行壓縮操作,產生內存碎片);以獲取最短回收停頓時間為目標; 並發收集、低停頓;需要更多的內存;
-
G1收集器
G1(Garbage-First)是JDK7-u4才推出商用的收集器;
七、內存分配與回收策略
對象優先在Eden區中分配,當Eden區沒有足夠空間進行分配虛擬機將執行一次Minor GC;
大對象直接進入老年代,所謂大對象就是需要大量連續內存空間的Java對象,避免在Eden區及兩個Survivor區之間發生大量的內存復制;
長期存活的對象將進入老年代,虛擬機給每個對象定義一個對象年齡及數據,如果對象在Eden區出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor區中並且對象年齡加1,對象在Survivor區中每熬過一次Minor GC,年齡就增加一歲,當年齡增加到一定程度就會被晉升到老年代中;
動態對象年齡判定,如果Survivor空間中相同年齡所有對象大小的總和大於Srvivor空間的一半,年齡大與或等於該年齡的對象就直接進入老年代;
空間分配擔保,在發生Minor GC之前,虛擬機會先檢查老年代最大可用連續空間是否大於所有對象總空間,如果大於,那麽Minor GC總是安全的。否則需要進行Full GC。但是可以進行冒險,新生代使用復制收集算法,但為了內存利用率,只是用一個Survivor空間作為輪換備份,因此當大量對象在Minor GC後仍然存活,就需要老年代進行擔保,把Survivor區無法容納的對象直接進入老年代。
八、類加載過程
加載
加載時類加載的第一個過程,在這個階段,將完成一下三件事情:
1. 通過一個類的全限定名獲取該類的二進制流。
2. 將該二進制流中的靜態存儲結構轉化為方法去運行時數據結構。
3. 在內存中生成該類的Class對象,作為該類的數據訪問入口。
驗證
驗證的目的是為了確保Class文件的字節流中的信息不回危害到虛擬機.在該階段主要完成以下四鐘驗證:
1. 文件格式驗證:驗證字節流是否符合Class文件的規範,如主次版本號是否在當前虛擬機範圍內,常量池中的常量是否有不被支持的類型.
2. 元數據驗證:對字節碼描述的信息進行語義分析,如這個類是否有父類,是否集成了不被繼承的類等。
3. 字節碼驗證:是整個驗證過程中最復雜的一個階段,通過驗證數據流和控制流的分析,確定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉指令是否正確等。
4. 符號引用驗證:這個動作在後面的解析過程中發生,主要是為了確保解析動作能正確執行。
準備
準備階段是為類的靜態變量分配內存並將其初始化為默認值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。
解析
該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之前,也有可能在初始化之後。
初始化
初始化時類加載的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。
九、類加載器
通過一個類的全限定名來獲取描述此類的二進制字節流這個動態實現的虛擬機外部代碼模塊叫做“類加載器”。
-
啟動類加載器:將\lib目錄下的類庫加載到虛擬機內存中;java程序無法直接使用,即rt.jar。
-
擴展類加載器:加載\lib\ext目錄中,或者被java.ext.dir系統變量指定的所有類庫,java程序可以直接使用,即\ext文件下的jar包;
-
應用程序類加載器:負責加載用戶類路徑上所指定的類庫,java程序可以直接使用;
-
用戶自定義類加載器,通過繼承 java.lang.ClassLoader類的方式實現。
十、雙親委派模型
當一個類收到了類加載請求時,不會自己先去加載這個類,而是將其委派給父類,由父類去加載,如果此時父類不能加載,反饋給子類,由子類去完成類的加載。
使用雙親委派模型使得Java類具有明顯的層級關系,確保各種類加載器之間的加載順序,保證系統運行安全。舉例來說,假如沒有雙親委派模型,用戶編寫Object類自行加載,會使得出現多個Object類,導致程序出現混亂。
十一、Java內存模型
java內存模型(JMM)是線程間通信的控制機制.JMM定義了主內存和線程之間抽象關系。線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。
十二、棧幀的結構
1)局部變量表:局部變量表裏存儲的是基本數據類型、returnAddress類型(指向一條字節碼指令的地址)和對象引用,這個對象引用有可能是指向對象起始地址的一個指針,也有可能是代表對象的句柄或者與對象相關聯的位置。局部變量所需的內存空間在編譯器間確定
2)操作棧:操作數棧的作用主要用來存儲運算結果以及運算的操作數,它不同於局部變量表通過索引來訪問,而是壓棧和出棧的方式
3)動態連接:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接.動態鏈接就是將常量池中的符號引用在運行期轉化為直接引用。
4)返回地址:方法執行後只有兩種退出方法:遇到任意一個返回字節碼指令或者在運行中遇到異常,無論采用什麽退出方式,都需要返回到方法被調用的位置,返回的位置保存在返回地址中。
十三、其他問題
- 使用GC Root枚舉根節點,由於整個方法區中枚舉耗時過高如何解決?
在HotSpot中,通過OopMap這個數據結構來達到這個目的,在類加載完成之後,會在JIT編譯時,將棧和寄存器存在引用的地方標記起來,GC直接對這些標記點進行掃描。
- 什麽是安全點?
OopMap雖然可以進行快速的GCRoot枚舉,但是由於虛擬機指令太多,如果每個指令生成OopMap會浪費大量的空間,所以虛擬機會在特定的位置生成OopMap,這些位置稱為安全點。
- 安全點如何選擇?
一般選擇方法調用,異常處理,循環跳轉的位置作為安全點。
- 方法去中哪些類是無用的類?
- java堆中不存在該類的任何實例;
- 加載該類的classLoader都被回收了;
- 該類的class對象沒有任何地方被引用到。
- 對象常見內存分配方式?
- 指針碰撞:對象需要多大的內存在類加載完成之後就已經確定,相當於將已分配內存放在一邊,將未分配內存放在另一邊,中間用指針進行隔離,內存分配的時候移動指針;
- 空閑列表:維護一個表,紀錄內存中未分配的內存。
- 空間分配擔保?
當新生代發生GC的時候,先判斷老年代中剩余空間的大小是否大於新生代中所有對象的總大小,如果大於,則進行minor GC,如果不是則需要進行老年代空間分配擔保,如果不允許進行老年代空間分配擔保,則進行full GC,如果允許老年代空間分配擔保,則判斷老年代中剩余空間大小是否大於每次minor GC進入到老年代中對象大小的平均值。
十四、虛擬機指令
-XMS:JVM堆初始大小;
-XMX:JVM堆最大大小;
-XSS:線程棧的初始大小;
-XX:NewSize:新生代的大小;
-XX:MaxNewSize:新生代的最大大小;
-XX:NewRatio:新生代和老年代大小的比例;
-XX:SurvivorRatio:Eden區和一個Survivor區的大小比例;
-XX:InitialTernuringThreadhold:老年代閾值;
-XX:MaxTernuringThreadhold:老年代最大閾值;
面試題——Java虛擬機