天天看點

java中堆棧(stack)和堆(heap)

(1)記憶體配置設定的政策

  按照編譯原理的觀點,程式運作時的記憶體配置設定有三種政策,分别是靜态的,棧式的,和堆式的.

 靜态存儲配置設定是指在編譯時就能确定每個資料目标在運作時刻的存儲空間需求,因而在編 譯時就可以給他們配置設定固定的記憶體空間.這種配置設定政策要求程式代碼中不允許有可變資料結構(比如可變數組)的存在,也不允許有嵌套或者遞歸的結構出現,因為 它們都會導緻編譯程式無法計算準确的存儲空間需求.

 棧式存儲配置設定也可稱為動态存儲配置設定,是由一個類似于堆棧的運作棧來實作的.和靜态存 儲配置設定相反,在棧式存儲方案中,程式對資料區的需求在編譯時是完全未知的,隻有到運作的時候才能夠知道,但是規定在運作中進入一個程式子產品時,必須知道該 程式子產品所需的資料區大小才能夠為其配置設定記憶體.和我們在資料結構所熟知的棧一樣,棧式存儲配置設定按照先進後出的原則進行配置設定。

 靜态存儲配置設定要求在編譯時能知道所有變量的存儲要求,棧式存儲配置設定要求在過程的入口 處必須知道所有的存儲要求,而堆式存儲配置設定則專門負責在編譯時或運作時子產品入口處都無法确定存儲要求的資料結構的記憶體配置設定,比如可變長度串和對象執行個體.堆 由大片的可利用塊或空閑塊組成,堆中的記憶體可以按照任意順序配置設定和釋放.

(2)堆和棧的比較

  上面的定義從編譯原理的教材中總結而來,除靜态存儲配置設定之外,都顯得很呆闆和難以了解,下面撇開靜态存儲配置設定,集中比較堆和棧:

 從堆和棧的功能和作用來通俗的比較, 堆主要用來存放對象的,棧主要是用來執行程式的 .而這種不同又主要是由于堆和棧的特點決定的:

   在程式設計中,例如C/C++中,所有的方法調用都是通過棧來進行的,所有的局部變量,形式參數都是從棧中配置設定記憶體空間的。實際上也不是什麼配置設定,隻是從棧頂 向上用就行,就好像工廠中的傳送帶(conveyor belt)一樣,Stack Pointer會自動指引你到放東西的位置,你所要做的隻是把東西放下來就行.退出函數的時候,修改棧指針就可以把棧中的内容銷毀.這樣的模式速度最快, 當然要用來運作程式了.需要注意的是,在配置設定的時候,比如為一個即将要調用的程式子產品配置設定資料區時,應事先知道這個資料區的大小,也就說是雖然配置設定是在程 序運作時進行的,但是配置設定的大小多少是确定的,不變的,而這個"大小多少"是在編譯時确定的,不是在運作時.

   堆是應用程式在運作的時候請求作業系統配置設定給自己記憶體,由于從作業系統管理的記憶體配置設定,是以在配置設定和銷毀時都要占用時間,是以用堆的效率非常低.但是堆的 優點在于,編譯器不必知道要從堆裡配置設定多少存儲空間,也不必知道存儲的資料要在堆裡停留多長的時間,是以,用堆儲存資料時會得到更大的靈活性。事實上,面 向對象的多态性,堆記憶體配置設定是必不可少的,因為多态變量所需的存儲空間隻有在運作時建立了對象之後才能确定.在C++中,要求建立一個對象時,隻需用 new指令編制相關的代碼即可。執行這些代碼時,會在堆裡自動進行資料的儲存.當然,為達到這種靈活性,必然會付出一定的代價:在堆裡配置設定存儲空間時會花 掉更長的時間!這也正是導緻我們剛才所說的效率低的原因,看來列甯同志說的好,人的優點往往也是人的缺點,人的缺點往往也是人的優點(暈~).

(3)JVM中的堆和棧

  JVM是基于堆棧的虛拟機.JVM為每個新建立的線程都配置設定一個堆棧.也就是說,對于一個Java程式來說,它的運作就是通過對堆棧的操作來完成的。堆棧以幀為機關儲存線程的狀态。JVM對堆棧隻進行兩種操作:以幀為機關的壓棧和出棧操作。

  我們知道,某個線程正在執行的方法稱為此線程的目前方法.我們可能不知道,目前方法使用的幀稱為目前幀。當線程激活一個Java方法,JVM就會線上程的 Java堆棧裡新壓入一個幀。這個幀自然成為了目前幀.在此方法執行期間,這個幀将用來儲存參數,局部變量,中間計算過程和其他資料.這個幀在這裡和編譯 原理中的活動紀錄的概念是差不多的.

  從Java的這種配置設定機制來看,堆棧又可以這樣了解:堆棧(Stack)是作業系統在建立某個程序時或者線程(在支援多線程的作業系統中是線程)為這個線程建立的存儲區域,該區域具有先進後出的特性。

   每一個Java應用都唯一對應一個JVM執行個體,每一個執行個體唯一對應一個堆。應用程式在運作中所建立的所有類執行個體或數組都放在這個堆中,并由應用所有的線程 共享.跟C/C++不同,Java中配置設定堆記憶體是自動初始化的。Java中所有對象的存儲空間都是在堆中配置設定的,但是這個對象的引用卻是在堆棧中配置設定,也 就是說在建立一個對象時從兩個地方都配置設定記憶體,在堆中配置設定的記憶體實際建立這個對象,而在堆棧中配置設定的記憶體隻是一個指向這個堆對象的指針(引用)而已。

static、final修飾符、内部類和Java記憶體配置設定

static修飾符

        static修飾符能夠與屬性、方法和内部類一起使用,表示靜态的。類中的靜态變量和靜态方法能夠與類名一起使用,不需要建立一個類的對象來通路該類的靜态成員,是以,static修飾的變量又稱作“類變量”。

static屬性的記憶體配置設定

         一個類中,一個static變量隻會有一個記憶體空間,雖然有多個類執行個體,但這些類執行個體中的這個static變量會共享同一個記憶體空間。

static的變量是在類裝載的時候就會被初始化,即,隻要類被裝載,不管是否使用了static變量,都會被初始化。

static的基本規則

  ·一個類的靜态方法隻能通路靜态屬性

  ·一個類的靜态方法不能直接調用非靜态方法

  ·如通路控制權限允許,static屬性和方法可以使用類名加“.”的方式調用,也可以使用執行個體加“.”的方式調用

  ·靜态方法中不存在目前對象,因而不能使用this,也不能使用super

  ·靜态方法不能被非靜态方法覆寫

  ·構造方法不允許聲明為static的

  注,非靜态變量隻限于執行個體,并隻能通過執行個體引用被通路。

靜态初始器——靜态塊

  靜态初始器是一個存在與類中方法外面的靜态塊,僅僅在類裝載的時候執行一次,通常用來初始化靜态的類屬性。

final修飾符

  在Java聲明類、屬性和方法時,可以使用關鍵字final來修飾,final所标記的成分具有終态的特征,表示最終的意思。

  final的具體規則

    ·final标記的類不能被繼承

    ·final标記的方法不能被子類重寫

    ·final标記的變量(成員變量或局部變量)即成為常量,隻能指派一次

    ·final标記的成員變量必須在聲明的同時指派,如果在聲明的時候沒有指派,那麼隻有一次指派的機會,而且隻能在構造方法中顯式指派,然後才能使用

    ·final标記的局部變量可以隻聲明不指派,然後再進行一次性的指派

    ·final一般用于标記那些通用性的功能、實作方式或取值不能随意被改變的成分,以避免被誤用

  如果将引用類型(即,任何類的類型)的變量标記為final,那麼,該變量不能指向任何其它對象,但可以改變對象的内容,因為隻有引用本身是final的。

内部類

  在一個類(或方法、語句塊)的内部定義另一個類,後者稱為内部類,有時也稱為嵌套類。

  内部類的特點

    ·内部類可以展現邏輯上的從屬關系,同時對于其它類可以控制内部類對外不可見等

    ·外部類的成員變量作用域是整個外部類,包括内部類,但外部類不能通路内部類的private成員

    ·邏輯上相關的類可以在一起,可以有效地實作資訊隐藏

    ·内部類可以直接通路外部類的成員,可以用此實作多繼承

    ·編譯後,内部類也被編譯為單獨的類,名稱為outclass$inclass的形式

内部類可以分為四種

    ·類級:成員式,有static修飾

    ·對象級:成員式,普通,無static修飾

    ·本地内部類:局部式

    ·匿名級:局部式

  成員式内部類的基本規則

    ·可以有各種修飾符,可以用4種權限、static、final、abstract定義

    ·若有static限定,就為類級,否則為對象級。類級可以通過外部類直接通路,對象級需要先生成外部的對象後才能通路

    ·内外部類不能同名

    ·非靜态内部類中不能聲明任何static成員

    ·内部類可以互相調用

  成員式内部類的通路

    内部類通路外層類對象的成員時,文法為:

      外層類名.this.屬性

    使用内部類時,由外部類對象加“.new”操作符調用内部類的構造方法,建立内部類的對象。

  在另一個外部類中使用非靜态内部類中定義的方法時,要先建立外部類的對象,再建立與外部類相關的内部類的對象,再調用内部類的方法。

  static内部類相當于其外部類的static成分,它的對象與外部類對象間不存在依賴關系,是以可以直接建立。

  由于内部類可以直接通路其外部類的成分,是以,當内部類與其外部類中存在同名屬性或方法時,也将導緻命名沖突。是以,在多層調用時要指明。

  本地類是定義在代碼塊中的類,隻在定義它們的代碼塊中可見。

  本地類有以下幾個重要特性:

    ·僅在定義了它們的代碼塊中可見

    ·可以使用定義它們的代碼塊中的任何本地final變量(注:本地類(也可以是局部内部類/匿名内部類等等)使用外部類的變量,原意是希 望這個變量在本地類中的對象和在外部類中的這個變量對象是一緻的,但如果這個變量不是final定義,它有可能在外部被修改,進而導緻内外部類的變量對象 狀态不一緻,是以,這類變量必須在外部類中加final字首定義)

    ·本地類不可以是static的,裡邊也不能定義static成員

    ·本地類不可以用public、private、protected修飾,隻能使用預設的

    ·本地類可以是abstract的

匿名内部類是本地内部類的一種特殊形式,即,沒有類名的内部類,而且具體的類實作會寫在這個内部類裡。

  匿名類的規則

    ·匿名類沒有構造方法

    ·匿名類不能定義靜态的成員

    ·匿名類不能用4種權限、static、final、abstract修飾

    ·隻可以建立一個匿名類執行個體

Java的記憶體配置設定

  Java程式運作時的記憶體結構分成:方法區、棧記憶體、堆記憶體、本地方法棧幾種。

  方法區存放裝載的類資料資訊,包括:

    ·基本資訊:每個類的全限定名、每個類的直接超類的全限定名、該類是類還是接口、該類型的通路修飾符、直接超接口的全限定名的有序清單。

    ·每個已裝載類的詳細資訊:運作時常量池、字段資訊、方法資訊、靜态變量、到類classloader的引用、到類class的引用。

  棧記憶體

    Java棧記憶體由局部變量區、操作數棧、幀資料區組成,以幀的形式存放本地方法的調用狀态(包括方法調用的參數、局部變量、中間結果……)。

  堆記憶體

    堆記憶體用來存放由new建立的對象和數組。在堆中配置設定的記憶體,由Java虛拟機的自動垃圾回收器來管理。

  本地方法棧記憶體

    Java通過Java本地接口JNI(Java Native Interface)來調用其它語言編寫的程式,在Java裡面用native修飾符來描述一個方法是本地方法。

  String的記憶體配置設定

    String是一個特殊的包裝類資料,由于String類的值不可變性,當String變量需要經常變換其值時,應該考慮使用StringBuffer或StringBuilder類,以提高程式效率。

Java記憶體配置設定、管理小結 

轉自: http://legend26.blog.163.com/blog/static/13659026020101122103954365/

首先是概念層面的幾個問題:

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屬性的一個具體屬性值。

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; 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(3);//(1) 

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

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棧中。我們在編寫對象的時候,其實即編寫了資料結構,也編寫的處理資料的邏輯。

梅花香自古寒來