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. 每個類的全限定名
  2. 每個類的直接超類的全限定名(可約束類型轉換)
  3. 該類是類還是接口
  4. 該類型的訪問修飾符
  5. 直接超接口的全限定名的有序列表

             1.1.2已裝載類的詳細信息

  1. 運行時常量池:

    在方法區中,每個類型都對應一個常量池,存放該類型所用到的所有常量,常量池中存儲了諸如文字字符串、final變量值、類名和方法名常量。它們以數組形式通過索引被訪問,是外部調用與類聯繫及類型對象化的橋樑。(存的可能是個普通的字符串,然後經過常量池解析,則變成指向某個類的引用)

  2. 字段信息:

    字段信息存放類中聲明的每一個字段的信息,包括字段的名、類型、修飾符。

    字段名稱指的是類或接口的實例變量或類變量,字段的描述符是一個指示字段的類型的字符串,如private A a=null;則a爲字段名,A爲描述符,private爲修飾符

  3. 方法信息:

    類中聲明的每一個方法的信息,包括方法名、返回值類型、參數類型、修飾符、異常、方法的字節碼。

    (在編譯的時候,就已經將方法的局部變量、操作數棧大小等確定並存放在字節碼中,在裝載的時候,隨着類一起裝入方法區。)

在運行時,JVM從常量池中獲得符號引用,然後在運行時解析成引用項的實際地址,最後通過常量池中的全限定名、方法和字段描述符,把當前類或接口中的代碼與其它類或接口中的代碼聯繫起來。
  1. 靜態變量:

    這個沒什麼好說的,就是類變量,類的所有實例都共享,我們只需知道,在方法區有個靜態區,靜態區專門存放靜態變量和靜態塊。

  2. 到類classloader的引用:到該類的類裝載器的引用。
  3. 到類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代碼
  1. int a=3; 
  2. 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的情況。

    而如果我們定義:

Java代碼
  1. Integer a=new Integer(3);//(1) 
  2. 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棧中。我們在編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章