面試中必問的JVM應該怎麼學(面試題含答案)
方法區與Java堆一樣,是各個執行緒共享的區域,它用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯(JIT)後的程式碼等資料。對於JDK1.8之前的HotSpot虛擬機器而言,很多人經常將方法區稱為我們上圖中所描述的永久代,實際上兩者並不等價,因為這僅僅是HotSpot的設計團隊選擇利用永久代來實現方法區而言。同時對於其他虛擬機器比如IBM J9中是不存在永久代的概念的。 其實,移除永久代的工作從JDK1.7就開始了。JDK1.7中,儲存在永久代的部分資料就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變數(class statics)轉移到了java heap。而在JDK1.8之後永久代概念也已經不再存在取而代之的是元空間metaspace。
常量池其實是方法區中的一部分,因為這裡比較重要,所以我們拿出來單獨看一下。注意我們這裡所說的執行時的常量池並僅僅是指Class檔案中的常量池,因為JVM可能會進行即時編譯進行優化,在執行時將部分常量載入到常量池中。
程式計數器
JVM中的程式計數器和計算機組成原理中提到的程式計數器PC概念類似,是執行緒私有的,用來記錄當前執行的位元組碼位置。還是稍微解釋一下吧,CPU的佔有時間是以分片的形式分配給給每個不同執行緒的,從作業系統的角度來講,在不同執行緒之間切換的時候就是依賴程式計數器來記錄上一次執行緒所執行到具體的程式碼的行數,在JVM中就是位元組碼。
Java虛擬機器棧
與程式計數器一樣,Java虛擬機器棧也是執行緒私有的,用通俗的話將它就是我們常常聽說到堆疊中的那個“棧記憶體”。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變量表(區域性變量表需要的記憶體在編譯期間就確定了所以在方法執行期間不會改變大小),運算元棧,動態連結,方法出口等資訊。每一個方法從呼叫至出棧的過程,就對應著棧幀在虛擬機器中從入棧到出棧的過程。p.s: 關於棧幀這裡我們以後講虛擬機器位元組碼執行引擎的時候再來仔細分析。
本地方法棧
本地方法棧和Java虛擬機器棧類似,只不過是為JVM執行Native方法服務,這裡就不解釋了。
堆
堆是用來存放物件的記憶體空間, 幾乎所有的物件都儲存在堆中。
堆的特點: 執行緒共享 整個Java虛擬機器只有一個堆,所有的執行緒都訪問同一個堆。而程式計數器、Java虛擬機器棧、本地方法棧都是一個執行緒對應一個的。 在虛擬機器啟動時建立 垃圾回收的主要場所。 可以進一步細分為:新生代、老年代。 新生代又可被分為:Eden、From Survior、To Survior。 不同的區域存放具有不同生命週期的物件。這樣可以根據不同的區域使用不同的垃圾回收演算法,從而更具有針對性,從而更高效。 堆的大小既可以固定也可以擴充套件,但主流的虛擬機器堆的大小是可擴充套件的,因此當執行緒請求分配記憶體,但堆已滿,且記憶體已滿無法再擴充套件時,就丟擲OutOfMemoryError。
總結
Java虛擬機器的記憶體模型中一共有兩個“棧”,分別是:Java虛擬機器棧和本地方法棧。 兩個“棧”的功能類似,都是方法執行過程的記憶體模型。並且兩個“棧”內部構造相同,都是執行緒私有。 只不過Java虛擬機器棧描述的是Java方法執行過程的記憶體模型,而本地方法棧是描述Java本地方法執行過程的記憶體模型。 Java虛擬機器的記憶體模型中一共有兩個“堆”,一個是原本的堆,一個是方法區。方法區本質上是屬於堆的一個邏輯部分。堆中存放物件,方法區中存放類資訊、常量、靜態變數、即時編譯器編譯的程式碼。 堆是Java虛擬機器中最大的一塊記憶體區域,也是垃圾收集器主要的工作區域。 程式計數器、Java虛擬機器棧、本地方法棧是執行緒私有的,即每個執行緒都擁有各自的程式計數器、Java虛擬機器棧、本地方法區。並且他們的生命週期和所屬的執行緒一樣。 而堆、方法區是執行緒共享的,在Java虛擬機器中只有一個堆、一個方法棧。並在JVM啟動的時候就建立,JVM停止才銷燬。
JVM面試問題
JVM 分為堆區和棧區,還有方法區,初始化的物件放在堆裡面,引用放在棧裡面,class類資訊常量池(static常量和static變數)等放在方法區new:方法區:主要是儲存類資訊,常量池(static常量和static變數),編譯後的程式碼(位元組碼)等資料堆:初始化的物件,成員變數 (那種非static的變數),所有的物件例項和陣列都要在堆上分配棧:棧的結構是棧幀組成的,呼叫一個方法就壓入一幀,幀上面儲存區域性變量表,運算元棧,方法出口等資訊,區域性變量表存放的是8大基礎型別加上一個應用型別,所以還是一個指向地址的指標本地方法棧:主要為Native方法服務程式計數器:記錄當前執行緒執行的行號
2、GC的兩種判定方法
引用計數法:指的是如果某個地方引用了這個物件就+1,如果失效了就-1,當為0就會回收但是JVM沒有用這種方式,因為無法判定相互迴圈引用(A引用B,B引用A)的情況
引用鏈法: 通過一種GC ROOT的物件(方法區中靜態變數引用的物件等-static變數)來判斷,如果有一條鏈能夠到達GC ROOT就說明,不能到達GC ROOT就說明可以回收
3、 GC的三種收集方法:標記清除、標記整理、複製演算法的原理與特點,分別用在什麼地方,如果讓你優化收集方法,有什麼思路?
先標記,標記完畢之後再清除,效率不高,會產生碎片
複製演算法:分為8:1的Eden區和survivor區,就是上面談到的YGC
標記整理:標記完畢之後,讓所有存活的物件向一端移動
4、幾種常用的記憶體除錯工具:jmap、jstack、jconsole、jhat
jstack可以看當前棧的情況,jmap檢視記憶體,jhat 進行dump堆的資訊 mat(eclipse的也要了解一下)
5、類載入的幾個過程
載入、驗證、準備、解析、初始化。然後是使用和解除安裝了通過全限定名來載入生成class物件到記憶體中,然後進行驗證這個class檔案,包括檔案格式校驗、元資料驗證,位元組碼校驗等。準備是對這個物件分配記憶體。解析是將符號引用轉化為直接引用(指標引用),初始化就是開始執行構造器的程式碼
6、雙親委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
Bootstrap ClassLoader:啟動類載入器,負責將 Java_Home /lib/ext或者由系統變數 java.ext.dir指定位置中的類庫載入到記憶體中。ApplicationClassLoader:它負責將系統類路徑(CLASSPATH)中指定的類庫載入到記憶體中。開發者可以直接使用系統類載入器雙親委派模型是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。-----例如類java.lang.Object,它存在在rt.jar中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的Bootstrap ClassLoader進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類載入器自行載入的話,如果使用者編寫了一個java.lang.Object的同名類並放在ClassPath中,那系統中將會出現多個不同的Object類,程式將混亂
7、SafePoint是什麼
比如GC的時候必須要等到Java執行緒都進入到safepoint的時候VMThread才能開始執行GC, 迴圈的末尾 (防止大迴圈的時候一直不進入safepoint,而其他執行緒在等待它進入safepoint) 方法返回前 呼叫方法的call之後 丟擲異常的位置
8、如和判斷一個物件是否存活?(或者GC物件的判定方法)
判斷一個物件是否存活有兩種方法:
-
- 引用計數法 所謂引用計數法就是給每一個物件設定一個引用計數器,每當有一個地方引用這個物件時,就將計數器加一,引用失效時,計數器就減一。當一個物件的引用計數器為零時,說明此物件沒有被引用,也就是“死物件”,將會被垃圾回收. 引用計數法有一個缺陷就是無法解決迴圈引用問題,也就是說當物件A引用物件B,物件B又引用者物件A,那麼此時A,B物件的引用計數器都不為零,也就造成無法完成垃圾回收,所以主流的虛擬機器都沒有采用這種演算法。
-
2.可達性演算法(引用鏈法) 該演算法的思想是:從一個被稱為GC Roots的物件開始向下搜尋,如果一個物件到GC Roots沒有任何引用鏈相連時,則說明此物件不可用。 在java中可以作為GC Roots的物件有以下幾種:
虛擬機器棧中引用的物件
方法區類靜態屬性引用的物件
方法區常量池引用的物件
本地方法棧JNI引用的物件
雖然這些演算法可以判定一個物件是否能被回收,但是當滿足上述條件時,一個物件比不一定會被回收。當一個物件不可達GC Root時,這個物件並
不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收需要經歷兩次標記
如果物件在可達性分析中沒有與GC
Root的引用鏈,那麼此時就會被第一次標記並且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法或者已被虛擬機器呼叫過,那麼就認為是沒必要的。
如果該物件有必要執行finalize()方法,那麼這個物件將會放在一個稱為F-Queue的對佇列中,虛擬機器會觸發一個Finalize()執行緒去執行,此執行緒是低優先順序的,並且虛擬機器不會承諾一直等待它執行完,這是因為如果finalize()執行緩慢或者發生了死鎖,那麼就會造成F-Queue佇列一直等待,造成了記憶體回收系統的崩潰。GC對處於F-Queue中的物件進行第二次被標記,這時,該物件將被移除”即將回收”集合,等待回收。
