天天看點

如何學習 JVM?

作者:Java小老太

前言

對于學習 Java 的同學來說,JVM 是一個繞不過的話題。如果你想要成為進階工程師,那麼了解 JVM 原理是必選項。

如何學習 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面試問題

1、記憶體模型以及分區,需要詳細到每個區放什麼。

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:啟動類加載器,負責将

如何學習 JVM?

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的對象有以下幾種:
  1. 虛拟機棧中引用的對象
  2. 方法區類靜态屬性引用的對象
  3. 方法區常量池引用的對象
  4. 本地方法棧JNI引用的對象

雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象比不一定會被回收。當一個對象不可達GC Root時,這個對象并 不會立馬被回收,而是出于一個死緩的階段,若要被真正的回收需要經曆兩次标記

如果對象在可達性分析中沒有與GCRoot的引用鍊,那麼此時就會被第一次标記并且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當對象沒有覆寫finalize()方法或者已被虛拟機調用過,那麼就認為是沒必要的。

如果該對象有必要執行finalize()方法,那麼這個對象将會放在一個稱為F-Queue的對隊列中,虛拟機會觸發一個Finalize()線程去執行,此線程是低優先級的,并且虛拟機不會承諾一直等待它運作完,這是因為如果finalize()執行緩慢或者發生了死鎖,那麼就會造成F-Queue隊列一直等待,造成了記憶體回收系統的崩潰。GC對處于F-Queue中的對象進行第二次被标記,這時,該對象将被移除”即将回收”集合,等待回收。

繼續閱讀