java虛擬機器記憶體區域的劃分以及作用詳解

一杯涼茶發表於2016-12-06

      序言

         為什麼有時候學著學著會突然之間覺得一切度是那麼無趣,男的每個月也有那麼幾天難道?哈哈,不然是什麼,我還是要堅持,可以做少一點,但是不能什麼度不做。總會過去的,加油

                                                                                                --WH

一、執行時資料區

       什麼叫執行時資料區呢,看下圖就知道了,今天的重點就圍繞這張圖講。

                

      1、程式計數器(暫存器)           

            當前執行緒所執行的位元組碼行號指示器

            位元組碼直譯器工作依賴計數器控制完成

            通過執行執行緒行號記錄,讓執行緒輪流切換各條執行緒之間計數器互不影響

            執行緒私有,生命週期與執行緒相同,隨JVM啟動而生,JVM關閉而死

            執行緒執行Java方法時,記錄其正在執行的虛擬機器位元組碼指令地址

            執行緒執行Nativan方法時,計數器記錄為空(Undefined)

            唯一在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況區域

        在這其中,很多不理解的沒關係,我們學過多執行緒,有兩個執行緒,其中一個執行緒可以暫停使用,讓其他執行緒執行,然後等自己獲得cpu資源時,又能從暫停的地方開始執行,那麼為什麼能夠記住暫停的位置的,這就依靠了程式計數器, 通過這個例子,大概瞭解一下程式計數器的功能。

 

      2、本地方法棧

            不知道大家看過原始碼沒有,看過的都應該知道,很多的演算法或者一個功能的實現,都被java封裝到了本地方法中,程式直接通過呼叫本地的方法就行了,本地方法棧就是用來存放這種方法的,實現該功能的程式碼可能是C也可能是C++,反正不一定就是java實現的。

 

      上面兩個不是我們所要學習的重點,接下來三個才是重點。

 

      3、虛擬機器棧

          這個大家都應該有所瞭解,現在來細講它,虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用來存放儲存區域性變數表、運算元表、動態連線、方法出口等資訊,每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。    這個話怎麼理解呢?比如執行一個類(類中有main方法)時,執行到main方法,就會把為main方法建立一個棧幀,然後在加到虛擬機器棧中,棧幀中會存放這main方法中的各種區域性變數,物件引用等東西。如圖

                    

          當在main方法中呼叫別的方法時,就會有另一個方法的棧幀入虛擬機器棧,當該方法呼叫完了之後,彈棧,然後main方法處於棧頂,就繼續執行,直到結束,然後main方法棧幀也彈棧,程式就結束了。總之虛擬機器棧中就是有很多個棧幀的入棧出棧,棧幀中存放的都市一些變數名等東西,所以我們平常說棧中存放的是一些區域性變數,因為區域性變數就是在方法中。也就是在棧幀中,就是這樣說過來的。

 

        以上說的三個都是執行緒不共享的,也就是這部分記憶體,每個執行緒獨有,不會讓別的執行緒訪問到,接下來的兩個就是執行緒共享了,也就會出現執行緒安全問題。

 

     4、堆

        所有執行緒共享的一塊記憶體區域。Java虛擬機器所管理的記憶體中最大的一塊,因為該記憶體區域的唯一目的就是存放物件例項。幾乎所有的物件例項度在這裡分配記憶體,也就是通常我們說的new物件,該物件就會在堆中開闢一塊記憶體來存放物件中的一些資訊,比如屬性呀什麼的。同時堆也是垃圾收集器管理的主要區域。因此很多時候被稱為"GC堆",虛擬機器的垃圾回收機制等下一篇文章來講解。 在上一點講的棧中存放的區域性引用變數所指向的大多數度會在堆中存放。

 

    5、方法區和其中的執行時常量池

        和堆一樣,是各個執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、和編譯器編譯後的程式碼(也就是儲存位元組碼檔案。.class)等資料,這裡可以看到常量也會在方法區中,是因為方法區中有一個執行時常量池,為什麼叫執行時常量池,因為在編譯後期生成的是各種字面量(字面量的意思就是值,比如int i=3,這個3就是字面量的意思)和符號引用,這些是存放在一個叫做常量池(這個常量池是在位元組碼檔案中)的地方,當類載入進入方法區時,就會把該常量池中的內容放入執行時常量池中。這裡要注意,執行時常量池和常量池,不要搞混淆了,位元組碼檔案中也有常量池,在後面的章節會詳細講解這個東西。現在只需要知道方法區中有一個執行時常量池,就是用來存放常量的。還有一點,執行時常量池不一定就一定要從位元組碼常量池中拿取常量,可能在程式執行期間將新的常量放入池中,比如String.intern()方法,這個方法的作用就是:先從方法區的執行時常量池中查詢看是否有該值,如果有,則返回該值的引用,如果沒有,那麼就會將該值加入執行時常量池中。

        

 

二、練習。畫記憶體圖。

        平常分析中用到的最多還是堆、虛擬機器棧和方法區。

        例如:看下面這段程式,然後畫出記憶體分析圖        

  

       最主要是看我的分析過程,這個圖由於要顯示出動態彈棧畫不了,所以只能夠那樣畫一下了。

        1、首先執行程式,Demo1_car.java就會變為Demo1_car.class,將Demo1_car.class加入方法區,檢查是否位元組碼檔案常量池中是否有常量值,如果有,那麼就加入執行時常量池

        2、遇到main方法,建立一個棧幀,入虛擬機器棧,然後開始執行main方法中的程式

        3、Car c1 = new Car(); 第一次遇到Car這個類,所以將Car.java編譯為Car.class檔案,然後加入方法區,跟第一步一樣。然後new Car()。就在堆中建立一塊區域,用於存放建立出來的例項物件,地址為0X001.其中有兩個屬性值 color和num。預設值是null 和 0

        4、然後通過c1這個引用變數去設定color和num的值,

        5、呼叫run方法,然後會建立一個棧幀,用來裝run方法中的區域性變數的,入虛擬機器棧,run方法中就列印了一句話,結束之後,該棧幀出虛擬機器棧。又只剩下main方法這個棧幀了

        6、接著又建立了一個Car物件,所以又在堆中開闢了一塊記憶體,之後就是跟之前的步驟一樣了。

 

 

    這樣就分析結束了,在腦袋中就應該有一個大概的認識對堆、虛擬機器棧、和方法區。注意這個方法區的名字,並不是就單單裝方法的,能裝很多東西。

 

 

  這個只是一個簡單的分析,可以再講具體一點,1、建立物件,在堆中開闢記憶體時是如何分配記憶體的?2、物件引用是如何找到我們在堆中的物件例項的?通過這兩個問題來加深我們的理解。

 1、建立物件,在堆中開闢記憶體時是如何分配記憶體的?

       兩種方式:指標碰撞和空閒列表。我們具體使用的哪一種,就要看我們虛擬機器中使用的是什麼了。

       指標碰撞:假設Java堆中記憶體是絕對規整的,所有用過的記憶體度放一邊,空閒的記憶體放另一邊,中間放著一個指標作為分界點的指示器,所分配記憶體就僅僅是把哪個指標向空閒空間那邊挪動一段與物件大小相等的舉例,這種分配方案就叫指標碰撞

       空閒列表:有一個列表,其中記錄中哪些記憶體塊有用,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,然後更新列表中的記錄。這就叫做空閒列表

 2、物件引用是如何找到我們在堆中的物件例項的?     

       這個問題也可以稱為物件的訪問定位問題,也有兩種方式。控制程式碼訪問直接指標訪問。 畫兩張圖就明白了。

       控制程式碼訪問:Java堆中會劃分出一塊記憶體來作為控制程式碼池,引用變數中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料和型別資料各自的具體地址資訊

              

        解釋圖:在棧中有一個引用變數指向控制程式碼池中一個控制程式碼的地址,這個控制程式碼又包含了兩個地址,一個物件例項資料,一個是物件型別資料(這個在方法區中,因為類位元組碼檔案就放在方法區中),

      

      直接指標訪問:引用變數中儲存的就直接是物件地址了,如圖所示

            

        解釋:在堆中就不會分控制程式碼池了,直接指向了物件的地址,物件中包含了物件型別資料的地址。

 

      區別:這兩種各有各的優勢,

          使用控制程式碼來訪問的最大好處就是引用變數中儲存的是穩定的控制程式碼地址,物件被移動(在垃圾收集時移動物件是很普通的行為)時就會改變控制程式碼中實力資料指標,但是引用變數所指向的地址不用改變。

          而使用直接指標訪問方式最大的好處就是速度更快,節省了一次指標定位的時間開銷,但是在物件被移動時,又需要改變引用變數的地址。在我們上面分析的例子中,就是使用的直接指標訪問的方式。

 

相關文章