天天看點

java運作記憶體配置設定

  Java 的記憶體主要包含 4 塊,即 heap (堆記憶體)、 stack (棧記憶體)、 datasegment( 靜态變量或是常量存放區)、 codesegment (方法區)   

棧:存取速度快,存放一些基本類型的變量或對象的引用變量。主要用來執行程式的。

堆:    存取速度慢,通過New()方法來建立的對象或數組。主要用來存放對象的。

靜态常量區:存放的是靜态變量(類變量)或是常量

方法區:存放的是對象的方法。是以即使new出多個對象也是隻是存在一個方法。

java運作記憶體配置設定

堆和棧的比較:

   堆主要用于存放對象,棧主要是用來執行程式。

       棧的優勢:存取速度比堆要快,僅次于寄存器,棧資料可以共享。缺點是,存在棧中的資料大小與生存期必須是确定的,缺乏靈活性。棧中主要存放一些基本類型的變量和對象句柄。修改棧指針就可以把棧中的内容銷毀,這樣模式速度最快。但是需要注意,配置設定空間時,應事先知道這個資料區的大小。

           堆是由垃圾回收來負責的,堆的優勢是可以動态地配置設定記憶體大小,生存期也不必事先告訴編譯器,因為它是在運作時動态配置設定記憶體的,Java的垃圾收集器會自動收走這些不再使用的資料。由于堆是動态的配置設定,編譯器不需要知道從堆裡配置設定多少空間或者是資料要停留多長時間,是以用堆儲存資料靈活性更大。缺點是,由于要在運作時動态配置設定記憶體,在配置設定和銷毀時都要占用時間,是以效率非常低。

這是一篇關于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).在單個線程類,棧中的資料可共享

    例如我們定義:

Java代碼

    int a=3;  

    int b=3;  

    編 譯器先處理int a = 3;首先它會在棧中建立一個變量為a 的引用,然後查找棧中是否有3 這個值,如果沒找到,就将3 存放進來,然後将a 指向3。接着處理int b = 3;在建立完b 的引用變量後,因為在棧中已經有3這個值,便将b 直接指向3。這樣,就出現了a 與b 同時均指向3的情況。

    而如果我們定義:

Java代碼

    Integer a=new Integer(3);//(1)  

    Integer b=new Integer(3);//(2)  

   這個時候執行過程為:在執行(1)時,首先在棧中建立一個變量a,然後在堆記憶體中執行個體化一個對象,并且将變量a指向這個執行個體化的對象。在執行(2)時,過程類似,此時,在堆記憶體中,會有兩個Integer類型的對象。

    2).在程序的各個線程之間,資料的共享通過堆來實作

        例:那麼,在多線程開發中,我們的資料共享又是怎麼實作的呢?

  如圖所示,堆中的資料是所有線程棧所共享的,我們可以通過參數傳遞,将一個堆中的資料傳入各個棧的工作記憶體中,進而實作多個線程間的資料共享

(多個程序間的資料共享則需要通過網絡傳輸了。)

3.從程式設計的的角度:

從軟體設計的角度看,JVM棧代表了處理邏輯,而JVM堆代表了資料。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、子產品化的思想在軟體設計的方方面面都有展現。

4.值傳遞和引用傳遞的真相

有了以上關于棧和堆的種種了解後,我們很容易就可以知道值傳遞和引用傳遞的真相:

1.程式運作永遠都是在JVM棧中進行的,因而參數傳遞時,隻存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。

但是傳引用的錯覺是如何造成的呢?

在運作JVM棧中,基本類型和引用的處理是一樣的,都是傳值,是以,如果是傳引用的方法調用,也同時可以了解為“傳引用值”的傳值調用,即引用的處理跟基本類型是完全一樣的。

但是當進入被調用方法時,被傳遞的這個引用的值,被程式解釋(或者查找)到JVM堆中的對象,這個時候才對應到真正的對象。

如果此時進行修改,修改的是引用對應的對象,而不是引用本身,即:修改的是JVM堆中的資料。是以這個修改是可以保持的了。

最後:

從某種意義上來說對象都是由基本類型組成的。

可以把一個對象看作為一棵樹,對象的屬性如果還是對象,則還是一顆樹(即非葉子節點),基本類型則為樹的葉子節點。程式參數傳遞時,被傳遞的值本身都是不能進行修改的,但是,如果這個值是一個非葉子節點(即一個對象引用),則可以修改這個節點下面的所有内容。

其實,面向對象方式的程式與以前結構化的程式在執行上沒有任何差別。

面向對象的引入,隻是改變了我們對待問題的思考方式,而更接近于自然方式的思考。

當我們把對象拆開,其實對象的屬性就是資料,存放在JVM堆中;而對象的行為(方法),就是運作邏輯,放在JVM棧中。我們在編寫對象的時候,其實即編寫了資料結構,也編寫的處理資料的邏輯。