Java虛擬機--Java運行時數據區

java運行時數據區域

1.java運行時數據區域

       Java 虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些數據區域有各自的用途,以及建立和銷燬的時間,有的內存區域隨着虛擬機進程啓動而一直存在,有些區域則是依賴用戶線程的啓動和結束而創建和銷燬。Java虛擬機所管理的內存主要包括如下幾個運行時數據區域: java

在這裏插入圖片描述

1.1 程序計數器

       程序計數器是一塊較小的內存空間,它能夠看作是當前線程所執行的字節碼的行號指示器。在Java虛擬機的概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
        因爲Java虛擬機中多線程是經過輪流切換、分配處理執行時間的方式來實現的。在任什麼時候刻,一個處理器智能執行一條線程中的指令,即每一個線程都有獨立的線程計數器,各條線程之間計數器互不影響,獨立存儲,因此程序計數器是線程私有的。
       若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地 址;若是正在執行的是本地(Native)方法,這個計數器值則應爲空(Undefined)。此內存區域是惟 一一個在《Java虛擬機規範》中沒有規定任何OutOfMemoryError狀況的區域算法

1.2java虛擬機棧

       虛擬機棧描述的是Java方法執行的線程內存模型;與程序計數器同樣,也是屬於線程私有的,生命週期與線程相同;每一個方法被執行的時候,java虛擬機都會同步建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。一個方法被調用直至執行完畢的過程,就對應一個棧幀在虛擬機中入棧到出棧的過程。
       局部變量表存放了編譯期可知的各類Java虛擬機基本數據類型(boolean、int、double、float、char、byte、long、short)、對象引用和returnAddress類型。
       這些數據類型在局部變量表中的存儲空間以局部變量槽來表示,其中64位長度的long和double類型的數據會佔用兩個變量槽,其他的數據類型只佔用一個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在棧幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變變量表的大小。
       在java虛擬機規範中,對虛擬機棧這個區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是虛擬機棧能夠動態擴展,若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。編程

1.3本地方法棧

       本地方法棧與虛擬機棧所發揮的做用是很是類似的,其區別只有虛擬機棧爲虛擬機執行Java方法服務。而本地方法棧則爲虛擬機使用到的Native方法服務。
        本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。數組

1.4java堆

       對於大多數應用來講,Java堆是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立,此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。全部的對象實例以及數組都在堆上分配內存。可是隨着JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也漸漸變得不那麼絕對了緩存

       java堆是垃圾收集器管理的主要區域,所以不少時候也被稱做GC堆。從內存回收的角度來看,因爲如今手機器基本都採用分代收集算法,因此Java堆中能夠細分爲:新生代、老年代;在細緻一點能夠分爲Eden空間、From Survivor空間、To Survivor空間等。
安全

在這裏插入圖片描述
        若是從分配內存的角度看,全部線程共享的Java堆中能夠劃分出多個線程私有的分配緩衝區 (Thread Local Allocation Buffer,TLAB),以提高對象分配時的效率。不過不管從什麼角度,不管如何劃分,都不會改變Java堆中存儲內容的共性,不管是哪一個區域,存儲的都只能是對象的實例,將Java 堆細分的目的只是爲了更好地回收內存,或者更快地分配內存。
       根據《Java虛擬機規範》的規定,Java堆能夠處於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的,這點就像咱們用磁盤空間去存儲文件同樣,並不要求每一個文件都連續存放。但對於大對象(典型的如數組對象),多數虛擬機實現出於實現簡單、存儲高效的考慮,極可能會要求連續的 內存空間。 Java堆既能夠被實現成固定大小的,也能夠是可擴展的,不過當前主流的Java虛擬機都是按照可擴展來實現的(經過參數-Xmx和-Xms設定)。若是在Java堆中沒有內存完成實例分配,而且堆也沒法再擴展時,Java虛擬機將會拋出OutOfMemoryError異常。

1.5方法區

       方法區與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即便編譯器編譯後的代碼等數據。
       對於習慣在HotSpot虛擬機上開發、部署程序的開發者來講,不少人都更願意把方法區成爲「永久代」,本質上二者並不等價,僅僅是由於HostSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已,這樣HotSpot的垃圾收集器能夠像管理Java堆同樣管理這部份內存,可以省去專門爲方法區編寫內存管理代碼的工做。
       《Java虛擬機規範》對方法區的約束是很是寬鬆的,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,甚至還能夠選擇不實現垃圾收集。相對而言,垃圾收集行爲在這個區域的 確是比較少出現的,但並不是數據進入了方法區就如永久代的名字同樣「永久」存在了。這區域的內存回 收目標主要是針對常量池的回收和對類型的卸載,通常來講這個區域的回收效果比較難使人滿意,尤 其是類型的卸載,條件至關苛刻,可是這部分區域的回收有時又確實是必要的。之前Sun公司的Bug列 表中,曾出現過的若干個嚴重的Bug就是因爲低版本的HotSpot虛擬機對此區域未徹底回收而致使內存 泄漏。
       根據《Java虛擬機規範》的規定,若是方法區沒法知足新的內存分配需求時,將拋出 OutOfMemoryError異常。數據結構

1.6運行時常量池

       運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table),用於存放編譯期生 成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。
       運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,Java語言並不要求常量 必定只有編譯期才能產生,也就是說,並不是預置入Class文件中常量池的內容才能進入方法區運行時常 量池,運行期間也能夠將新的常量放入池中,這種特性被開發人員利用得比較多的即是String類的 intern()方法。
       既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存 時會拋出OutOfMemoryError異常。多線程

1.7直接內存

       直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是《Java虛擬機規範》中 定義的內存區域。可是這部份內存也被頻繁地使用,並且也可能致使OutOfMemoryError異常出現。
       在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區 (Buffer)的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆裏面的 DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了 在Java堆和Native堆中來回複製數據。
        顯然,本機直接內存的分配不會受到Java堆大小的限制,可是,既然是內存,則確定仍是會受到 本機總內存(包括物理內存、SWAP分區或者分頁文件)大小以及處理器尋址空間的限制,通常服務 器管理員配置虛擬機參數時,會根據實際內存去設置-Xmx等參數信息,但常常忽略掉直接內存,使得 各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),從而致使動態擴展時出現 OutOfMemoryError異常。併發

2.HotSpot虛擬機對象探祕

2.1 對象建立

        Java是一門面向對象的編程語言,Java程序運行過程當中無時無刻都有對象被建立出來。在語言層面 上,建立對象一般(例外:複製、反序列化)僅僅是一個new關鍵字而已,而在虛擬機中,對象的建立又是怎樣一個過程呢?
        當Java虛擬機遇到一條字節碼new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到 一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那 必須先執行相應的類加載過程。
        在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需內存的大小在類加載完成 後即可徹底肯定,爲對象分配空間的任務實際上便等同於把一塊肯定大小的內存塊從Java堆中劃分出來。假設Java堆中內存是絕對規整的,全部被使用過的內存都被放在一 邊,空閒的內存被放在另外一邊,中間放着一個指針做爲分界點的指示器,那所分配內存就僅僅是把那 個指針向空閒空間方向挪動一段與對象大小相等的距離,這種分配方式稱爲「指針碰撞」(Bump The Pointer)。但若是Java堆中的內存並非規整的,已被使用的內存和空閒的內存相互交錯在一塊兒,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱 爲「空閒列表」(Free List)。選擇哪一種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用 的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定。所以,當使用Serial、ParNew等帶壓縮 整理過程的收集器時,系統採用的分配算法是指針碰撞,既簡單又高效;而當使用CMS這種基於清除 (Sweep)算法的收集器時,理論上就只能採用較爲複雜的空閒列表來分配內存。
        除如何劃分可用空間以外,還有另一個須要考慮的問題:對象建立在虛擬機中是很是頻繁的行爲,即便僅僅修改一個指針所指向的位置,在併發狀況下也並非線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的狀況。解決這個問題有兩種可選方案:一種是對分配內存空間的動做進行同步處理——實際上虛擬機是採用CAS配上失敗重試的方式保證更新操做的原子性;另一種是把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(ThreadLocalAllocationBuffer,TLAB),哪一個線程要分配內存,就在哪一個線程的本地緩衝區中分配,只有本地緩衝區用完了,分配新的緩存區時才須要同步鎖定。虛擬機是否使用TLAB,能夠經過-XX:+/-UseTLAB參數來設定。
        內存分配完成以後,虛擬機必須將分配到的內存空間(但不包括對象頭)都初始化爲零值,若是使用了TLAB的話,這一項工做也能夠提早至TLAB分配時順便進行。這步操做保證了對象的實例字段在Java代碼中能夠不賦初始值就直接使用,使程序能訪問到這些字段的數據類型所對應的零值。
        接下來,Java虛擬機還要對對象進行必要的設置,例如這個對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼(實際上對象的哈希碼會延後到真正調用Object::hashCode()方法時才計算)、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(ObjectHeader)之中。根據虛擬機當前運行狀態的不一樣,如是否啓用偏向鎖等,對象頭會有不一樣的設置方式。關於對象頭的具體內容,稍後會詳細介紹。編程語言

2.2 對象的內存佈局

        在HotSpot虛擬機裏,對象在堆內存中的存儲佈局能夠劃分爲三個部分:對象頭(Header)、實例數據(InstanceData)和對齊填充(Padding)。

        HotSpot虛擬機對象的對象頭部分包括兩類信息。第一類是用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32個比特和64個比特,官方稱它爲「MarkWord」。對象須要存儲的運行時數據不少,其實已經超出了3二、64位Bitmap結構所能記錄的最大限度,但對象頭裏的信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,MarkWord被設計成一個有着動態定義的數據結構,以便在極小的空間內存儲儘可能多的數據,根據對象的狀態複用本身的存儲空間。

在這裏插入圖片描述

       對象頭的另一部分是類型指針,即對象指向它的類型元數據的指針,Java虛擬機經過這個指針來肯定該對象是哪一個類的實例。並非全部的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息並不必定要通過對象自己。此外,若是對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,由於虛擬機能夠經過普通Java對象的元數據信息肯定Java對象的大小,可是若是數組的長度是不肯定的,將沒法經過元數據中的信息推斷出數組的大小。
       對象的第三部分是對齊填充,這並非必然存在的,也沒有特別的含義,它僅僅起着佔位符的做用。因爲HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心設計成正好是8字節的倍數(1倍或者2倍),所以,若是對象實例數據部分沒有對齊的話,就須要經過對齊填充來補全。

2.3 對象的訪問定位

       建立對象天然是爲了後續使用該對象,咱們的Java程序會經過棧上的reference數據來操做堆上的具體對象。因爲reference類型在《Java虛擬機規範》裏面只規定了它是一個指向對象的引用,並無定義這個引用應該經過什麼方式去定位、訪問到堆中對象的具體位置,因此對象訪問方式也是由虛擬機實現而定的,主流的訪問方式主要有使用句柄和直接指針兩種:

  • ·若是使用句柄訪問的話,Java堆中將可能會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息。

    在這裏插入圖片描述

  • 若是使用直接指針訪問的話,Java堆中對象的內存佈局就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,若是隻是訪問對象自己的話,就不須要多一次間接訪問的開銷。

    在這裏插入圖片描述
    ==================END ==================
相關文章
相關標籤/搜索