咱們都知道虛擬機的內存劃分了多個區域,並非一張大餅。那麼爲何要劃分爲多塊區域呢,直接搞一塊區域,全部用到內存的地方都往這塊區域裏扔不就好了,豈不痛快。是的,若是不進行區域劃分,扔的時候確實痛快,可用的時候再去找怎麼辦呢,這就引入了第一個問題,分類管理,相似於衣櫃,系統磁盤等等,爲了方便查找,咱們會進行分區分類。另外若是不進行分區,內存用盡了怎麼辦呢?這裏就引入了內存劃分的第二個緣由,就是爲了方便內存的回收。若是不分,回收內存須要所有內存掃描,那就慢死了,內存根據不一樣的使用功能分紅不一樣的區域,那麼內存回收也就能夠根據每一個區域的特定進行回收,好比像棧內存中的棧幀,隨着方法的執行棧幀進棧,方法執行完畢就出棧了,而對於像堆內存的回收就須要使用經典的回收算法來進行回收了,因此看起來分類這麼麻煩,實際上是大有好處的。java
提到虛擬機的內存結構,可能首先想起來的就是堆棧。對象分配到堆上,棧上用來分配對象的引用以及一些基本數據類型相關的值。可是·虛擬機的內存結構遠比此要複雜的多。除了咱們所認識的(尚未認識徹底)的堆棧之外,還有程序計數器,本地方法棧和方法區。咱們平時所說的棧內存,通常是指的棧內存中的局部變量表。下面是官方所給的虛擬機的內存結構圖算法
從圖中能夠看到有5大內存區域,按照是否被線程所共享可分爲兩部分,一部分是線程獨佔區域,包括Java棧,本地方法棧和程序計數器。還有一部分是被線程所共享的,包括方法區和堆。什麼是線程共享和線程獨佔呢,很是好理解,咱們知道每個Java進行都會有多個線程同時運行,那麼線程共享區的這片區域就是被全部線程一塊兒使用的,無論有多少個線程,這片空間始終就這一個。而線程的獨佔區,是每一個線程都有這麼一分內存空間,每一個線程的這片空間都是獨有的,有多少個線程就有多少個這麼個空間。上圖的區域的大小並不表明實際內存區域的大小,實際運行過程當中,內存區域的大小也是能夠動態調整的。下面來具體說說每個區域的主要功能。安全
程序計數器,咱們在寫代碼的過程當中,開發工具通常都會給咱們標註行號方便查看和閱讀代碼。那麼在程序在運行過程當中也有一個相似的行號方便虛擬機的執行,就是程序計數器,在c語言中,咱們知道會有一個goto語句,其實就是跳轉到了指定的行,這個行號就是程序計數器。存儲的就是程序下一條所執行的指令。這部分區域是線程所獨享的區域,咱們知道線程是一個順序執行流,每一個線程都有本身的執行順序,若是全部線程共用一個程序計數器,那麼程序執行確定就會出亂子。爲了保證每一個線程的執行順序,因此程序計數器是被單個線程所獨顯的。程序計數器這塊內存區域是惟一一個在jvm規範中沒有規定內存溢出的。數據結構
java虛擬機棧,java虛擬機棧是程序運行的動態區域,每一個方法的執行都伴隨着棧幀的入棧和出棧。 棧幀也叫過程活動記錄,是編譯器用來實現過程/函數調用的一種數據結構。棧幀中包括了局部變量表,操做數棧,方法返回地址以及額外的一些附加信息,在編譯過程當中,局部變量表的大小已經肯定,操做數棧深度也已經肯定,所以棧幀在運行的過程當中須要分配多大的內存是固定的,不受運行時影響。對於沒有逃逸的對象也會在棧上分配內存,對象的大小其實在運行時也是肯定的,所以即便出現了棧上內存分配,也不會致使棧幀改變大小。jvm
一個線程中,可能調用鏈會很長,不少方法都同時處於執行狀態。對於執行引擎來說,活動線程中,只有棧頂的棧幀是最有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法。執行引擎所運行的字節碼指令僅對當前棧幀進行操做。函數
局部變量表:咱們平時所說的棧內存通常就是指棧內存中的局部變量表。這裏主要是存儲變量所用。對於基本數據類型直接存儲其值,對於引用數據類型則存儲其地址。局部變量表的最小存儲單位是Slot,每一個Slot都能存放一個boolean、byte、char、short、int、float、reference或returnAddress類型的數據。工具
既然前面提到了數據類型,在此順便說一下,一個Slot能夠存放一個32位之內的數據類型,Java中佔用32位之內的數據類型有boolean、byte、char、short、int、float、reference和returnAddress八種類型。前面六種不須要多解釋,你們都認識,然後面的reference是對象的引用。虛擬機規範既沒有說明它的長度,也沒有明確指出這個引用應有怎樣的結構,可是通常來講,虛擬機實現至少都應當能今後引用中直接或間接地查找到對象在Java堆中的起始地址索引和方法區中的對象類型數據。而returnAddress是爲字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。性能
對於64位的數據類型,虛擬機會以高位在前的方式爲其分配兩個連續的Slot空間。Java語言中明確規定的64位的數據類型只有long和double兩種(reference類型則多是32位也多是64位)。值得一提的是,這裏把long和double數據類型讀寫分割爲兩次32讀寫的作法相似。不過,因爲局部變量表創建在線程的堆棧上,是線程私有的數據,不管讀寫兩個連續的Slot是不是原子操做,都不會引發數據安全問題。開發工具
操做數棧是一個後入先出(Last In First Out, LIFO)棧。同局部變量表同樣,操做數棧的最大深度也在編譯的時候被寫入到字節碼文件中,關於字節碼文件,後面我會具體的來描述。操做數棧的每個元素能夠是任意的Java數據類型,包括long和double。32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2。在方法執行的任什麼時候候,操做數棧的深度都不會超過在max_stacks數據項中設定的最大值。spa
當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令向操做數棧中寫入和提取內容,也就是入棧出棧操做。例如,在作算術運算的時候是經過操做數棧來進行的,又或者在調用其餘方法的時候是經過操做數棧來進行參數傳遞的。
舉個例子,整數加法的字節碼指令iadd在運行的時候要求操做數棧中最接近棧頂的兩個元素已經存入了兩個int型的數值,當執行這個指令時,會將這兩個int值和並相加,而後將相加的結果入棧。
操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯程序代碼的時候,編譯器要嚴格保證這一點,在類校驗階段的數據流分析中還要再次驗證這一點。再以上面的iadd指令爲例,這個指令用於整型數加法,它在執行時,最接近棧頂的兩個元素的數據類型必須爲int型,不能出現一個long和一個float使用iadd命令相加的狀況。
本地方法棧 與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
方法區常常會被人稱之爲永久代,但這倆並非一個概念。首先永久代的概念僅僅在HotSpot虛擬機中存在,不幸的是,在jdk8中,Hotspot去掉了永久代這一說法,使用了Native Memory,也就是Metaspace空間。那麼方法區是幹嗎的呢?咱們能夠這麼理解,咱們要運行Java代碼,首先須要編譯,而後才能運行。在運行的過程當中,咱們知道首先須要加載字節碼文件。也就是說要把字節碼文件加載到內存中。好了,問題就來了,字節碼文件放到內存中的什麼地方呢,就是方法區中。固然除了編譯後的字節碼以外,方法區中還會存放常量,靜態變量以及及時編譯器編譯後的代碼等數據。
堆,通常來說堆內存是Java虛擬機中最大的一塊內存區域,同方法區同樣,是被全部線程所共享的區域。此區域所存在的惟一目的就存放對象的實例(對象實例並不必定所有在堆中建立)。堆內存是垃圾收集器主要光顧的區域,通常來說根據使用的垃圾收集器的不一樣,堆中還會劃分爲一些區域,好比新生代和老年代。新生代還能夠再劃分爲Eden,Survivor等區域。另外爲了性能和安全性的角度,在堆中還會爲線程劃分單獨的區域,稱之爲線程分配緩衝區。更細緻的劃分是爲了讓垃圾收集器可以更高效的工做,提升垃圾收集的效率。
若是想要了解更多的關於虛擬機的內容,歡迎觀看由我錄製的<深刻理解Java虛擬機>這套視頻教程。