Java內存模型-堆和棧

Java內存模型-堆和棧

BangQ IT哈哈 java

  i.Java內存管理簡介:

  內存管理在Java語言中是JVM自動操做的,當JVM發現某些對象再也不須要的時候,就會對該對象佔用的內存進行重分配(釋放)操做,並且使得分配出來的內存可以提供給所須要的對象。在一些編程語言裏面,內存管理是一個程序的職責,可是書寫過C++的程序員很清楚,若是該程序須要本身來書寫頗有可能引發很嚴重的錯誤或者說不可預料的程序行爲,最終大部分開發時間都花在了調試這種程序以及修復相關錯誤上。通常狀況下在Java程序開發過程把手動內存管理稱爲顯示內存管理,而顯示內存管理常常發生的一個狀況就是引用懸掛——也就是說有可能在從新分配過程釋放掉了一個被某個對象引用正在使用的內存空間,釋放掉該空間事後,該引用就處於懸掛狀態。若是這個被懸掛引用指向的對象試圖進行原來對象(由於這個時候該對象有可能已經不存在了)進行操做的時候,因爲該對象自己的內存空間已經被手動釋放掉了,這個結果是不可預知的。顯示內存管理另一個常見的狀況是內存泄漏,當某些引用再也不引用該內存對象的時候,而該對象本來佔用的內存並無被釋放,這種狀況簡言爲內存泄漏。好比,若是針對某個鏈表進行了內存分配,而由於手動分配不當,僅僅讓引用指向了某個元素所處的內存空間,這樣就使得其餘鏈表中的元素不能再被引用並且使得這些元素所處的內存讓應用程序處於不可達狀態並且這些對象所佔有的內存也不可以被再使用,這個時候就發生了內存泄漏。而這種狀況一旦在程序中發生,就會一直消耗系統的可用內存直到可用內存耗盡,而針對計算機而言內存泄漏的嚴重程度大了會使得原本正常運行的程序直接由於內存不足而中斷,並非Java程序裏面出現Exception那麼輕量級。
  在之前的編程過程當中,手動內存管理帶了計算機程序不可避免的錯誤,並且這種錯誤對計算機程序是毀滅性的,因此內存管理就成爲了一個很重要的話題,可是針對大多數純面嚮對象語言而言,好比Java,提供了語言自己具備的內存特性:自動化內存管理,這種語言提供了一個程序垃圾回收器(Garbage Collector[GC]),自動內存管理提供了一個抽象的接口以及更加可靠的代碼使得內存可以在程序裏面進行合理的分配。最多見的狀況就是垃圾回收器避免了懸掛引用的問題,由於一旦這些對象沒有被任何引用「可達」的時候,也就是這些對象在JVM的內存池裏面成爲了避免可引用對象,該垃圾回收器會直接回收掉這些對象佔用的內存,固然這些對象必須知足垃圾回收器回收的某些對象規則,而垃圾回收器在回收的時候會自動釋放掉這些內存。不只僅如此,垃圾回收器一樣會解決內存泄漏問題。c++

  ii.詳解堆和棧[圖片以及部份內容來自《Inside JVM》]:

  1)通用簡介

  [編譯原理]學過編譯原理的人都明白,程序運行時有三種內存分配策略:靜態的、棧式的、堆式的
  靜態存儲——是指在編譯時就可以肯定每一個數據目標在運行時的存儲空間需求,於是在編譯時就能夠給它們分配固定的內存空間。這種分配策略要求程序代碼中不容許有可變數據結構的存在,也不容許有嵌套或者遞歸的結構出現,由於它們都會致使編譯程序沒法計算準確的存儲空間。
  棧式存儲——該分配可成爲動態存儲分配,是由一個相似於堆棧的運行棧來實現的,和靜態存儲的分配方式相反,在棧式存儲方案中,程序對數據區的需求在編譯時是徹底未知的,只有到了運行的時候才能知道,可是規定在運行中進入一個程序模塊的時候,必須知道該程序模塊所須要的數據區的大小才能分配其內存。和咱們在數據結構中所熟知的棧同樣,棧式存儲分配按照先進後出的原則進行分配。
  堆式存儲——堆式存儲分配則專門負責在編譯時或運行時模塊入口處都沒法肯定存儲要求的數據結構的內存分配,好比可變長度串和對象實例,堆由大片的可利用塊或空閒塊組成,堆中的內存能夠按照任意順序分配和釋放。
  [C++語言]對比C++語言裏面,程序佔用的內存分爲下邊幾個部分:
  [1]棧區(Stack):由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧。咱們在程序中定義的局部變量就是存放在棧裏,當局部變量的生命週期結束的時候,它所佔的內存會被自動釋放。
  [2]堆區(Heap):通常由程序員分配和釋放,若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式卻是相似於鏈表。咱們在程序中使用c++中new或者c中的malloc申請的一塊內存,就是在heap上申請的,在使用完畢後,是須要咱們本身動手釋放的,不然就會產生「內存泄露」的問題。
  [3]全局區(靜態區)(Static):全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另外一塊區域。程序結束後由系統釋放。
  [4]文字常量區:常量字符串就是放在這裏的,程序結束後由系統釋放。在Java中對應有一個字符串常量池。
  [5]程序代碼區:存放函數體的二進制代碼git

  2)JVM結構【堆、棧解析】:

  在Java虛擬機規範中,一個虛擬機實例的行爲主要描述爲:子系統、內存區域、數據類型和指令,這些組件在描述了抽象的JVM內部的一個抽象結構。與其說這些組成部分的目的是進行JVM內部結構的一種支配,更多的是提供一種嚴格定義實現的外部行爲,該規範定義了這些抽象組成部分以及相互做用的任何Java虛擬機執行所須要的行爲。下圖描述了JVM內部的一個結構,其中主要包括主要的子系統、內存區域,如同之前在《Java基礎知識》中描述的:Java虛擬機有一個類加載器做爲JVM的子系統,類加載器針對Class進行檢測以鑑定徹底合格的類接口,而JVM內部也有一個執行引擎:
Java內存模型-堆和棧
  當JVM運行一個程序的時候,它的內存須要用來存儲不少內容,包括字節碼、以及從類文件中提取出來的一些附加信息、以及程序中實例化的對象、方法參數、返回值、局部變量以及計算的中間結果。JVM的內存組織須要在不一樣的運行時數據區進行以上的幾個操做,下邊針對上圖裏面出現的幾個運行時數據區進行詳細解析:一些運行時數據區共享了全部應用程序線程和其餘特有的單個線程,每一個JVM實例有一個方法區和一個內存堆,這些是共同在虛擬機內運行的線程。在Java程序裏面,每一個新的線程啓動事後,它就會被JVM在內部分配本身的PC寄存器[PC registers](程序計數器器)和Java堆棧(Java stacks)。若該線程正在執行一個非本地Java方法,在PC寄存器的值指示下一條指令執行,該線程在Java內存棧中保存了非本地Java方法調用狀態,其狀態包括局部變量、被調用的參數、它的返回值、以及中間計算結果。而本地方法調用的狀態則是存儲在獨立的本地方法內存棧裏面(native method stacks),這種狀況下使得這些本地方法和其餘內存運行時數據區的內容儘量保證和其餘內存運行時數據區獨立,並且該方法的調用更靠近操做系統,這些方法執行的字節碼有可能根據操做系統環境的不一樣使得其編譯出來的本地字節碼的結構也有必定的差別。JVM中的內存棧是一個棧幀的組合,一個棧幀包含了某個Java方法調用的狀態,當某個線程調用方法的時候,JVM就會將一個新的幀壓入到Java內存棧,當方法調用完成事後,JVM將會從內存棧中移除該棧幀。JVM裏面不存在一個能夠存放中間計算數據結果值的寄存器,其內部指令集使用Java棧空間來存儲中間計算的數據結果值,這種作法的設計是爲了保持Java虛擬機的指令集緊湊,使得與寄存器原理可以緊密結合而且進行操做。
Java內存模型-堆和棧程序員

  1)方法區(Method Area)

  在JVM實例中,對裝載的類型信息是存儲在一個邏輯方法內存區中,當Java虛擬機加載了一個類型的時候,它會跟着這個Class的類型去路徑裏面查找對應的Class文件,類加載器讀取類文件(線性二進制數據),而後將該文件傳遞給Java虛擬機,JVM從二進制數據中提取信息而且將這些信息存儲在方法區,而類中聲明(靜態)變量就是來自於方法區中存儲的信息。在JVM裏面用什麼樣的方式存儲該信息是由JVM設計的時候決定的,例如:當數據進入方法的時候,多類文件字節的存儲量以Big-Endian(第一次最重要的字節)的順序存儲,儘管如此,一個虛擬機能夠用任何方式針對這些數據進行存儲操做,若它存儲在一個Little-Endian處理器上,設計的時候就有可能將多文件字節的值按照Little-Endian順尋存儲。算法

  ——【$Big-Endian和Little-Endian】——

  程序存儲數據過程當中,若是數據是跨越多個字節對象就必須有一種約定:編程

  • 它的地址是多少:對於跨越多個字節的對象,通常它所佔的字節都是連續的,它的地址等於它所佔字節最低地址,這種狀況鏈表可能存儲的僅僅是表頭
  • 它的字節在內存中是如何組織的
      好比:int x,它的地址爲0x100,那麼它佔據了內存中的0x100、0x10一、0x10二、0x103四個字節,因此通常狀況咱們以爲int是4個字節。上邊只是內存組織的一種狀況,多字節對象在內存中的組織有兩種約定,還有一種狀況:若一個整數爲W位,它的表示以下:
      每一位表示爲:[Xw-1,Xw-2,...,X1,X0]
      它的最高有效字節MSB(Most Significant Byte)爲:[Xw-1,Xw-2,...,Xw-8]
      最低有效字節LSB(Least Significant Byte)爲:[X7,X6,...,X0]
      其他字節則位於LSB和MSB之間
      LSB和MSB誰位於內存的最低地址,即表明了該對象的地址,這樣就引出了Big-Endian和Little-Endian的問題,若是LSB在MSB前,LSB是最低地址,則該機器是小端,反之則是大端。DES(Digital Equipment Corporation,如今是Compaq公司的一部分)和Intel機器(x86平臺)通常採用小端,IBM、Motorola(Power PC)、Sun的機器通常採用大端。固然這種不能表明全部狀況,有的CPU既能工做於小端、又能夠工做於大端,好比ARM、Alpha、摩托羅拉的PowerPC,這些狀況根據具體的處理器型號有所不一樣。可是大部分操做系統(Windows、FreeBSD、Linux)通常都是Little Endian的,少部分系統(Mac OS)是Big Endian的,因此用什麼方式存儲還得依賴宿主操做系統環境。
    Java內存模型-堆和棧
      由上圖能夠看到,映射訪問(「寫32位地址的0」)主要是由寄存器到內存、由內存到寄存器的一種數據映射方式,Big-Endian在上圖能夠看出的原子內存單位(Atomic Unit)在系統內存中的增加方向爲從左到右,而Little-Endian的地址增加方向爲從右到左。舉個例子:
      若要存儲數據0x0A0B0C0D:

      Big-Endian:

      以8位爲一個存儲單位,其存儲的地址增加爲:
    Java內存模型-堆和棧
      上圖中能夠看出MSB的值存儲了0x0A,這種狀況下數據的高位是從內存的低地址開始存儲的,而後從左到右開始增加,第二位0x0B就是存儲在第二位的,若是是按照16位爲一個存儲單位,其存儲方式又爲:
    Java內存模型-堆和棧
      則能夠看到Big-Endian的映射地址方式爲:
    Java內存模型-堆和棧數組

  MSB:在計算機中,最高有效位(MSB)是指位值的存儲位置爲轉換爲二進制數據後的最大值,MSB有時候在Big-Endian的架構中稱爲最左最大數據位,這種狀況下再往左邊的內存位則不是數據位了,而是有效位數位置的最高符號位,不只僅如此,MSB也能夠對應一個二進制符號位的符號位補碼標記:「1」的含義爲負,「0」的含義爲正。最高位表明了「最重要字節」,也就是說當某些多字節數據擁有了最大值的時候它就是存儲的時候最高位數據的字節對應的內存位置:
Java內存模型-堆和棧
  Little-Endian:
  與Big-Endian相對的就是Little-Endian的存儲方式,一樣按照8位爲一個存儲單位上邊的數據0x0A0B0C0D存儲格式爲:
Java內存模型-堆和棧
  能夠看到LSB的值存儲的0x0D,也就是數據的最低位是從內存的低地址開始存儲的,它的高位是從右到左的順序逐漸增長內存分配空間進行存儲的,若是按照十六位爲存儲單位存儲格式爲:
Java內存模型-堆和棧
  從上圖能夠看到最低的16位的存儲單位裏面存儲的值爲0x0C0D,接着纔是0x0A0B,這樣就能夠看到按照數據從高位到低位在內存中存儲的時候是從右到左進行遞增存儲的,實際上能夠從寫內存的順序來理解,實際上數據存儲在內存中無非在使用的時候是寫內存和讀內存,針對LSB的方式最好的書面解釋就是向左增長來看待,若是真正在進行內存讀寫的時候使用這樣的順序,其意義就體現出來了:
Java內存模型-堆和棧
  按照這種讀寫格式,0x0D存儲在最低內存地址,而從右往左的增加就能夠看到LSB存儲的數據爲0x0D,和初衷吻合,則十六位的存儲就能夠按照下邊的格式來解釋:
Java內存模型-堆和棧
  實際上從上邊的存儲還會考慮到另一個問題,若是按照這種方式從右往左的方式進行存儲,若是是遇到Unicode文字就和從左到右的語言顯示方式相反。好比一個單詞「XRAY」,使用Little-Endian的方式存儲格式爲:
Java內存模型-堆和棧
  使用這種方式進行內存讀寫的時候就會發現計算機語言和語言自己的順序會有衝突,這種衝突主要是以使用語言的人的習慣有關,而書面化的語言從左到右就能夠知道其衝突是不可避免的。咱們通常使用語言的閱讀方式都是從左到右,而低端存儲(Little-Endian)的這種內存讀寫的方式使得咱們最終從計算機裏面讀取字符須要進行倒序,並且考慮另一個問題,若是是針對中文而言,一個字符是兩個字節,就會出現總體順序和每個位的順序會進行兩次倒序操做,這種方式真正在製做處理器的時候也存在一種計算上的衝突,而針對使用文字從左到右進行閱讀的國家而言,從右到左的方式(Big-Endian)則會有這樣的文字衝突,另一方面,儘管有不少國家使用語言是從右到左,可是僅僅和Big-Endian的方式存在衝突,這些國家畢竟佔少數,因此能夠理解的是,爲何主流的系統都是使用的Little-Endian的方式
  *:這裏不解釋Middle-Endian的方式以及Mixed-Endian的方式】**
  LSB:在計算機中,最低有效位是一個二進制給予單位的整數,位的位置肯定了該數據是一個偶數仍是奇數,LSB有時被稱爲最右位。在使用具體位二進制數以內,常見的存儲方式就是每一位存儲1或者0的方式,從0向上到1每一比特逢二進一的存儲方式。LSB的這種特性用來指定單位位,而不是位的數字,而這種方式也有可能產生必定的混亂。
Java內存模型-堆和棧安全

  ——以上是關於Big-Endian和Little-Endian的簡單講解——

  JVM虛擬機將搜索和使用類型的一些信息也存儲在方法區中以方便應用程序加載讀取該數據。設計者在設計過程也考慮到要方便JVM進行Java應用程序的快速執行,而這種取捨主要是爲了程序在運行過程當中內存不足的狀況可以經過必定的取捨去彌補內存不足的狀況。在JVM內部,全部的線程共享相同的方法區,所以,訪問方法區的數據結構必須是線程安全的,若是兩個線程都試圖去調用去找一個名爲Lava的類,好比Lava尚未被加載,只有一個線程能夠加載該類而另外的線程只可以等待。方法區的大小在分配過程當中是不固定的,隨着Java應用程序的運行,JVM能夠調整其大小,須要注意一點,方法區的內存不須要是連續的,由於方法區內存能夠分配在內存堆中,即便是虛擬機JVM實例對象本身所在的內存堆也是可行的,而在實現過程是容許程序員自身來指定方法區的初始化大小的。
  一樣的,由於Java自己的自動內存管理,方法區也會被垃圾回收的,Java程序能夠經過類擴展動態加載器對象,類能夠成爲「未引用」向垃圾回收器進行申請,若是一個類是「未引用」的,則該類就可能被卸載,
  而方法區針對具體的語言特性有幾種信息是存儲在方法區內的:數據結構

  【類型信息】:

  • 類型的徹底限定名(java.lang.String格式)
  • 類型的徹底限定名的直接父類的徹底限定名(除非這個父類的類型是一個接口或者java.lang.Object)
  • 不論類型是一個類或者接口
  • 類型的修飾符(例如public、abstract、final)
  • 任何一個直接超類接口的徹底限定名的列表
      在JVM和類文件名的內部,類型名通常都是徹底限定名(java.lang.String)格式,在Java源文件裏面,徹底限定名必須加入包前綴,而不是咱們在開發過程寫的簡單類名,而在方法上,只要是符合Java語言規範的類的徹底限定名均可以,而JVM可能直接進行解析,好比:(java.lang.String)在JVM內部名稱爲java/lang/String,這就是咱們在異常捕捉的時候常常看到的ClassNotFoundException的異常裏面類信息的名稱格式。
      除此以外,還必須爲每一種加載過的類型在JVM內進行存儲,下邊的信息不存儲在方法區內,下邊的章節會一一說明
  • 類型常量池
  • 字段信息
  • 方法信息
  • 全部定義在Class內部的(靜態)變量信息,除開常量
  • 一個ClassLoader的引用
  • Class的引用

      【常量池】

      針對類型加載的類型信息,JVM將這些存儲在常量池裏,常量池是一個根據類型定義的常量的有序常量集,包括字面量(String、Integer、Float常量)以及符號引用(類型、字段、方法),整個長量池會被JVM的一個索引引用,如同數組裏面的元素集合按照索引訪問同樣,JVM針對這些常量池裏面存儲的信息也是按照索引方式進行。實際上長量池在Java程序的動態連接過程起到了一個相當重要的做用。多線程

      【字段信息】

      針對字段的類型信息,下邊的信息是存儲在方法區裏面的:

  • 字段名
  • 字段類型
  • 字段修飾符(public,private,protected,static,final,volatile,transient)

      【方法信息】

      針對方法信息,下邊信息存儲在方法區上:

  • 方法名
  • 方法的返回類型(包括void)
  • 方法參數的類型、數目以及順序
  • 方法修飾符(public,private,protected,static,final,synchronized,native,abstract)
     針對非本地方法,還有些附加方法信息須要存儲在方法區內:
  • 方法字節碼
  • 方法中局部變量區的大小、方法棧幀
  • 異常表

      【類變量】

      類變量在一個類的多個實例之間共享,這些變量直接和類相關,而不是和類的實例相關,(定義過程簡單理解爲類裏面定義的static類型的變量),針對類變量,其邏輯部分就是存儲在方法區內的。在JVM使用這些類以前,JVM先要在方法區裏面爲定義的non-final變量分配內存空間;常量(定義爲final)則在JVM內部則不是以一樣的方式來進行存儲的,儘管針對常量而言,一個final的類變量是擁有它本身的常量池,做爲常量池裏面的存儲某部分,類常量是存儲在方法區內的,而其邏輯部分則不是按照上邊的類變量的方式來進行內存分配的。雖然non-final類變量是做爲這些類型聲明中存儲數據的某一部分,final變量存儲爲任何使用它類型的一部分的數據格式進行簡單存儲。

      【ClassLoader引用】

      對於每種類型的加載,JVM必須檢測其類型是否符合了JVM的語言規範,對於經過類加載器加載的對象類型,JVM必須存儲對類的引用,而這些針對類加載器的引用是做爲了方法區裏面的類型數據部分進行存儲的。

      【類Class的引用】

      JVM在加載了任何一個類型事後會建立一個java.lang.Class的實例,虛擬機必須經過必定的途徑來引用該類型對應的一個Class的實例,而且將其存儲在方法區內

      【方法表】

      爲了提升訪問效率,必須仔細的設計存儲在方法區中的數據信息結構。除了以上討論的結構,jvm的實現者還添加一些其餘的數據結構,如方法表【下邊會說明】。

      2)內存棧(Stack):

      當一個新線程啓動的時候,JVM會爲Java線程建立每一個線程的獨立內存棧,如前所言Java的內存棧是由棧幀構成,棧幀自己處於遊離狀態,在JVM裏面,棧幀的操做只有兩種:出棧和入棧。正在被線程執行的方法通常稱爲當前線程方法,而該方法的棧幀就稱爲當前幀,而在該方法內定義的類稱爲當前類,常量池也稱爲當前常量池。當執行一個方法如此的時候,JVM保留當前類和當前常量池的跟蹤,當虛擬機遇到了存儲在棧幀中的數據上的操做指令的時候,它就執行當前幀的操做。當一個線程調用某個Java方法時,虛擬機建立而且將一個新幀壓入到內存堆棧中,而這個壓入到內存棧中的幀成爲當前棧幀,當該方法執行的時候,JVM使用內存棧來存儲參數、局部變量、中間計算結果以及其餘相關數據。方法在執行過程有可能由於兩種方式而結束:若是一個方法返回完成就屬於方法執行的正常結束,若是在這個過程拋出異常而結束,能夠稱爲非正常結束,不管是正常結束仍是異常結束,JVM都會彈出或者丟棄該棧幀,則上一幀的方法就成爲了當前幀。
      在JVM中,Java線程的棧數據是屬於某個線程獨有的,其餘的線程不可以修改或者經過其餘方式來訪問該線程的棧幀,正由於如此這種狀況不用擔憂多線程同步訪問Java的局部變量,當一個線程調用某個方法的時候,方法的局部變量是在方法內部進行的Java棧幀的存儲,只有當前線程能夠訪問該局部變量,而其餘線程不能隨便訪問該內存棧裏面存儲的數據。內存棧內的棧幀數據和方法區以及內存堆同樣,Java棧的棧幀不須要分配在連續的堆棧內,或者說它們多是在堆,或者二者組合分配,實際數據用於表示Java堆棧和棧幀結構是JVM自己的設計結構決定的,並且在編程過程能夠容許程序員指定一個用於Java堆棧的初始大小以及最大、最小尺寸。

      【概念區分】

  • 內存棧:這裏的內存棧和物理結構內存堆棧有點點區別,是內存裏面數據存儲的一種抽象數據結構。從操做系統上講,在程序執行過程對內存的使用自己經常使用的數據結構就是內存堆棧,而這裏的內存堆棧指代的就是JVM在使用內存過程整個內存的存儲結構,多指內存的物理結構,而Java內存棧不是指代的一個物理結構,更多的時候指代的是一個抽象結構,就是符合JVM語言規範的內存棧的一個抽象結構。由於物理內存堆棧結構和Java內存棧的抽象模型結構自己比較類似,因此咱們在學習過程就正常把這兩種結構放在一塊兒考慮了,並且兩者除了概念上有一點點小的區別,理解成爲一種結構對於初學者也何嘗不可,因此實際上也能夠以爲兩者沒有太大的本質區別。可是在學習的時候最好分清楚內存堆棧和Java內存棧的一小點細微的差距,前者是物理概念和自己模型,後者是抽象概念和自己模型的一個共同體。而內存堆棧更多的說法能夠理解爲一個內存塊,由於內存塊能夠經過索引和指針進行數據結構的組合,內存棧就是內存塊針對數據結構的一種表示,而內存堆則是內存塊的另一種數據結構的表示,這樣理解更容易區份內存棧和內存堆棧(內存塊)的概念。
  • 棧幀:棧幀是內存棧裏面的最小單位,指的是內存棧裏面每個最小內存存儲單元,它針對內存棧僅僅作了兩個操做:入棧和出棧,通常狀況下:所說的堆棧幀和棧幀卻是一個概念,因此在理解上記得加以區分
  • 內存堆:這裏的內存堆和內存棧是相對應的,其實內存堆裏面的數據也是存儲在系統內存堆棧裏面的,只是它使用了另一種方式來進行堆裏面內存的管理,而本章題目要講到的就是Java語言自己的內存堆和內存棧,而這兩個概念都是抽象的概念模型,並且是相對的。
      棧幀:棧幀主要包括三個部分:局部變量、操做數棧幀(操做幀)和幀數據(數據幀)。本地變量和操做數幀的大小取決於須要,這些大小是在編譯時就決定的,而且在每一個方法的類文件數據中進行分配,幀的數據大小則不同,它雖然也是在編譯時就決定的可是它的大小和自己代碼實現有關。當JVM調用一個Java方法的時候,它會檢查類的數據來肯定在本地變量和操做方法要求的棧大小,它計算該方法所須要的內存大小,而後將這些數據分配好內存空間壓入到內存堆棧中。
      棧幀——局部變量:局部變量是以Java棧幀組合成爲的一個以零爲基的數組,使用局部變量的時候使用的其實是一個包含了0的一個基於索引的數組結構。int類型、float、引用以及返回值都佔據了一個數組中的局部變量的條目,而byte、short、char則在存儲到局部變量的時候是先轉化成爲int再進行操做的,則long和double則是在這樣一個數組裏面使用了兩個元素的空間大小,在局部變量裏面存儲基本數據類型的時候使用的就是這樣的結構。舉個例子:
class Example3a{
    public static int runClassMethod(int i,long l,float f,double d,Object o,byte b)
    {
        return 0;
    }
    public int runInstanceMethod(char c,double d,short s,boolean b)
    {
        return 0;
    }
}

Java內存模型-堆和棧

  棧幀——操做幀:和局部變量同樣,操做幀也是一組有組織的數組的存儲結構,可是和局部變量不同的是這個不是經過數組的索引訪問的,而是直接進行的入棧和出棧的操做,當操做指令直接壓入了操做棧幀事後,從棧幀裏面出來的數據會直接在出棧的時候被讀取和使用。除了程序計數器之外,操做幀也是能夠直接被指令訪問到的,JVM裏面沒有寄存器。處理操做幀的時候Java虛擬機是基於內存棧的而不是基於寄存器的,由於它在操做過程是直接對內存棧進行操做而不是針對寄存器進行操做。而JVM內部的指令也能夠來源於其餘地方好比緊接着操做符以及操做數的字節碼流或者直接從常量池裏面進行操做。JVM指令其實真正在操做過程的焦點是集中在內存棧棧幀的操做幀上的。JVM指令將操做幀做爲一個工做空間,有許多指令都是從操做幀裏面出棧讀取的,對指令進行操做事後將操做幀的計算結果從新壓入內存堆棧內。好比iadd指令將兩個整數壓入到操做幀裏面,而後將兩個操做數進行相加,相加的時候從內存棧裏面讀取兩個操做數的值,而後進行運算,最後將運算結果從新存入到內存堆棧裏面。舉個簡單的例子:
begin

iload_0 //將整數類型的局部變量0壓入到內存棧裏面
iload_1 //將整數類型的局部變量1壓入到內存棧裏面
iadd     //將兩個變量出棧讀取,而後進行相加操做,將結果從新壓入棧中
istore_2 //將最終輸出結果放在另一個局部變量裏面

end

  綜上所述,就是整個計算過程針對內存的一些操做內容,而總體的結構能夠用下圖來描述:
Java內存模型-堆和棧
  棧幀——數據幀:除了局部變量和操做幀之外,Java棧幀還包括了數據幀,用於支持常量池、普通的方法返回以及異常拋出等,這些數據都是存儲在Java內存棧幀的數據幀中的。不少JVM的指令集實際上使用的都是常量池裏面的一些條目,一些指令,只是把int、long、float、double或者String從常量池裏面壓入到Java棧幀的操做幀上邊,一些指令使用常量池來管理類或者數組的實例化操做、字段的訪問控制、或者方法的調用,其餘的指令就用來決定常量池條目中記錄的某一特定對象是否某一類或者常量池項中指定的接口。常量池會判斷類型、字段、方法、類、接口、類字段以及引用是如何在JVM進行符號化描述,而這個過程由JVM自己進行對應的判斷。這裏就能夠理解JVM如何來判斷咱們一般說的:「原始變量存儲在內存棧上,而引用的對象存儲在內存堆上邊。」除了常量池判斷幀數據符號化描述特性之外,這些數據幀必須在JVM正常執行或者異常執行過程輔助它進行處理操做。若是一個方法是正常結束的,JVM必須恢復棧幀調用方法的數據幀,並且必須設置PC寄存器指向調用方法後邊等待的指令完成該調用方法的位置。若是該方法存在返回值,JVM也必須將這個值壓入到操做幀裏面以提供給須要這些數據的方法進行調用。不只僅如此,數據幀也必須提供一個方法調用的異常表,當JVM在方法中拋出異常而非正常結束的時候,該異常表就用來存放異常信息。

  3)內存堆(Heap):

  當一個Java應用程序在運行的時候在程序中建立一個對象或者一個數組的時候,JVM會針對該對象和數組分配一個新的內存堆空間。可是在JVM實例內部,只存在一個內存堆實例,全部的依賴該JVM的Java應用程序都須要共享該堆實例,而Java應用程序自己在運行的時候它本身包含了一個由JVM虛擬機實例分配的本身的堆空間,而在應用程序啓動的時候,任何一個Java應用程序都會獲得JVM分配的堆空間,並且針對每個Java應用程序,這些運行Java應用程序的堆空間都是相互獨立的。這裏所說起到的共享堆實例是指JVM在初始化運行的時候總體堆空間只有一個,這個是Java語言平臺直接從操做系統上可以拿到的總體堆空間,因此的依賴該JVM的程序均可以獲得這些內存空間,可是針對每個獨立的Java應用程序而言,這些堆空間是相互獨立的,每個Java應用程序在運行最初都是依靠JVM來進行堆空間的分配的。即便是兩個相同的Java應用程序,一旦在運行的時候處於不一樣的操做系統進程(通常爲java.exe)中,它們各自分配的堆空間都是獨立的,不能相互訪問,只是兩個Java應用進程初始化拿到的堆空間來自JVM的分配,而JVM是從最初的內存堆實例裏面分配出來的。在同一個Java應用程序裏面若是出現了不一樣的線程,則是能夠共享每個Java應用程序拿到的內存堆空間的,這也是爲何在開發多線程程序的時候,針對同一個Java應用程序必須考慮線程安全問題,由於在一個Java進程裏面全部的線程是能夠共享這個進程拿到的堆空間的數據的。可是Java內存堆有一個特性,就是JVM擁有針對新的對象分配內存的指令,可是它卻不包含釋放該內存空間的指令,固然開發過程能夠在Java源代碼中顯示釋放內存或者說在JVM字節碼中進行顯示的內存釋放,可是JVM僅僅只是檢測堆空間中是否有引用不可達(不能夠引用)的對象,而後將接下來的操做交給垃圾回收器來處理。

  對象表示:

  JVM規範裏面並無說起到Java對象如何在堆空間中表示和描述,對象表示能夠理解爲設計JVM的工程師在最初考慮到對象調用以及垃圾回收器針對對象的判斷而獨立的一種Java對象在內存中的存儲結構,該結構是由設計最初考慮的。針對一個建立的類實例而言,它內部定義的實例變量以及它的超類以及一些相關的核心數據,是必須經過必定的途徑進行該對象內部存儲以及表示的。當開發過程給定了一個對象引用的時候,JVM必須可以經過這個引用快速從對象堆空間中去拿到該對象可以訪問的數據內容。也就是說,堆空間內對象的存儲結構必須爲外圍對象引用提供一種能夠訪問該對象以及控制該對象的接口使得引用可以順利地調用該對象以及相關操做。所以,針對堆空間的對象,分配的內存中每每也包含了一些指向方法區的指針,由於從總體存儲結構上講,方法區彷佛存儲了不少原子級別的內容,包括方法區內最原始最單一的一些變量:好比類字段、字段數據、類型數據等等。而JVM自己針對堆空間的管理存在兩種設計結構:

  【1】設計一:

  堆空間的設計能夠劃分爲兩個部分:一個處理池和一個對象池,一個對象的引用能夠拿處處理池的一個本地指針,而處理池主要分爲兩個部分:一個指向對象池裏面的指針以及一個指向方法區的指針。這種結構的優點在於JVM在處理對象的時候,更加可以方便地組合堆碎片以使得全部的數據被更加方便地進行調用。當JVM須要將一個對象移動到對象池的時候,它僅僅須要更新該對象的指針到一個新的對象池的內存地址中就能夠完成了,而後在處理池中針對該對象的內部結構進行相對應的處理工做。不過這樣的方法也會出現一個缺點就是在處理一個對象的時候針對對象的訪問須要提供兩個不一樣的指針,這一點可能很差理解,其實能夠這樣講,真正在對象處理過程存在一個根據時間戳有區別的對象狀態,而對象在移動、更新以及建立的整個過程當中,它的處理池裏面老是包含了兩個指針,一個指針是指向對象內容自己,一個指針是指向了方法區,由於一個完整的對外的對象是依靠這兩部分被引用指針引用到的,而咱們開發過程是不可以操做處理池的兩個指針的,只有引用指針咱們能夠經過外圍編程拿到。若是Java是按照這種設計進行對象存儲,這裏的引用指針就是平時說起到的「Java的引用」,只是JVM在引用指針還作了必定的封裝,這種封裝的規則是JVM自己設計的時候作的,它就經過這種結構在外圍進行一次封裝,好比Java引用不具有直接操做內存地址的能力就是該封裝的一種限制規則。這種設計的結構圖以下:
Java內存模型-堆和棧

  【2】設計二:

  另一種堆空間設計就是使用對象引用拿到的本地指針,將該指針直接指向綁定好的對象的實例數據,這些數據裏面僅僅包含了一個指向方法區原子級別的數據去拿到該實例相關數據,這種狀況下只須要引用一個指針來訪問對象實例數據,可是這樣的狀況使得對象的移動以及對象的數據更新變得更加複雜。當JVM須要移動這些數據以及進行堆內存碎片的整理的時候,就必須直接更新該對象全部運行時的數據區,這種狀況能夠用下圖進行表示:
Java內存模型-堆和棧
  JVM須要從一個對象引用來得到該引用可以引用的對象數據存在多個緣由,當一個程序試圖將一個對象的引用轉換成爲另一個類型的時候,JVM就會檢查兩個引用指向的對象是否存在父子類關係,而且檢查兩個引用引用到的對象是否可以進行類型轉換,並且全部這種類型的轉換必須執行一樣的一個操做:instanceof操做,在上邊兩種狀況下,JVM都必需要去分析引用指向的對象內部的數據。當一個程序調用了一個實例方法的時候,JVM就必須進行動態綁定操做,它必須選擇調用方法的引用類型,是一個基於類的方法調用仍是一個基於對象的方法調用,要作到這一點,它又要獲取該對象的惟一引用才能夠。無論對象的實現是使用什麼方式來進行對象描述,都是在針對內存中關於該對象的方法表進行操做,由於使用這樣的方式加快了實例針對方法的調用,並且在JVM內部實現的時候這樣的機制使得其運行表現比較良好,因此方法表的設計在JVM總體結構中發揮了極其重要的做用。關於方法表的存在與否,在JVM規範裏面沒有嚴格說明,也有可能真正在實現過程只是一個抽象概念,物理層它根本不存在,針對放發表實現對於一個建立的實例而言,它自己具備不過高的內存須要求,若是該實現裏面使用了方法表,則對象的方法表應該是能夠很快被外圍引用訪問到的。
  有一種辦法就是經過對象引用鏈接到方法表的時候,以下圖:
Java內存模型-堆和棧
  該圖代表,在每一個指針指向一個對象的時候,其實是使用的一個特殊的數據結構,這些特殊的結構包括幾個部分:

  • 一個指向該對象類全部數據的指針
  • 該對象的方法表
      實際上從圖中能夠看出,方法表就是一個指針數組,它的每個元素包含了一個指針,針對每一個對象的方法均可以直接經過該指針在方法區中找到匹配的數據進行相關調用,而這些方法表須要包括的內容以下:
  • 方法內存堆棧段空間中操做棧的大小以及局部變量
  • 方法字節碼
  • 一個方法的異常表
      這些信息使得JVM足夠針對該方法進行調用,在調用過程,這種結構也可以方便子類對象的方法直接經過指針引用到父類的一些方法定義,也就是說指針在內存空間以內經過JVM自己的調用使得父類的一些方法表也能夠一樣的方式被調用,固然這種調用過程避免不了兩個對象之間的類型檢查,可是這樣的方式就使得繼承的實現變得更加簡單,並且方法表提供的這些數據足夠引用對對象進行帶有任何OO特徵的對象操做。
      另一種數據在上邊的途中沒有顯示出來,也是從邏輯上講內存堆中的對象的真實數據結構——對象的鎖。這一點可能須要關聯到JMM模型中講的進行理解。JVM中的每個對象都是和一個鎖(互斥)相關聯的,這種結構使得該對象能夠很容易支持多線程訪問,並且該對象的對象鎖一次只能被一個線程訪問。當一個線程在運行的時候具備某個對象的鎖的時候,僅僅只有這個線程能夠訪問該對象的實例變量,其餘線程若是須要訪問該實例的實例變量就必須等待這個線程將它佔有的對象鎖釋放事後纔可以正常訪問,若是一個線程請求了一個被其餘線程佔有的對象鎖,這個請求線程也必須等到該鎖被釋放事後纔可以拿到這個對象的對象鎖。一旦這個線程擁有了一個對象鎖事後,它本身能夠屢次向同一個鎖發送對象的鎖請求,可是若是它要使得被該線程鎖住的對象能夠被其餘鎖訪問到的話就須要一樣的釋放鎖的次數,好比線程A請求了對象B的對象鎖三次,那麼A將會一直佔有B對象的對象鎖,直到它將該對象鎖釋放了三次。
      不少對象也可能在整個生命週期都沒有被對象鎖鎖住過,在這樣的狀況下對象鎖相關的數據是不須要對象內部實現的,除非有線程向該對象請求了對象鎖,不然這個對象就沒有該對象鎖的存儲結構。因此上邊的實現圖能夠知道,不少實現不包括指向對象鎖的「鎖數據」,鎖數據的實現必需要等待某個線程向該對象發送了對象鎖請求事後,並且是在第一次鎖請求事後纔會被實現。這個結構中,JVM卻可以間接地經過一些辦法針對對象的鎖進行管理,好比把對象鎖放在基於對象地址的搜索樹上邊。實現了鎖結構的對象中,每個Java對象邏輯上都在內存中成爲了一個等待集,這樣就使得全部的線程在鎖結構裏面針對對象內部數據能夠獨立操做,等待集就使得每一個線程可以獨立於其餘線程去完成一個共同的設計目標以及程序執行的最終結果,這樣就使得多線程的線程獨享數據以及線程共享數據機制很容易實現。
      不只僅如此,針對內存堆對象還必須存在一個對象的鏡像,該鏡像的主要目的是提供給垃圾回收器進行監控操做,垃圾回收器是經過對象的狀態來判斷該對象是否被應用,一樣它須要針對堆內的對象進行監控。而當監控過程垃圾回收器收到對象回收的事件觸發的時候,雖然使用了不一樣的垃圾回收算法,不論使用什麼算法都須要經過獨有的機制來判斷對象目前處於哪一種狀態,而後根據對象狀態進行操做。開發過程程序員每每不會去仔細分析當一個對象引用設置成爲null了事後虛擬機內部的操做,但實際上Java裏面的引用每每不像咱們想像中那麼簡單,Java引用中的虛引用、弱引用就是使得Java引用在顯示提交可回收狀態的狀況下對內存堆中的對象進行的反向監控,這些引用能夠監視到垃圾回收器回收該對象的過程。垃圾回收器自己的實現也是須要內存堆中的對象可以提供相對應的數據的。其實這個位置到底JVM裏面是否使用了完整的Java對象的鏡像仍是使用的一個鏡像索引我沒有去仔細分析過,總之是在堆結構裏面存在着堆內對象的一個相似拷貝的鏡像機制,使得垃圾回收器可以順利回收再也不被引用的對象。

      4)內存棧和內存堆的實現原理探測【該部分爲不肯定概念】:

      實際上不管是內存棧結構、方法區仍是內存堆結構,歸根到底使用的是操做系統的內存,操做系統的內存結構能夠理解爲內存塊,經常使用的抽象方式就是一個內存堆棧,而JVM在OS上邊安裝了事後,就在啓動Java程序的時候按照配置文件裏面的內容向操做系統申請內存空間,該內存空間會按照JVM內部的方法提供相應的結構調整。
      內存棧應該是很容易理解的結構實現,通常狀況下,內存棧是保持連續的,可是不絕對,內存棧申請到的地址實際上不少狀況下都是連續的,而每一個地址的最小單位是按照計算機位來算的,該計算機位裏面只有兩種狀態1和0,而內存棧的使用過程就是典型的相似C++裏面的普通指針結構的使用過程,直接針對指針進行++或者--操做就修改了該指針針對內存的偏移量,而這些偏移量就使得該指針能夠調用不一樣的內存棧中的數據。至於針對內存棧發送的指令就是常見的計算機指令,而這些指令就使得該指針針對內存棧的棧幀進行指令發送,好比發送操做指令、變量讀取等等,直接就使得內存棧的調用變得更加簡單,並且棧幀在接受了該數據事後就知道到底針對棧幀內部的哪個部分進行調用,是操做幀、數據幀仍是局部變量。
      內存堆實際上在操做系統裏面使用了雙向鏈表的數據結構,雙向鏈表的結構使得即便內存堆不具備連續性,每個堆空間裏面的鏈表也能夠進入下一個堆空間,而操做系統自己在整理內存堆的時候會作一些簡單的操做,而後經過每個內存堆的雙向鏈表就使得內存堆更加方便。並且堆空間不須要有序,甚至說有序不影響堆空間的存儲結構,由於它歸根究竟是在內存塊上邊進行實現的,內存塊自己是一個堆棧結構,只是該內存堆棧裏面的塊如何分配不禁JVM決定,是由操做系統已經最開始分配好了,也就是最小存儲單位。而後JVM拿到從操做系統申請的堆空間事後,先進行初始化操做,而後就能夠直接使用了。
      常見的對程序有影響的內存問題主要是兩種:溢出和內存泄漏,上邊已經講過了內存泄漏,其實從內存的結構分析,泄漏這種狀況很難甚至說不可能發生在棧空間裏面,其主要緣由是棧空間自己很難出現懸停的內存,由於棧空間的存儲結構有多是內存的一個地址數組,因此在訪問棧空間的時候使用的都是索引或者下標或者就是最原始的出棧和入棧的操做,這些操做使得棧裏面很難出現像堆空間同樣的內存懸停(也就是引用懸掛)問題。堆空間懸停的內存是由於棧中存放的引用的變化,其實引用能夠理解爲從棧到堆的一個指針,當該指針發生變化的時候,堆內存碎片就有可能產生,而這種狀況下在原始語言裏面就常常發生內存泄漏的狀況,由於這些懸停的堆空間在系統裏面是不可以被任何本地指針引用到,就使得這些對象在未被回收的時候脫離了可操做區域而且佔用了系統資源。
      棧溢出問題一直都是計算機領域裏面的一個安全性問題,這裏不作深刻討論,說多了就偏離主題了,而內存泄漏是程序員最容易理解的內存問題,還有一個問題來自於我一個***朋友就是:堆溢出現象,這種現象可能更加複雜。
      其實Java裏面的內存結構,最初看來就是堆和棧的結合,實際上能夠這樣理解,實際上對象的實際內容才存在對象池裏面,而有關對象的其餘東西有可能會存儲於方法區,而平時使用的時候的引用是存在內存棧上的,這樣就更加容易理解它內部的結構,不只僅如此,有時候還須要考慮到Java裏面的一些字段和屬性究竟是對象域的仍是類域的,這個也是一個比較複雜的問題。

      兩者的區別簡單總結一下:

  • 管理方式:JVM本身能夠針對內存棧進行管理操做,並且該內存空間的釋放是編譯器就能夠操做的內容,而堆空間在Java中JVM自己執行引擎不會對其進行釋放操做,而是讓垃圾回收器進行自動回收
  • 空間大小:通常狀況下棧空間相對於堆空間而言比較小,這是由棧空間裏面存儲的數據以及自己須要的數據特性決定的,而堆空間在JVM堆實例進行分配的時候通常大小都比較大,由於堆空間在一個Java程序中須要存儲太多的Java對象數據
  • 碎片相關:針對堆空間而言,即便垃圾回收器可以進行自動堆內存回收,可是堆空間的活動量相對棧空間而言比較大,頗有可能存在長期的堆空間分配和釋放操做,並且垃圾回收器不是實時的,它有可能使得堆空間的內存碎片主鍵累積起來。針對棧空間而言,由於它自己就是一個堆棧的數據結構,它的操做都是一一對應的,並且每個最小單位的結構棧幀和堆空間內複雜的內存結構不同,因此它通常在使用過程不多出現內存碎片。
  • 分配方式:通常狀況下,棧空間有兩種分配方式:靜態分配和動態分配,靜態分配是自己由編譯器分配好了,而動態分配可能根據狀況有所不一樣,而堆空間倒是徹底的動態分配的,是一個運行時級別的內存分配。而棧空間分配的內存不須要咱們考慮釋放問題,而堆空間即便在有垃圾回收器的前提下仍是要考慮其釋放問題。
  • 效率:由於內存塊自己的排列就是一個典型的堆棧結構,因此棧空間的效率天然比起堆空間要高不少,並且計算機底層內存空間自己就使用了最基礎的堆棧結構使得棧空間和底層結構更加符合,它的操做也變得簡單就是最簡單的兩個指令:入棧和出棧;棧空間針對堆空間而言的弱點是靈活程度不夠,特別是在動態管理的時候。而堆空間最大的優點在於動態分配,由於它在計算機底層實現多是一個雙向鏈表結構,因此它在管理的時候操做比棧空間複雜不少,天然它的靈活度就高了,可是這樣的設計也使得堆空間的效率不如棧空間,並且低不少。
相關文章
相關標籤/搜索