天天看點

【深入了解JVM】:Java記憶體區域程式計數器Java虛拟機棧本地方法棧Java堆方法區直接記憶體總結

JVM具有自動記憶體管理機制,Java不需要像c/c++一樣,為每一個new操作寫配對的delete/free代碼,不容易出現記憶體洩露和溢出。JVM記憶體區域主要包括如下部分:程式計數器、Java虛拟機棧、本地方法棧、Java堆、方法區。

【深入了解JVM】:Java記憶體區域程式計數器Java虛拟機棧本地方法棧Java堆方法區直接記憶體總結

程式計數器

程式計數器可以視為目前線程所執行的位元組碼行号訓示器,如果目前執行的是Native方法,計數器的值為空(Undefined)。在JVM的概念模型中,位元組碼解釋器通過改變計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程回複等都依賴程式計數器完成。

每條線程都有獨立的計數器,保證線程切換恢複正确位置,是以程式計數器這一塊記憶體區域是線程隔離的。該區域是唯一一個沒有規定任何OutOfMemoryError的區域。

Java虛拟機棧

Java虛拟機棧也是線程隔離的,一個線程一個棧,并且生命周期與線程相同。

它内部由棧幀構成,一個棧幀代表一個調用的方法,線程在每次方法調用執行時建立一個棧幀然後壓棧,棧幀用于存放局部變量、操作數、動态連結、傳回位址等資訊。方法執行完成後對應的棧幀出棧。我們平時說的棧記憶體就是指這個棧。

一個線程中的方法可能還會調用其他方法,這樣就會構成方法調用鍊,而且這個鍊可能會很長,而且每個線程都有方法處于執行狀态。對于執行引擎來說,隻有活動線程棧頂的棧幀才是有效的,稱為目前棧幀(Current Stack Frame),這個棧幀關聯的方法稱為目前方法(Current Method)。

【深入了解JVM】:Java記憶體區域程式計數器Java虛拟機棧本地方法棧Java堆方法區直接記憶體總結

虛拟機棧的2種異常:

  • StackOverFlowError:調用鍊過長,線程請求深度大于JVM所允許的深度。
  • OutOfMemoryError:虛拟機棧動态擴充時無法申請到足夠記憶體。

本地方法棧

本地方法棧與虛拟機棧的所用很相似,是虛拟機線程調用Native方法執行時的棧。Java可以通過java本地接口JNI(Java Native Interface)來調用其它語言編寫(如C)的程式,在Java裡面用native修飾符來描述一個方法是本地方法。

虛拟機規範中沒有對本地方法棧作強制規定,虛拟機可以自由實作,是以可以不是位元組碼。如果是以位元組碼實作的話,虛拟機棧本地方法棧就可以合二為一,事實上,OpenJDK和SunJDK所自帶的HotSpot虛拟機就是直接将虛拟機棧和本地方法棧合二為一的。

Java堆

Java堆是JVM管理的最大一塊記憶體,是線程共享的,在JVM啟動時建立。堆存放所有對象執行個體以及數組。不過在JIT(Just-in-time)和逃逸情況下,棧上配置設定、标量替換有可能在棧上配置設定對象執行個體。

堆是java垃圾收集器管理的主要區域(是以很多時候會稱它為GC堆)。從GC回收的角度看,由于現在GC基本都是采用的分代收集算法,是以堆記憶體結構還可以分塊成:新生代和老年代、永久代;再細一點的有Eden空間、From Survivor空間、To Survivor空間等。值得注意的是,從JKD1.7開始,永久代Perm逐漸被移除,最新的JDK1.8中已經使用元空間(MetaSpace)代替永久代。

【深入了解JVM】:Java記憶體區域程式計數器Java虛拟機棧本地方法棧Java堆方法區直接記憶體總結

Java堆可以像磁盤空間一樣,允許邏輯上連續而實體不連續。如果堆中沒有記憶體完成執行個體配置設定并且無法擴充,将會抛出OutOfMemoryError異常。

方法區

Java虛拟機規範将方法區描述為堆的邏輯部分,但是卻稱為非堆(Non-Heap),在Sun的HotSpot虛拟機中,可以将方法區了解為堆記憶體塊中的永久代(Permanent Generation)。它是線程共享的。

方法去用于存儲在加載類檔案時,用于存放加載過的類資訊,常量,靜态變量,及JIT編譯後的代碼(類方法)等資料。

類的基本資訊

每個類的全限定名、每個類的直接超類的全限定名(可限制類型轉換)、該類是類還是接口、該類型的通路修飾符、直接超接口的全限定名的有序清單。

運作時常量池

常量池(Constant Pool Table),用于存放編譯期生成的各種字面量、符号引用,文字字元串、final變量值、類名和方法名常量,這部分内容将在類加載後存放到方法區的運作時常量池中。它們以數組形式通路,是調用方法、與類聯系及類的對象化的橋梁。

運作時常量池除了存放編譯期産生的Class檔案的常量外,還可存放在程式運作期間生成的新常量,比較常見增加新常量方法有String類的internd()方法。String.intern()是一個Native方法,它的作用是:如果運作時常量池中已經包含一個等于此String對象内容的字元串,則傳回常量池中該字元串的引用;如果沒有,則在常量池中建立與此String内容相同的字元串,并傳回常量池中建立的字元串的引用。不過JDK7的intern()方法的實作有所不同,當常量池中沒有該字元串時,不再是在常量池中建立與此String内容相同的字元串,而改為在常量池中記錄堆中首次出現的該字元串的引用,并傳回該引用。

但是,JDK1.7之前運作時常量池是方法區的一部分,JDK1.7及之後版本已經将運作時常量池從方法區中移了出來,在堆(Heap)中開辟了一塊區域存放運作時常量池。

字段資訊

字段資訊存放類中聲明的每一個字段(執行個體變量)的資訊,包括字段的名、類型、修飾符。如privateStringa=“”;則a為字段名,String為描述符,private為修飾符。

方法資訊

類中聲明的每一個方法的資訊,包括方法名、傳回值類型、參數類型、修飾符、異常、方法的位元組碼。(在編譯的時候,就已經将方法的局部變量表、操作數棧大小等完全确定并存放在位元組碼中,在加載載的時候,随着類一起裝入方法區。)在運作時,虛拟機線程調用方法時從常量池中獲得符号引用,然後在運作時解析成方法的實際位址,最後通過常量池中的全限定名、方法和字段描述符,把目前類或接口中的代碼與其它類或接口中的代碼聯系起來。

靜态變量

即類變量,被類的所有執行個體對象共享。方法區的靜态區專門存放靜态變量和靜态塊。

到類ClassLoader的引用

到該類的類裝載器的引用。

到類Class的引用

虛拟機為每一個被裝載的類型建立一個Class執行個體,用來代表這個被裝載的類。

Java虛拟機規範規定方法區可抛出OutOfMemoryError。

直接記憶體

直接記憶體(Direct memory)并不是JVM運作時資料區的一部分,也不是Java虛拟機規範中定義的記憶體區域。但這部分記憶體也被頻繁使用,而且它也可能導緻OutOfMemoryError異常出現。

本機直接記憶體的配置設定不會受到Java堆大小的限制,但是,還是會受到本機總記憶體(包括RAM及SWAP區或者分頁檔案)的大小及處理器尋址空間的限制,進而導緻動态擴充時出現OutOfMemoryError異常。

總結

類和對象在運作時的記憶體裡是怎麼樣的?以及各類型變量、方法在運作時是怎麼互動的?

  • 在程式運作時類是在方法區,執行個體對象本身在堆裡面。
  • 方法位元組碼在方法區。
  • 線程調用方法執行時建立棧幀并壓棧,方法的參數和局部變量在棧幀的局部變量表。
  • 對象的執行個體變量和對象一起在堆裡,是以各個線程都可以共享通路對象的執行個體變量。
  • 靜态變量在方法區,所有對象共享。字元串常量等常量在運作時常量池。
  • 各線程調用的方法,通過堆内的對象,方法區的靜态資料,可以共享互動資訊。

參考

1、周志明,深入了解Java虛拟機:JVM進階特性與最佳實踐,機械工業出版社