歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Java虛擬機內存區域的劃分以及作用詳解

序言

為什麼有時候學著學著會突然之間覺得一切度是那麼無趣,男的每個月也有那麼幾天難道?哈哈,不然是什麼,我還是要堅持,可以做少一點,但是不能什麼度不做。總會過去的,加油                                                                                           --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堆中會劃分出一塊內存來作為句柄池,引用變量中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息

解釋圖:在棧中有一個引用變量指向句柄池中一個句柄的地址,這個句柄又包含了兩個地址,一個對象實例數據,一個是對象類型數據(這個在方法區中,因為類字節碼文件就放在方法區中),

直接指針訪問:引用變量中存儲的就直接是對象地址了,如圖所示

解釋:在堆中就不會分句柄池了,直接指向了對象的地址,對象中包含了對象類型數據的地址。

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

使用句柄來訪問的最大好處就是引用變量中存儲的是穩定的句柄地址,對象被移動(在垃圾收集時移動對象是很普通的行為)時就會改變句柄中實力數據指針,但是引用變量所指向的地址不用改變。

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

Copyright © Linux教程網 All Rights Reserved