本系列文章講解 面試中常見的 JVM 問題。這些問題之因此常見,是由於很基礎,對於一個有點逼格的程序猿來講, JVM 的相關特性和原理在工做也須要熟知。筆者也在面試的過程當中屢屢受挫,屢敗屢戰,總結一些常見知識點,這些知識點既能夠應付面試,也能夠幫助讀者深刻了解 JVM 提供大綱。html
在用 C 之類的編程語言時,程序員須要本身手動分配和釋放內存。而 Java 不同,它有垃圾回收器,釋放內存由回收器負責。java
Java 虛擬機在執行 Java 程序的過程當中會把它管理的內存劃分紅若干個不一樣的數據區域。那咱們來簡單看一下 Java 程序具體執行的過程:程序員
首先 Java 源代碼文件(.java 後綴)會被 Java 編譯器編譯爲字節碼文件(.class 後綴),而後由 JVM 中的類加載器加載各個類的字節碼文件,加載完畢以後,交由 JVM 執行引擎執行。在整個程序執行過程當中,JVM 會用一段空間來存儲程序執行期間須要用到的數據和相關信息,這段空間通常被稱做爲 Runtime Data Area(運行時數據區),也就是咱們常說的 JVM 內存。所以,在 Java 中咱們經常說到的內存管理就是針對這段空間進行管理(如何分配和回收內存空間)。面試
本文的主要內容:算法
運行時數據區分爲線程私有和共享數據區兩大類。其中線程私有的數據區包含程序計數器、虛擬機棧、本地方法區,全部線程共享的數據區包含 Java 堆、方法區,在方法區內有一個常量池。編程
下面咱們依次介紹這些數據區。數組
堆用於存放對象實例,全部的對象和數組都要在堆上分配。是 JVM 所管理的內存中最大的一塊區域。Java 堆是全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例以及數組都在這裏分配內存。Java 堆是垃圾收集器管理的主要區域,所以也被稱做 GC 堆(Garbage Collected Heap).從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此 Java 堆還能夠細分爲:新生代和老年代。新生代具體劃分有:Eden 空間、From Survivor、To Survivor 空間等,進一步劃分的目的是更好地回收內存,或者更快地分配內存。緩存
方法區與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即編譯器編譯後的代碼等數據。
HotSpot 虛擬機中方法區也常被稱爲永久代
,本質上二者並不等價。僅僅是由於 HotSpot 虛擬機設計團隊用永久代來實現方法區而已,這樣 HotSpot 虛擬機的垃圾收集器就能夠像管理 Java 堆同樣管理這部份內存了。可是這並非一個好主意,由於這樣更容易遇到內存溢出問題。相對而言,垃圾收集行爲在這個區域是較少出現的,但並不是數據進入方法區後就永久存在了。安全
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用於存放編譯期生成的各類字面量和符號引用)微信
Java 虛擬機棧是線程私有的,它的生命週期和線程相同,描述的是 Java 方法執行的內存模型。 Java 內存能夠粗糙的區分爲堆內存(Heap)和棧內存(Stack),其中棧就是如今說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。存儲局部變量表、操做數棧、動態連接和方法出口等信息。 局部變量表主要存放了編譯器可知的各類數據類型、對象引用。
和虛擬機棧所發揮的做用很是類似,區別是: 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 一個 Native Method 就是一個 Java 程序調用非 Java 代碼的接口。在定義一個 Native method 時,並不提供實現體(有些像定義一個java interface),由於其實現體是由非 Java 語言在外面實現的。標識符native能夠與全部其它的 Java 標識符連用,可是 abstract 除外。
咱們知道,當一個類第一次被使用到時,這個類的字節碼會被加載到內存,而且只會回載一次。在這個被加載的字節碼的入口維持着一個該類全部方法描述符的 list,這些方法描述符包含這樣一些信息:方法代碼存於何處,它有哪些參數,方法的描述符(public 等)等等。
若是一個方法描述符內有 native,這個描述符塊將有一個指向該方法的實現的指針。這些實如今一些 DLL 文件內,可是它們會被操做系統加載到 Java 程序的地址空間。當一個帶有本地方法的類被加載時,其相關的 DLL 並未被加載,所以指向方法實現的指針並不會被設置。當本地方法被調用以前,這些 DLL 纔會被加載,這是經過調用 java.system.loadLibrary()
實現的。
須要提示的是,使用本地方法是有開銷的,它喪失了 Java 的不少好處。若是別無選擇,咱們能夠選擇使用本地方法。
程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都須要依賴這個計數器來完。 另外,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。
棧解決程序的運行問題,即程序如何執行,或者說如何處理數據;堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。
在 Java 中一個線程就會相應有一個線程棧與之對應,這點很容易理解,由於不一樣的線程執行邏輯有所不一樣,所以須要一個獨立的線程棧。而堆則是全部線程共享的。棧由於是運行單位,所以裏面存儲的信息都是跟當前線程(或程序)相關信息的。包括局部變量、程序運行狀態、方法返回值等等;而堆只負責存儲對象信息。
Java 的堆是一個運行時數據區,類的(對象從中分配空間。這些對象經過 new、newarray、anewarray 和 multianewarray 等指令創建,它們不須要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優點是能夠動態地分配內存大小,生存期也沒必要事先告訴編譯器,由於它是在運行時 動態分配內存的,Java 的垃圾收集器會自動收走這些再也不使用的數據。但缺點是,因爲要在運行時動態分配內存,存取速度較慢。棧的優點是,存取速度比堆要快,僅次於寄存器,棧數據能夠共享。但缺點是,存在棧中的數據大小與生存期必須是肯定的,缺少靈活性。棧中主要存放一些基本類 型的變量(int, short, long, byte, float, double, boolean, char)和對象句柄。
在 Java 中當咱們要對數據進行更底層的操做時,通常是操做數據的字節(byte)形式,這時常常會用到 ByteBuffer 這樣一個類。ByteBuffer 提供了兩種靜態實例方式:
public static ByteBuffer allocate(int capacity) public static ByteBuffer allocateDirect(int capacity) 複製代碼
爲何要提供兩種方式呢?這與 Java 的內存使用機制有關。ByteBuffer 有兩種,一種是 heap ByteBuffer,該類對象分配在 JVM 的堆內存裏面,直接由 Java 虛擬機負責垃圾回收;一種是 direct ByteBuffer 是經過 JNI 在虛擬機外內存中分配的。JDK1.4 中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel) 與緩存區(Buffer) 的 I/O 方式,它能夠直接使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣就能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆之間來回複製數據。本機直接內存的分配不會收到 Java 堆的限制,可是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。經過 Jmap 沒法查看該快內存的使用狀況。只能經過 top 來看它的內存使用狀況。
直接內存並非虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,可是這部份內存也被頻繁地使用。並且也可能致使 OutOfMemoryError 異常出現。 DirectMemory 容量能夠經過 -XX:MaxDirectMemorySize
指定,若是不指定,則默認爲與 Java 堆的最大值。
direct ByteBuffer 經過 full gc 來回收內存,direct ByteBuffer 會本身檢測狀況而調用 system.gc()
,可是若是參數中使用了 -DisableExplicitGC
那麼就沒法回收該快內存了,-XX:+DisableExplicitGC
標誌自動將 System.gc()
調用轉換成一個空操做,就是應用中調用 System.gc()
會變成一個空操做,所以須要咱們手動來回收內存了。
@Test
public void testGcDirectBuffer() throws NoSuchFieldException, IllegalAccessException {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
cleanerField.setAccessible(true);
Cleaner cleaner = (Cleaner) cleanerField.get(buffer);
cleaner.clean();
}
複製代碼
除此以外,CMS GC 也會回收 Direct ByteBuffer 的內存,CMS 主要是針對老年代空間的垃圾回收。
在 Java 中,類型的加載、鏈接和初始化過程都在程序運行期間完成的,這種策略雖然會使類加載時增長一些性能開銷,可是提供了高度的靈活性,Java 天生能夠動態擴展的語言就是依賴於運行期動態加載和動態鏈接的特色實現的。
虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型,這就是 Java 虛擬機的類加載機制。Class 文件是一串二進制的字節流。實際上,每一個 Class 文件都有可能表明着 Java 語言中的一個類或者接口。
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱爲鏈接(Linking)。
加載
查找並加載類的二進制數據。 加載是類加載過程的第一個階段,虛擬機在這一階段須要完成如下三件事情:
驗證
確保被加載的類的正確性。 這一階段是確保 Class 文件的字節流中包含的信息符合當前虛擬機的規範,而且不會損害虛擬機自身的安全。包含了四個驗證動做:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
java.lang.IncompatibleClassChangeError
異常的子類,如 IllegalAccessError
、NoSuchfiledError
、NoSuchMethodError
等。準備
爲類的靜態變量分配內存,並將其初始化爲默認值。 準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。
解析
把類中的符號引用轉換爲直接引用。 解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行。
初始化
類變量進行初始化 爲類的靜態變量賦予正確的初始值,JVM 負責對類進行初始化,主要對類變量進行初始化。
除此以外,還有用戶自定義類加載器,是 java.lang.ClassLoader 的子類。在程序運行期間,經過java.lang.ClassLoader 的子類動態加載 class 文件,體現 Java 動態實時類裝入特性.
雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去加載這個類,而是把請求委託給父加載器去完成,依次向上。所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器沒有找到所需的類時,子加載器纔會嘗試去加載該類。
經過帶有優先級的層級關係能夠避免類的重複加載; 保證 Java 程序安全穩定運行,Java 核心 API 定義類型不會被隨意替換。
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。
在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後即可肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從Java堆中劃分出來。分配方式有 「指針碰撞」 和 「空閒列表」 兩種:
選擇那種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性。
在 Hotspot 虛擬機中,對象在內存中的佈局能夠分爲3塊區域:對象頭、實例數據和對齊填充。
對象頭,Hotspot 虛擬機中的對象頭包括兩部分信息,第一部分用於存儲對象自身的自身運行時數據(哈希嗎、GC 分代年齡、鎖狀態標誌等等);另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是那個類的實例。
實例數據,是對象真正存儲的有效信息,也是在程序中所定義的各類類型的字段內容。
對齊填充部分,不是必然存在的,也沒有什麼特別的含義,僅僅起佔位做用。 由於Hotspot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或2倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。
創建對象就是爲了使用對象,咱們的Java程序經過棧上的reference數據來操做堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有句柄和直接指針兩種:
這兩種對象訪問方式各有優點。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針。而 reference 自己不須要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。
本文主要講了 JVM 中運行時數據區的劃分以及類加載機制。JVM 中的對象建立以後,如何回收無用的對象呢?JVM 的垃圾回收算法和多種垃圾收集器是怎麼樣的呢?下篇文章將會具體講解。