這是一篇關於Java內存結構組織的文章,涉及的概念主要有方法區、Java棧、java堆。通過這個文章,可以加深對Java對象的理解,以及優化代碼的結構。
開始:
想寫這篇總結醞釀了有個來月了,卻始終感覺還差點什么東西,一直未敢動筆。 最近兩天連夜奮戰,重新整理下前面查閱的資料、筆記,還是決定將它寫出來。 現在提出幾個問題,如果都能熟練回答的大蝦,請您飄過.如以往一樣,我是小菜,本文自然也是針對小菜階層的總結。 |
首先是概念層面的幾個問題:
- Java中運行時內存結構有哪幾種?
- Java中為什么要設計堆棧分離?
- Java多線程中是如何實現數據共享的?
- Java反射的基礎是什么?
然后是運用層面:
- 引用類型變量和對象的區別?
- 什么情況下用局部變量,什么情況下用成員變量?
- 數組如何初始化?聲明一個數組的過程中,如何分配內存?
- 聲明基本類型數組和聲明引用類型的數組,初始化時,內存分配機制有什么區?
- 在什么情況下,我們的方法設計為靜態化,為什么?(上次胡老師問文奇,問的啞口無言,當時想回答,卻老感覺表述不清楚,這里也簡單說明一下)
好了,問題提完了,如果您都能一眼看出答案,那么,沒有必要再浪費您寶貴的時間看下去了。
如果您還不太明白,請跟隨我一路走下去。
Java中運行時內存結構
1.1 方法區:
方法區是系統分配的一個內存邏輯區域,是JVM在裝載類文件時,用於存儲類型信息的(類的描述信息)。
方法區存放的信息包括:
1.1.1類的基本信息:
- 每個類的全限定名
- 每個類的直接超類的全限定名(可約束類型轉換)
- 該類是類還是接口
- 該類型的訪問修飾符
- 直接超接口的全限定名的有序列表
1.1.2已裝載類的詳細信息:
- 運行時常量池:
在方法區中,每個類型都對應一個常量池,存放該類型所用到的所有常量,常量池中存儲了諸如文字字符串、final變量值、類名和方法名常量。它們以數組形式通過索引被訪問,是外部調用與類聯系及類型對象化的橋梁。(存的可能是個普通的字符串,然后經過常量池解析,則變成指向某個類的引用)
- 字段信息:
字段信息存放類中聲明的每一個字段的信息,包括字段的名、類型、修飾符。
字段名稱指的是類或接口的實例變量或類變量,字段的描述符是一個指示字段的類型的字符串,如private A a=null;則a為字段名,A為描述符,private為修飾符
- 方法信息:
類中聲明的每一個方法的信息,包括方法名、返回值類型、參數類型、修飾符、異常、方法的字節碼。
(在編譯的時候,就已經將方法的局部變量、操作數棧大小等確定並存放在字節碼中,在裝載的時候,隨着類一起裝入方法區。)
在運行時,JVM從常量池中獲得符號引用,然后在運行時解析成引用項的實際地址,最后通過常量池中的全限定名、方法和字段描述符,把當前類或接口中的代碼與其它類或接口中的代碼聯系起來。 - 靜態變量:
這個沒什么好說的,就是類變量,類的所有實例都共享,我們只需知道,在方法區有個靜態區,靜態區專門存放靜態變量和靜態塊。
- 到類classloader的引用:到該類的類裝載器的引用。
- 到類class 的引用:虛擬機為每一個被裝載的類型創建一個class 實例,用來代表這個被裝載的類。
由此我們可以知道反射的基礎:
在裝載類的時候,加入方法區中的所有信息,最后都會形成Class類的實例,代表這個被裝載的類。方法區中的所有的信息,都是可以通過這個Class類對象反射得到。我們知道對象是類的實例,類是相同結構的對象的一種抽象。同類的各個對象之間,其實是擁有相同的結構(屬性),擁有相同的功能(方法),各個對象的區別只在於屬性值的不同。 同樣的,我們所有的類,其實都是Class類的實例,他們都擁有相同的結構-----Field數組、Method數組。而各個類中的屬性都是Field屬性的一個具體屬性值,方法都是Method屬性的一個具體屬性值。 |
在運行時,JVM從常量池中獲得符號引用,然后在運行時解析成引用項的實際地址,最后通過常量池中的全限定名、方法和字段描述符,把當前類或接口中的代碼與其它類或接口中的代碼聯系起來。
1.2 Java棧
JVM棧是程序運行時單位,決定了程序如何執行,或者說數據如何處理。
在Java中,一個線程就會有一個線程的JVM棧與之對應,因為不過的線程執行邏輯顯然不同,因此都需要一個獨立的JVM棧來存放該線程的執行邏輯。
對方法的調用:
Java棧內存,以幀的形式存放本地方法的調用狀態,包括方法調用的參數、局部變量、中間結果等(方法都是以方法幀的形式存放在方法區的),每調用一個方法就將對應該方法的方法幀壓入Java 棧,成為當前方法幀。當調用結束(返回)時,就彈出該幀。
這意味着:
在方法中定義的一些基本類型的變量和引用變量都在方法的棧內存中分配。當在一段代碼塊定義一個變量時,Java 就在棧中為這個變量分配內存空間,當超過變量的作用域后(方法執行完成后),Java 會自動釋放掉為該變量所分配的內存空間,該內存空間可以立即被另作它用。--------同時,因為變量被釋放,該變量對應的對象,也就失去了引用,也就變成了可以被gc對象回收的垃圾。
因此我們可以知道成員變量與局部變量的區別:
局部變量,在方法內部聲明,當該方法運行完時,內存即被釋放。 成員變量,只要該對象還在,哪怕某一個方法運行完了,還是存在。 從系統的角度來說,聲明局部變量有利於內存空間的更高效利用(方法運行完即回收)。 成員變量可用於各個方法間進行數據共享。 |
Java 棧內存的組成:
局部變量區、操作數棧、幀數據區組成。
(1): 局部變量區為一個以字為單位的數組,每個數組元素對應一個局部變量的值。調用方法時,將方法的局部變量組成一個數組,通過索引來訪問。若為非靜態方法,則 加入一個隱含的引用參數this,該參數指向調用這個方法的對象。而靜態方法則沒有this參數。因此,對象無法調用靜態方法。
由此,我們可以知道,方法什么時候設計為靜態,什么時候為非靜態?
前面已經說過,對象是類的一個實例,各個對象結構相同,只是屬性不同。 而靜態方法是對象無法調用的。 所以,靜態方法適合那些工具類中的工具方法,這些類只是用來實現一些功能,也不需要產生對象,通過設置對象的屬性來得到各個不同的個體。 |
(2):操作數棧也是一個數組,但是通過棧操作來訪問。所謂操作數是那些被指令操作的數據。當需要對參數操作時如a=b+c,就將即將被操作的參數壓棧,如將b 和c 壓棧,然后由操作指令將它們彈出,並執行操作。虛擬機將操作數棧作為工作區。
(3):幀數據區處理常量池解析,異常處理等
1.3 java堆
java的堆是一個運行時的數據區,用來存儲數據的單元,存放通過new關鍵字新建的對象和數組,對象從中分配內存。
在堆中聲明的對象,是不能直接訪問的,必須通過在棧中聲明的指向該引用的變量來調用。引用變量就相當於是為數組或對象起的一個名稱,以后就可以在程序中使用棧中的引用變量來訪問堆中的數組或對象。
由此我們可以知道,引用類型變量和對象的區別:
聲明的對象是在堆內存中初始化的, 真正用來存儲數據的。不能直接訪問。 引用類型變量是保存在棧當中的,一個用來引用堆中對象的符號而已(指針)。 |
堆與棧的比較:
JAVA堆與棧都是用來存放數據的,那么他們之間到底有什么差異呢?既然棧也能存放數據,為什么還要設計堆呢?
1.從存放數據的角度:
前面我們已經說明:
棧中存放的是基本類型的變量or引用類型的變量
堆中存放的是對象or數組對象.
在棧中,引用變量的大小為32位,基本類型為1-8個字節。
但是對象的大小和數組的大小是動態的,這也決定了堆中數據的動態性,因為它是在運行時動態分配內存的,生存期也不必在編譯時確定,Java 的垃圾收集器會自動收走這些不再使用的數據。
2.從數據共享的角度:
1).在單個線程類,棧中的數據可共享
例如我們定義:
- int a=3;
- int b=3;
編譯器先處理int a = 3;首先它會在棧中創建一個變量為a 的引用,然后查找棧中是否有3 這個值,如果沒找到,就將3 存放進來,然后將a 指向3。接着處理int b = 3;在創建完b 的引用變量后,因為在棧中已經有3這個值,便將b 直接指向3。這樣,就出現了a 與b 同時均指向3的情況。
而如果我們定義:
- Integer a=new Integer(128);//(1)
- Integer b=new Integer(128);//(2)
這個時候執行過程為:在執行(1)時,首先在棧中創建一個變量a,然后在堆內存中實例化一個對象,並且將變量a指向這個實例化的對象。在執行(2)時,過程類似,此時,在堆內存中,會有兩個Integer類型的對象。
2).在進程的各個線程之間,數據的共享通過堆來實現
例:那么,在多線程開發中,我們的數據共享又是怎么實現的呢?
堆中的數據是所有線程棧所共享的,我們可以通過參數傳遞,將一個堆中的數據傳入各個棧的工作內存中,從而實現多個線程間的數據共享
(多個進程間的數據共享則需要通過網絡傳輸了。)
3.從程序設計的的角度:
從軟件設計的角度看,JVM棧代表了處理邏輯,而JVM堆代表了數據。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、模塊化的思想在軟件設計的方方面面都有體現。
4.值傳遞和引用傳遞的真相
有了以上關於棧和堆的種種了解后,我們很容易就可以知道值傳遞和引用傳遞的真相:
1.程序運行永遠都是在JVM棧中進行的,因而參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。 但是傳引用的錯覺是如何造成的呢? 在運行JVM棧中,基本類型和引用的處理是一樣的,都是傳值,所以,如果是傳引用的方法調用,也同時可以理解為“傳引用值”的傳值調用,即引用的處理跟基本類型是完全一樣的。 但是當進入被調用方法時,被傳遞的這個引用的值,被程序解釋(或者查找)到JVM堆中的對象,這個時候才對應到真正的對象。 如果此時進行修改,修改的是引用對應的對象,而不是引用本身,即:修改的是JVM堆中的數據。所以這個修改是可以保持的了。 |
最后:
從某種意義上來說對象都是由基本類型組成的。
可以把一個對象看作為一棵樹,對象的屬性如果還是對象,則還是一顆樹(即非葉子節點),基本類型則為樹的葉子節點。程序參數傳遞時,被傳遞的值本身都是不能進行修改的,但是,如果這個值是一個非葉子節點(即一個對象引用),則可以修改這個節點下面的所有內容。 |
其實,面向對象方式的程序與以前結構化的程序在執行上沒有任何區別。
面向對象的引入,只是改變了我們對待問題的思考方式,而更接近於自然方式的思考。
當我們把對象拆開,其實對象的屬性就是數據,存放在JVM堆中;而對象的行為(方法),就是運行邏輯,放在JVM棧中。我們在編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。