Java 引覺得豪的就是它的自動內存管理機制。相比於 C++的手動內存管理、複雜難以理解的指針等,Java 程序寫起來就方便的多。
然而這種呼之即來揮之即去的內存申請和釋放方式,天然也有它的代價。爲了管理這些快速的內存申請釋放操做,就必須引入一個池子來延遲這些內存區域的回收操做。
咱們常說的內存回收,就是針對這個池子的操做。咱們把上面說的這個池子,叫做堆,能夠暫時把它當作一個總體。java
Java 程序的數據結構是很是豐富的。其中的內容,舉一些例子:
靜態成員變
動態成員變量
區域變量
短小緊湊的對象聲明
龐大複雜的內存申請面試
咱們先看一下 JVM 的內存佈局。隨着 Java 的發展,內存佈局一直在調整之中。好比,Java 8 及以後的版本,完全移除了持久代,而使用 Metaspace 來進行替代。這也表示着 -XX:PermSize 和 -XX:MaxPermSize 等參數調優,已經沒有了意義。但大致上,比較重要的內存區域是固定的。
JVM 內存區域劃分如圖所示,從圖中咱們能夠看出:數組
Java 虛擬機棧是基於線程的。哪怕你只有一個 main() 方法,也是以線程的方式運行的。在線程的生命週期中,參與計算的數據會頻繁地入棧和出棧,棧的生命週期是和線程同樣的。數據結構
棧裏的每條數據,就是棧幀。在每一個 Java 方法被調用的時候,都會建立一個棧幀,併入棧。一旦完成相應的調用,則出棧。全部的棧幀都出棧後,線程也就結束了。每一個棧幀,都包含四個區域:多線程
咱們的應用程序,就是在不斷操做這些內存空間中完成的。
佈局
本地方法棧是和虛擬機棧很是類似的一個區域,它服務的對象是 native 方法。你甚至能夠認爲虛擬機棧和本地方法棧是同一個區域,這並不影響咱們對 JVM 的瞭解。spa
這裏有一個比較特殊的數據類型叫做 returnAdress。由於這種類型只存在於字節碼層面,因此咱們日常打交道的比較少。對於 JVM 來講,程序就是存儲在方法區的字節碼指令,而 returnAddress 類型的值就是指向特定指令內存地址的指針。
操作系統
既然是線程,就表明它在獲取 CPU 時間片上,是不可預知的,須要有一個地方,對線程正在運行的點位進行緩衝記錄,以便在獲取 CPU 時間片時可以快速恢復。線程
程序計數器是一塊較小的內存空間,它的做用能夠看做是當前線程所執行的字節碼的行號指示器。這裏面存的,就是當前線程執行的進度。下面這張圖,可以加深你們對這個過程的理解。
能夠看到,程序計數器也是由於線程而產生的,與虛擬機棧配合完成計算操做。程序計數器還存儲了當前正在運行的流程,包括正在執行的指令、跳轉、分支、循環、異常處理等。3d
咱們能夠看一下程序計數器裏面的具體內容。下面這張圖,就是使用 javap 命令輸出的字節碼。你們能夠看到在每一個 opcode 前面,都有一個序號。就是圖中紅框中的偏移地址,你能夠認爲它們是程序計數器的內容。
堆是 JVM 上最大的內存區域,咱們申請的幾乎全部的對象,都是在這裏存儲的。咱們常說的垃圾回收,操做的對象就是堆。
堆空間通常是程序啓動時,就申請了,可是並不必定會所有使用。
隨着對象的頻繁建立,堆空間佔用的愈來愈多,就須要不按期的對再也不使用的對象進行回收。這個在 Java 中,就叫做 GC(Garbage Collection)。
因爲對象的大小不一,在長時間運行後,堆空間會被許多細小的碎片佔滿,形成空間浪費。因此,僅僅銷燬對象是不夠的,還須要堆空間整理。這個過程很是的複雜。
那一個對象建立的時候,究竟是在堆上分配,仍是在棧上分配呢?這和兩個方面有關:對象的類型和在 Java 類中存在的位置。
Java 的對象能夠分爲基本數據類型和普通對象。
對於普通對象來講,JVM 會首先在堆上建立對象,而後在其餘地方使用的實際上是它的引用。好比,把這個引用保存在虛擬機棧的局部變量表中。
對於基本數據類型來講(byte、short、int、long、float、double、char),有兩種狀況。
咱們上面提到,每一個線程擁有一個虛擬機棧。當你在方法體內聲明瞭基本數據類型的對象,它就會在棧上直接分配。其餘狀況,都是在堆上分配。
注意,像 int[] 數組這樣的內容,是在堆上分配的。數組並非基本數據類型。
這就是 JVM 的基本的內存分配策略。而堆是全部線程共享的,若是是多個線程訪問,會涉及數據同步問題。
關於元空間,咱們仍是以一個很是高頻的面試題開始:「爲何有 Metaspace 區域?它有什麼問題?」
說到這裏,你應該回想一下類與對象的區別。對象是一個活生生的個體,能夠參與到程序的運行中;類更像是一個模版,定義了一系列屬性和操做。那麼你能夠設想一下。咱們前面生成的 A.class,是放在 JVM 的哪一個區域的?
想要問答這個問題,就不得不提下 Java 的歷史。在 Java 8 以前,這些類的信息是放在一個叫 Perm 區的內存裏面的。更早版本,甚至 String.intern 相關的運行時常量池也放在這裏。這個區域有大小限制,很容易形成 JVM 內存溢出,從而形成 JVM 崩潰。
Perm 區在 Java 8 中已經被完全廢除,取而代之的是 Metaspace。原來的 Perm 區是在堆上的,如今的元空間是在非堆上的,這是背景。關於它們的對比,能夠看下這張圖。
而後,元空間的好處也是它的壞處。使用非堆可使用操做系統的內存,JVM 不會再出現方法區的內存溢出;可是,無限制的使用會形成操做系統的死亡。因此,通常也會使用參數 -XX:MaxMetaspaceSize 來控制大小。
方法區,做爲一個概念,依然存在。它的物理存儲的容器,就是 Metaspace。如今,只須要了解到,這個區域存儲的內容,包括:類的信息、常量池、方法數據、方法代碼就能夠了。
因爲常量池,在 Java 7 以後,放到了堆中,咱們建立的字符串,將會在堆上分配。
關於它們的關係,咱們能夠看一張圖。在個人感受裏,堆是軟綿綿的,鬆散而有彈性;而非堆是冰冷生硬的,內存很是緊湊。
你們都知道,JVM 在運行時,會從操做系統申請大塊的堆內內存,進行數據的存儲。可是,堆外內存也就是申請後操做系統剩餘的內存,也會有部分受到 JVM 的控制。比較典型的就是一些 native 關鍵詞修飾的方法,以及對內存的申請和處理。
在 Linux 機器上,使用 top 或者 ps 命令,在大多數狀況下,可以看到 RSS 段(實際的內存佔用),是大於給 JVM 分配的堆內存的。
若是你申請了一臺系統內存爲 2GB 的主機,可能 JVM 能用的就只有 1GB,這即是一個限制。
JVM 的運行時區域是棧,而存儲區域是堆。不少變量,其實在編譯期就已經固定了。