类和对象运行时在内存里是怎么样的?各种变量、方法在运行时是怎么交互的?
轉載自? ?類和對象運行時在內存里是怎么樣的?各種變量、方法在運行時是怎么交互的?
在回答這個問題之前先了解一下Java的一些基礎知識。
我們知道Java程序運行在虛擬機環境里,那我們先看一下虛擬機的大致內存結構。如下圖所示,虛線框為整個虛擬機內存區域,其中有顏色的區域為Java程序所占的內存區域。
圖中可見Java程序所占的內存區域可劃分成5個部分:程序計數器、虛擬機棧(線程棧)、本地方法棧、堆(heap)和方法區(內含常量池)。其中方法區和堆由所有線程共享。
這5個區域作用和功能分別如下:
一、程序計數器:
它類似CPU寄存器中的PC寄存器,用于存放指令地址。因為Java虛擬機是多線程的,所以每一個線程都有一個獨立的程序計數器結構,它與線程共存亡。不過Java虛擬機中的程序計數器指向的是正在執行的字節碼地址,而CPU的PC寄存器指向的是下一條指令的地址。當線程去執行Native方法時,程序計數器則為Undefined。
二、虛擬機棧(線程棧):
一個線程一個棧,并且生命周期與線程相同。它內部由一個個棧幀構成,一個棧幀代表一個調用的方法,線程在每次方法調用執行時創建一個棧幀然后壓棧,棧幀用于存放局部變量、操作數、動態鏈接、方法出口等信息。方法執行完成后對應的棧幀出棧。我們平時說的棧內存就是指這個棧。
一個線程中的方法可能還會調用其他方法,這樣就會構成方法調用鏈,而且這個鏈可能會很長,而且每個線程都有方法處于執行狀態。對于執行引擎來說,只有活動線程棧頂的棧幀才是有效的,稱為當前棧幀(Current?Stack?Frame),這個棧幀關聯的方法稱為當前方法(Current?Method)。
棧幀的大致結構如下圖所示:
每一個棧幀的結構都包括了局部變量表、操作數棧、方法返回地址和一些額外的附加信息。某個方法的棧幀需要多大的局部變量表、多深的操作數棧都在編譯程序時完全確定了,并且寫入到類方法表的相應屬性中了,因此某個方法的棧幀需要分配多少內存,不會受到程序運行期變量數據變化的影響,而僅僅取決于具體虛擬機的實現。
棧幀結構各部分功能:
1)局部變量區域:存儲方法的局部變量和參數,存儲單位以slot(4?byte)為最小單位。局部變量存放的數據類型有:基本數據類型、對象引用和return address(指向一條字節碼指令的地址)。其中64位長度的long和double類型的變量會占用2個slot,其它數據類型只占用1個slot。
類的靜態方法和對象的實例方法被調用時,各自棧幀對應的局部變量結構基本類似。但有以下如圖示區別:實例方法中第一個位置存放的是它所屬對象的引用。而靜態方法則沒有對象的引用。另外靜態方法里所操作的靜態變量存放在方法區。
void?test(Object?object){
????int?i=0;
????Boolean?b = false;
}
static?void?test1(int?i, Object?object, boolean?b){
????...
}
關于局部變量,還有一點需要強調,就是局部變量不像類的實例變量那樣會有默認初始化值。所以局部變量需要手工初始化,如果一個局部變量定義了但沒有賦初始值是不能使用的。
2)操作數棧?所謂操作數是指那些被指令操作的數據。當需要對參數操作時如c=a+b,就將即將被操作的參數數據壓棧,如將a和b壓棧,然后由操作指令將它們彈出,并執行操作。虛擬機將操作數棧作為工作區。Java虛擬機沒有寄存器,所有參數傳遞、值返回都是使用操作數棧來完成的。
Java虛擬機的解釋執行引擎稱為“基于棧的執行引擎”,其中所指的“棧”就是操作數棧。
例如下面這段代碼:
public?static?int?add(int?a,int?b){
????int?c=0;
????c=a+b;
????return?c;
}
add(25,23);
主要步驟如圖:
壓棧的步驟如下:
0:??....
1:???iload_0??//?把局部變量0壓棧,int?a;
2:???iload_1?//?局部變量1壓棧,int?b;
3:???iadd??????//彈出2個變量,求和,結果壓棧48
4:???istore_2?//彈出結果,放于局部變量2;int?c;
5:?...
3)動態連接,它是個指向運行時常量池中該棧幀所屬方法的引用。這個引用是為了支持方法調用過程中能進行動態連接。我們知道Class文件的常量池存有方法的符號引用,字節碼中的方法調用指令就以指向常量池中方法的符號引用為參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析。余下部分將在每一次運行期間轉化為直接引用,這部分稱為動態連接。
4)方法返回地址
正常退出,執行引擎遇到方法返回的字節碼,將返回值傳遞給調用者。
異常退出,遇到Exception, 并且方法未捕捉異常,返回地址由異常處理器來確定,并且不會有任何返回值。
方法退出的過程實際上等同于把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的局部變量表和操作數棧,把返回值(如果有的話)壓入調用者棧幀的操作數棧中,調整PC計數器的值以指向方法調用指令后面的一條指令等。
5)額外附加信息,虛擬機規范沒有明確規定,由具體虛擬機實現。
Java虛擬機規范規定該區域有兩種異常:
StackOverFlowError:當線程請求棧深度超出虛擬機棧所允許的深度時拋出
OutOfMemoryError:當Java虛擬機動態擴展到無法申請足夠內存時拋出
另外需要提醒一下,在規范模型中,棧幀相互之間是完全獨立的。但在大多數虛擬機的實現里都會做一些優化處理,這樣兩個棧幀可能會出現一部分重疊。這樣在下面的棧幀會有部分操作數棧與上面棧幀的部分局部變量表重疊在一起,這樣在進行方法調用時就可以有部分數據共享,而無須進行額外的參數復制傳遞了。具體情形如下圖所示:
三、本地方法棧
Java可以通過java本地接口JNI(Java Native Interface)來調用其它語言編寫(如C)的程序,在Java里面用native修飾符來描述一個方法是本地方法。本地方法棧就是虛擬機線程調用Native方法執行時的棧,它與虛擬機棧發揮類似的作用。但是要注意,虛擬機規范中沒有對本地方法棧作強制規定,虛擬機可以自由實現,所以可以不是字節碼。如果是以字節碼實現的話,虛擬機棧本地方法棧就可以合二為一,事實上,OpenJDK和SunJDK所自帶的HotSpot虛擬機就是直接將虛擬機棧和本地方法棧合二為一的。
Java虛擬機規范規定該區域也可拋出StackOverFlowError和OutOfMemoryError。
四、堆(heap)
這個區域用來放置所有對象實例以及數組。不過在JIT(Just-in-time)情況下有些時候也有可能在棧上分配對象實例。堆也是java垃圾收集器管理的主要區域(所以很多時候會稱它為GC堆)。
從GC回收的角度看,由于現在GC基本都是采用的分代收集算法,所以堆內存結構還可以分塊成:新生代和老年代;再細一點的有Eden空間、From?Survivor空間、To?Survivor空間等。如下圖:
五、方法區
它是虛擬機在加載類文件時,用于存放加載過的類信息,常量,靜態變量,及jit編譯后的代碼(類方法)等數據的內存區域。它是線程共享的。
方法區存放的信息包括:
類的基本信息:
每個類的全限定名
每個類的直接超類的全限定名(可約束類型轉換)
該類是類還是接口
該類型的訪問修飾符
直接超接口的全限定名的有序列表
已裝載類的詳細信息:
運行時常量池:
類信息除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant?Pool?Table),用于存放編譯期生成的各種字面量、符號引用,文字字符串、final變量值、類名和方法名常量,這部分內容將在類加載后存放到方法區的運行時常量池中。它們以數組形式訪問,是調用方法、與類聯系及類的對象化的橋梁。
這里再講一下,JDK1.7之前運行時常量池是方法區的一部分,JDK1.7及之后版本已經將運行時常量池從方法區中移了出來,在堆(Heap)中開辟了一塊區域存放運行時常量池。
運行時常量池除了存放編譯期產生的Class文件的常量外,還可存放在程序運行期間生成的新常量,比較常見增加新常量方法有String類的internd()方法。String.intern()是一個Native方法,它的作用是:如果運行時常量池中已經包含一個等于此String對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,則在常量池中創建與此String內容相同的字符串,并返回常量池中創建的字符串的引用。不過JDK7的intern()方法的實現有所不同,當常量池中沒有該字符串時,不再是在常量池中創建與此String內容相同的字符串,而改為在常量池中記錄堆中首次出現的該字符串的引用,并返回該引用。
字段信息:
字段信息存放類中聲明的每一個字段(實例變量)的信息,包括字段的名、類型、修飾符。
如private String a = ""; 則a為字段名,String為描述符,private為修飾符。
方法信息:
類中聲明的每一個方法的信息,包括方法名、返回值類型、參數類型、修飾符、異常、方法的字節碼。(在編譯的時候,就已經將方法的局部變量表、操作數棧大小等完全確定并存放在字節碼中,在加載載的時候,隨著類一起裝入方法區。)
在運行時,虛擬機線程調用方法時從常量池中獲得符號引用,然后在運行時解析成方法的實際地址,最后通過常量池中的全限定名、方法和字段描述符,把當前類或接口中的代碼與其它類或接口中的代碼聯系起來。
靜態變量:
就是類變量,被類的所有實例對象共享,我們只需知道,在方法區有個靜態區,靜態區專門存放靜態變量和靜態塊。
到類ClassLoader的引用:
到該類的類裝載器的引用。
到類Class的引用:
虛擬機為每一個被裝載的類型創建一個Class實例,用來代表這個被裝載的類。
Java虛擬機規范規定該區域可拋出OutOfMemoryError。
六、直接內存
直接內存(Direct?Memory)雖然不是程序運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,但這部分內存也被頻繁使用,而且它也可能導致OutOfMemoryError異常出現。
在JDK1.4中新加入了NIO(New?Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用Native方法庫直接分配堆外內存,然后通過一個存儲在Java堆里面的DirecByteBuffer對象作為這塊內存的引用進行操作。這樣能在某些應用場景中顯著提高性能,因為它避免了在Java堆和Native堆中來回復制數據。
顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,還是會受到本機總內存(包括RAM及SWAP區或者分頁文件)的大小及處理器尋址空間的限制,從而導致動態擴展時出現OutOfMemoryError異常。
七、執行引擎
將字節碼即時編譯?優化?為本地代碼,?然后執行。
在了解完這些知識以后,就可以知道:類和對象在運行時的內存里是怎么樣的,以及各類型變量、方法在運行時是怎么交互的。
在程序運行時類是在方法區,實例對象本身在堆里面。
方法字節碼在方法區。線程調用方法執行時創建棧幀并壓棧,方法的參數和局部變量在棧幀的局部變量表。
對象的實例變量和對象一起在堆里,所以各個線程都可以共享訪問對象的實例變量。
靜態變量在方法區,所有對象共享。字符串常量等常量在運行時常量池。
各線程調用的方法,通過堆內的對象,方法區的靜態數據,可以共享交互信息。
各線程調用的方法所有參數傳遞、方法返回值的返回,都是使用棧幀里的操作數棧來完成的。
總結
以上是生活随笔為你收集整理的类和对象运行时在内存里是怎么样的?各种变量、方法在运行时是怎么交互的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2016年的电脑配置(2016的电脑配置
- 下一篇: 从Java类到对象的创建过程都做了些啥?