java+內存分配及變量存儲位置的區別

1、引題java

在java語言的全部數據類型中,String類型是比較特殊的一種類型,同時也是面試的時候常常被問到的一個知識點,本文結合java內存分配深度分析關於String的許多使人迷惑的問題。下面是本文將要涉及到的一些問題,若是讀者對這些問題都瞭如指掌,則可忽略此文。

一、java內存具體指哪塊內存?這塊內存區域爲何要進行劃分?是如何劃分的?劃分以後每塊區域的做用是什麼?如何設置各個區域的大小?

二、String類型在執行鏈接操做時,效率爲何會比StringBuffer或者StringBuilder低?StringBuffer和StringBuilder有什麼聯繫和區別?

三、java中常量是指什麼?String s = "s" 和 String s = new String("s") 有什麼不同?

本文經多方資料的收集整理和概括,最終撰寫成文,若是有錯誤之處,請多多指教!

2、java內存分配面試

一、JVM簡介
  Java虛擬機(Java Virtual Machine 簡稱JVM)是運行全部Java程序的抽象計算機,是Java語言的運行環境,它是Java 最具吸引力的特性之一。Java虛擬機有本身完善的硬體架構,如處理器、堆棧、寄存器等,還具備相應的指令系統。JVM屏蔽了與具體操做系統平臺相關的信息,使得Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就能夠在多種平臺上不加修改地運行。
        一個運行時的Java虛擬機實例的天職是:負責運行一個java程序。當啓動一個Java程序時,一個虛擬機實例也就誕生了。當該程序關閉退出,這個虛擬機實例也就隨之消亡。若是同一臺計算機上同時運行三個Java程序,將獲得三個Java虛擬機實例。每一個Java程序都運行於它本身的Java虛擬機實例中。
    以下圖所示,JVM的體系結構包含幾個主要的子系統和內存區:
         垃圾回收器(Garbage Collection):負責回收堆內存(Heap)中沒有被使用的對象,即這些對象已經沒有被引用了。
         類裝載子系統(Classloader Sub-System):除了要定位和導入二進制class文件外,還必須負責驗證被導入類的正確性,爲類變量分配並初始化內存,以及幫助解析符號引用。
         執行引擎(Execution Engine):負責執行那些包含在被裝載類的方法中的指令。
         運行時數據區(Java Memory Allocation Area):又叫虛擬機內存或者Java內存,虛擬機運行時須要從整個計算機內存劃分一塊內存區域存儲許多東西。例如:字節碼、從已裝載的class文件中獲得的其餘信息、程序建立的對象、傳遞給方法的參數,返回值、局部變量等等。



二、java內存分區
  從上節知道,運行時數據區便是java內存,並且數據區要存儲的東西比較多,若是不對這塊內存區域進行劃分管理,會顯得比較雜亂無章。程序喜歡有規律的東西,最討厭雜亂無章的東西。 根據存儲數據的不一樣,java內存一般被劃分爲5個區域:程序計數器(Program Count Register)、本地方法棧(Native Stack)、方法區(Methon Area)、棧(Stack)、堆(Heap)。
  程序計數器(Program Count Register):又叫程序寄存器。JVM支持多個線程同時運行,當每個新線程被建立時,它都將獲得它本身的PC寄存器(程序計數器)。若是線程正在執行的是一個Java方法(非native),那麼PC寄存器的值將老是指向下一條將被執行的指令,若是方法是 native的,程序計數器寄存器的值不會被定義。 JVM的程序計數器寄存器的寬度足夠保證能夠持有一個返回地址或者native的指針。
        棧(Stack):又叫堆棧。JVM爲每一個新建立的線程都分配一個棧。也就是說,對於一個Java程序來講,它的運行就是經過對棧的操做來完成的。棧以幀爲單位保存線程的狀態。JVM對棧只進行兩種操做:以幀爲單位的壓棧和出棧操做。咱們知道,某個線程正在執行的方法稱爲此線程的當前方法。咱們可能不知道,當前方法使用的幀稱爲當前幀。當線程激活一個Java方法,JVM就會在線程的 Java堆棧裏新壓入一個幀,這個幀天然成爲了當前幀。在此方法執行期間,這個幀將用來保存參數、局部變量、中間計算過程和其餘數據。從Java的這種分配機制來看,堆棧又能夠這樣理解:棧(Stack)是操做系統在創建某個進程時或者線程(在支持多線程的操做系統中是線程)爲這個線程創建的存儲區域,該區域具備先進後出的特性。其相關設置參數:

-Xss --設置方法棧的最大值算法

本地方法棧(Native Stack):存儲本地方方法的調用狀態。



      方法區(Method Area):當虛擬機裝載一個class文件時,它會從這個class文件包含的二進制數據中解析類型信息,而後把這些類型信息(包括類信息、常量、靜態變量等)放到方法區中,該內存區域被全部線程共享,以下圖所示。本地方法區存在一塊特殊的內存區域,叫常量池(Constant Pool),這塊內存將與String類型的分析密切相關。


      堆(Heap):Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域。在此區域的惟一目的就是存放對象實例,幾乎全部的對象實例都是在這裏分配內存,可是這個對象的引用倒是在棧(Stack)中分配。所以,執行String s = new String("s")時,須要從兩個地方分配內存:在堆中爲String對象分配內存,在棧中爲引用(這個堆對象的內存地址,即指針)分配內存,以下圖所示。



        JAVA虛擬機有一條在堆中分配新對象的指令,卻沒有釋放內存的指令,正如你沒法用Java代碼區明確釋放一個對象同樣。虛擬機本身負責決定如何以及什麼時候釋放再也不被運行的程序引用的對象所佔據的內存,一般,虛擬機把這個任務交給垃圾收集器(Garbage Collection)。其相關設置參數:

-Xms -- 設置堆內存初始大小數組

-Xmx -- 設置堆內存最大值多線程

-XX:MaxTenuringThreshold -- 設置對象在新生代中存活的次數架構

-XX:PretenureSizeThreshold -- 設置超過指定大小的大對象直接分配在舊生代中app

Java堆是垃圾收集器管理的主要區域,所以又稱爲「GC 堆」(Garbage Collectioned Heap)。如今的垃圾收集器基本都是採用的分代收集算法,因此Java堆還能夠細分爲:新生代(Young Generation)和老年代(Old Generation),以下圖所示。分代收集算法的思想:第一種說法,用較高的頻率對年輕的對象(young generation)進行掃描和回收,這種叫作minor collection,而對老對象(old generation)的檢查回收頻率要低不少,稱爲major collection。這樣就不須要每次GC都將內存中全部對象都檢查一遍,以便讓出更多的系統資源供應用系統使用;另外一種說法,在分配對象遇到內存不足時,先對新生代進行GC(Young GC);當新生代GC以後仍沒法知足內存空間分配需求時, 纔會對整個堆空間以及方法區進行GC(Full GC)。


     

    在這裏可能會有讀者表示疑問:記得還有一個什麼永久代(Permanent Generation)的啊,難道它不屬於Java堆?親,你答對了!其實傳說中的永久代就是上面所說的方法區,存放的都是jvm初始化時加載器加載的一些類型信息(包括類信息、常量、靜態變量等),這些信息的生存週期比較長,GC不會在主程序運行期對PermGen Space進行清理,因此若是你的應用中有不少CLASS的話,就極可能出現PermGen Space錯誤。其相關設置參數:

-XX:PermSize --設置Perm區的初始大小jvm

-XX:MaxPermSize --設置Perm區的最大值ide

新生代(Young Generation)又分爲:Eden區和Survivor區,Survivor區有分爲From Space和To Space。Eden區是對象最初分配到的地方;默認狀況下,From Space和To Space的區域大小相等。JVM進行Minor GC時,將Eden中還存活的對象拷貝到Survivor區中,還會將Survivor區中還存活的對象拷貝到Tenured區中。在這種GC模式下,JVM爲了提高GC效率, 將Survivor區分爲From Space和To Space,這樣就能夠將對象回收和對象晉升分離開來。新生代的大小設置有2個相關參數:

-Xmn -- 設置新生代內存大小。函數

-XX:SurvivorRatio -- 設置Eden與Survivor空間的大小比例

老年代(Old Generation): 當 OLD 區空間不夠時, JVM 會在 OLD 區進行 major collection ;徹底垃圾收集後,若Survivor及OLD區仍然沒法存放從Eden複製過來的部分對象,致使JVM沒法在Eden區爲新對象建立內存區域,則出現"Out of memory錯誤"  。

Java內存分配與管理是Java的核心技術之一,以前咱們曾介紹過Java的內存管理與內存泄露以及Java垃圾回收方面的知識,今天咱們再次深刻Java核心,詳細介紹一下Java在內存分配方面的知識。通常Java在內存分配時會涉及到如下區域:

  ◆寄存器:咱們在程序中沒法控制

  ◆棧:存放基本類型的數據和對象的引用,但對象自己不存放在棧中,而是存放在堆中(new 出來的對象)

  ◆堆:存放用new產生的數據

  ◆靜態域:存放在對象中用static定義的靜態成員

  ◆常量池:存放常量

  ◆非RAM存儲:硬盤等永久存儲空間

Java內存分配中的棧

  在函數中定義的一些基本類型的變量數據和對象的引用變量都在函數的棧內存中分配。

  當在一段代碼塊定義一個變量時,Java就在棧中 爲這個變量分配內存空間,當該變量退出該做用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間能夠當即被另做他用。棧中的數據大小和生命週期是能夠肯定的,當沒有引用指向數據時,這個數據就會消失。

Java內存分配中的堆

  堆內存用來存放由new建立的對象和數組。 在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。

  在堆中產生了一個數組或對象後,還能夠 在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。 引用變量就至關因而 爲數組或對象起的一個名稱,之後就能夠在程序中使用棧中的引用變量來訪問堆中的數組或對象。引用變量就至關因而爲數組或者對象起的一個名稱。

  引用變量是普通的變量,定義時在棧中分配,引用變量在程序運行到其做用域以外後被釋放。而數組和對象自己在堆中分配,即便程序 運行到使用 new 產生數組或者對象的語句所在的代碼塊以外,數組和對象自己佔據的內存不會被釋放,數組和對象在沒有引用變量指向它的時候,才變爲垃圾,不能在被使用,但仍 然佔據內存空間不放,在隨後的一個不肯定的時間被垃圾回收器收走(釋放掉)。這也是 Java 比較佔內存的緣由。

  實際上,棧中的變量指向堆內存中的變量,這就是Java中的指針!

堆與棧

  Java的堆是一個運行時數據區,類的(對象從中分配空間。這些對象經過new、newarray、 anewarray和multianewarray等指令創建,它們不須要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優點是能夠動態地分配內存 大小,生存期也沒必要事先告訴編譯器,由於它是在運行時動態分配內存的,Java的垃圾收集器會自動收走這些再也不使用的數據。但缺點是,因爲要在運行時動態 分配內存,存取速度較慢。

  棧的優點是,存取速度比堆要快,僅次於寄存器,棧數據能夠共享。但缺點是,存在棧中的數據大小與生存期必須是 肯定的,缺少靈活性。棧中主要存放一些基本類型的變量數據(int, short, long, byte, float, double, boolean, char)和對象句柄(引用)。

棧有一個很重要的特殊性,就是存在棧中的數據能夠共享。假設咱們同時定義:

  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的狀況。

  這時,若是再令 a=4;那麼編譯器會從新搜索棧中是否有4值,若是沒有,則將4存放進來,並令a指向4;若是已經有了,則直接將a指向這個地址。所以a值的改變不會影響 到b的值。

  要注意這種數據的共享與兩個對象的引用同時指向一個對象的這種共享是不一樣的,由於這種狀況a的修改並不會影響到b, 它是由編譯器完成的,它有利於節省空間。而一個對象引用變量修改了這個對象的內部狀態,會影響到另外一個對象引用變量。

Java代碼 1.int i1 = 9;
2.int i2 = 9;
3.int i3 = 9;
4.public static final int INT1 = 9;
5.public static final int INT2 = 9;
6.public static final int INT3 = 9;

對於成員變量和局部變量:成員變量就是方法外部,類的內部定義的變量;局部變量就是方法或語句塊內部定義的變量。局部變量必須初始化。 形式參數是局部變量,局部變量的數據存在於棧內存中。棧內存中的局部變量隨着方法的消失而消失。 成員變量存儲在堆中的對象裏面,由垃圾回收器負責回收。 如如下代碼: Java代碼 1.class BirthDate {
2. private int day;
3. private int month;
4. private int year;
5. public BirthDate(int d, int m, int y) {
6. day = d;
7. month = m;
8. year = y;
9. }
10. 省略get,set方法………
11.}
12.
13.public class Test{
14. public static void main(String args[]){
15.int date = 9;
16. Test test = new Test();
17. test.change(date);
18. BirthDate d1= new BirthDate(7,7,1970);
19. }
20.
21. public void change1(int i){
22. i = 1234;
23. }
對於以上這段代碼,date爲局部變量,i,d,m,y都是形參爲局部變量,day,month,year爲成員變量。下面分析一下代碼執行時候的變化:

  1. main方法開始執行:int date = 9; date局部變量,基礎類型,引用和值都存在棧中。
  2. Test test = new Test(); test爲對象引用,存在棧中,對象(new Test())存在堆中。
  3. test.change(date); i爲局部變量,引用和值存在棧中。當方法change執行完成後,i就會從棧中消失。
  4. BirthDate d1= new BirthDate(7,7,1970);
    d1 爲對象引用,存在棧中,對象(new BirthDate())存在堆中,其中d,m,y爲局部變量存儲在棧中,且它們的類型爲基礎類型,所以它們的數據也存儲在棧中。 day,month,year爲成員變量,它們存儲在堆中(new BirthDate()裏面)。當BirthDate構造方法執行完以後,d,m,y將從棧中消失。 5.main方法執行完以後,date變量,test,d1引用將從棧中消失,new Test(),new BirthDate()將等待垃圾回收。 常量池 (constant pool)

  常量池指的是在編譯期被肯定,並被保存在已編譯的.class文件中的一些數據。

除了包含代碼中所定義的各類基本類型(如int、long等等)和對象型(如String及數組)的常量值(final)還包含一些以文本形式出現的符號引用,好比:

  ◆類和接口的全限定名;

  ◆字段的名稱和描述符;

  ◆方法和名稱和描述符。

若是是編譯期已經建立好(直接用雙引號定義的)的就存儲在常量池中,若是是運行期(new出來的)才能肯定的就存儲在堆中。對於equals相等的字符串,在常量池中永遠只有一份,在堆中有多份。

String是一個特殊的包裝類數據。能夠用:

  Java代碼

  String str = new String("abc");

  String str = "abc";

  兩種的形式來建立,第一種是用new()來新建對象的,它會在存放於堆中。每調用一次就會建立一個新的對象。而第二種是先在棧中建立一個對 String類的對象引用變量str,而後經過符號引用去字符串常量池 裏找有沒有"abc",若是沒有,則將"abc"存放進字符串常量池 ,並令str指向」abc」,若是已經有」abc」 則直接令str指向「abc」。

  比較類裏面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==,下面用例子說明上面的理論。

  Java代碼

  String str1 = "abc";

  String str2 = "abc";

  System.out.println(str1==str2); //true

  能夠看出str1和str2是指向同一個對象的。

  Java代碼

  String str1 =new String ("abc");

  String str2 =new String ("abc");

  System.out.println(str1==str2); // false

  用new的方式是生成不一樣的對象。每一次生成一個。

  所以用第二種方式建立多個」abc」字符串,在內存中 其實只存在一個對象而已. 這種寫法有利與節省內存空間. 同時它能夠在必定程度上提升程序的運行速度,由於JVM會自動根據棧中數據的實際狀況來決定是否有必要建立新對象。而對於String str = new String("abc");的代碼,則一律在堆中建立新對象,而無論其字符串值是否相等,是否有必要建立新對象,從而加劇了程序的負擔。

  另 一方面, 要注意: 咱們在使用諸如String str = "abc";的格式定義類時,老是想固然地認爲,建立了String類的對象str。擔憂陷阱!對象可能並無被建立!而可能只是指向一個先前已經建立的 對象。只有經過new()方法才能保證每次都建立一個新的對象。

String常量池問題的幾個例子

示例1:

Java代碼

  String s0="kvill";

  String s1="kvill";

  String s2="kv" + "ill";

  System.out.println( s0==s1 );

  System.out.println( s0==s2 );

  結果爲:

  true

  true

分析:首先,咱們要知結果爲道Java 會確保一個字符串常量只有一個拷貝。

  由於例子中的 s0和s1中的」kvill」都是字符串常量,它們在編譯期就被肯定了,因此s0==s1爲true;而」kv」和」ill」也都是字符串常量,當一個字 符串由多個字符串常量鏈接而成時,它本身確定也是字符串常量,因此s2也一樣在編譯期就被解析爲一個字符串常量,因此s2也是常量池中」 kvill」的一個引用。因此咱們得出s0==s1==s2;

示例2:

示例:

  Java代碼

  String s0="kvill";

  String s1=new String("kvill");

  String s2="kv" + new String("ill");

  System.out.println( s0==s1 );

  System.out.println( s0==s2 );

  System.out.println( s1==s2 );

  結果爲:

  false

  false

  false

分析:用new String() 建立的字符串不是常量,不能在編譯期就肯定,因此new String() 建立的字符串不放入常量池中,它們有本身的地址空間。

s0仍是常量池 中"kvill」的應用,s1由於沒法在編譯期肯定,因此是運行時建立的新對象」kvill」的引用,s2由於有後半部分 new String(」ill」)因此也沒法在編譯期肯定,因此也是一個新建立對象」kvill」的應用;明白了這些也就知道爲什麼得出此結果了。

示例3:

Java代碼

  String a = "a1";

  String b = "a" + 1;

  System.out.println((a == b)); //result = true

String a = "atrue";

  String b = "a" + "true";

  System.out.println((a == b)); //result = true

String a = "a3.4";

  String b = "a" + 3.4;

  System.out.println((a == b)); //result = true

分析:JVM對於字符串常量的"+"號鏈接,將程序編譯期,JVM就將常量字符串的"+"鏈接優化爲鏈接後的值,拿"a" + 1來講,經編譯器優化後在class中就已是a1。在編譯期其字符串常量的值就肯定下來,故上面程序最終的結果都爲true。

示例4:

Java代碼

  String a = "ab";

  String bb = "b";

  String b = "a" + bb;

  System.out.println((a == b)); //result = false

分析:JVM對於字符串引用,因爲在字符串的"+"鏈接中,有字符串引用存在,而引用的值在程序編譯期是沒法肯定的,即"a" + bb沒法被編譯器優化,只有在程序運行期來動態分配並將鏈接後的新地址賦給b。因此上面程序的結果也就爲false。

示例5:

Java代碼

  String a = "ab";

  final String bb = "b";

  String b = "a" + bb;

  System.out.println((a == b)); //result = true

分析:和[4]中惟一不一樣的是bb字符串加了final修飾,對於final修飾的變量,它在編譯時被解析爲常量值的一個本地拷貝存儲到本身的常量 池中或嵌入到它的字節碼流中。因此此時的"a" + bb和"a" + "b"效果是同樣的。故上面程序的結果爲true。

示例6:

Java代碼

  String a = "ab";

  final String bb = getBB();

  String b = "a" + bb;

  System.out.println((a == b)); //result = false

  private static String getBB() { return "b"; }

分析:JVM對於字符串引用bb,它的值在編譯期沒法肯定,只有在程序運行期調用方法後,將方法的返回值和"a"來動態鏈接並分配地址爲b,故上面 程序的結果爲false。

關於String是不可變的

經過上面例子能夠得出得知:

  String s = "a" + "b" + "c";

  就等價於String s = "abc";

  String a = "a";

  String b = "b";

  String c = "c";

  String s = a + b + c;

  這個就不同了,最終結果等於:

  Java代碼

  StringBuffer temp = new StringBuffer();

  temp.append(a).append(b).append(c);

  String s = temp.toString();

  由上面的分析結果,可就不難推斷出String 採用鏈接運算符(+)效率低下緣由分析,形如這樣的代碼:

  Java代碼

  public class Test {

   public static void main(String args[]) {

   String s = null;

   for(int i = 0; i < 100; i++) {

   s += "a";

   }

   }

  }

  每作一次 + 就產生個StringBuilder對象,而後append後就扔掉。下次循環再到達時從新產生個StringBuilder對象,而後 append 字符串,如此循環直至結束。若是咱們直接採用 StringBuilder 對象進行 append 的話,咱們能夠節省 N - 1 次建立和銷燬對象的時間。因此對於在循環中要進行字符串鏈接的應用,通常都是用StringBuffer或StringBulider對象來進行 append操做。

  因爲String類的immutable性質,這一說又要說不少,你們只 要知道String的實例一旦生成就不會再改變了,好比說:String str=」kv」+」ill」+」 「+」ans」; 就是有4個字符串常量,首先」kv」和」ill」生成了」kvill」存在內存中,而後」kvill」又和」 」 生成 「kvill 「存在內存中,最後又和生成了」kvill ans」;並把這個字符串的地址賦給了str,就是由於String的」不可變」產生了不少臨時變量,這也就是爲何建議用StringBuffer的原 因了,由於StringBuffer是可改變的。

String中的final用法和理解

  Java代碼

  final StringBuffer a = new StringBuffer("111");

  final StringBuffer b = new StringBuffer("222");

  a=b;//此句編譯不經過

  final StringBuffer a = new StringBuffer("111");

  a.append("222");// 編譯經過

  可見,final只對引用的"值"(即內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會致使編譯期錯誤。至於它所指向的對象 的變化,final是不負責的。

總結

  棧中用來存放一些原始數據類型的局部變量數據和對象的引用(String,數組.對象等等)但不存放對象內容

  堆中存放使用new關鍵字建立的對象.

  字符串是一個特殊包裝類,其引用是存放在棧裏的,而對象內容必須根據建立方式不一樣定(常量池和堆).有的是編譯期就已經建立好,存放在字符串常 量池中,而有的是運行時才被建立.使用new關鍵字,存放在堆中。

相關文章
相關標籤/搜索