熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

深入Java核心 Java內存分配原理精講

2022-06-13   來源: Java核心技術 

  Java內存分配與管理是Java的核心技術之一之前我們曾介紹過Java的內存管理與內存洩露以及Java垃圾回收方面的知識今天我們再次深入Java核心詳細介紹一下Java在內存分配方面的知識一般Java在內存分配時會涉及到以下區域

  ◆寄存器我們在程序中無法控制

  ◆棧存放基本類型的數據和對象的引用但對象本身不存放在棧中而是存放在堆中

  ◆堆存放用new產生的數據

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

  ◆常量池存放常量

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

  Java內存分配中的棧

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

  當在一段代碼塊定義一個變量時Java就在棧中 為這個變量分配內存空間當該變量退出該作用域後Java會自動釋放掉為該變量所分配的內存空間該內存空間可以立即被另作他用

  Java內存分配中的堆

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

  在堆中產生了一個數組或對象後還可以 在棧中定義一個特殊的變量讓棧中這個變量的取值等於數組或對象在堆內存中的首地址棧中的這個變量就成了數組或對象的引用變量  引用變量就相當於是 為數組或對象起的一個名稱以後就可以在程序中使用棧中的引用變量來訪問堆中的數組或對象引用變量就相當於是為數組或者對象起的一個名稱

  引用變量是普通的變量定義時在棧中分配引用變量在程序運行到其作用域之外後被釋放而數組和對象本身在堆中分配即使程序 運行到使用 new 產生數組或者對象的語句所在的代碼塊之外數組和對象本身占據的內存不會被釋放數組和對象在沒有引用變量指向它的時候才變為垃圾不能在被使用但仍 然占據內存空間不放在隨後的一個不確定的時間被垃圾回收器收走(釋放掉)這也是 Java 比較占內存的原因

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

  常量池 (constant pool)

  常量池指的是在編譯期被確定並被保存在已編譯的class文件中的一些數據除了包含代碼中所定義的各種基本類型(如intlong等等)和對象型(如String及數組)的常量值(final)還包含一些以文本形式出現的符號引用比如

  ◆類和接口的全限定名

  ◆字段的名稱和描述符

  ◆方法和名稱和描述符

  虛擬機必須為每個被裝載的類型維護一個常量池常量池就是該類型所用到常量的一個有序集和包括直接常量(stringinteger和 floating point常量)和對其他類型字段和方法的符號引用

  對於String常量它的值是在常量池中的而JVM中的常量池在內存當中是以表的形式存在的 對於String類型有一張固定長度的CONSTANT_String_info表用來存儲文字字符串值注意該表只存儲文字字符串值不存儲符號引 用說到這裡對常量池中的字符串值的存儲位置應該有一個比較明了的理解了

  在程序執行的時候常量池 會儲存在Method Area而不是堆中

  堆與棧

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

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

  棧有一個很重要的特殊性就是存在棧中的數據可以共享假設我們同時定義

  Java代碼

  int a = ;

  int b = ;

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

  這時如果再令 a=那麼編譯器會重新搜索棧中是否有如果沒有則將存放進來並令a指向如果已經有了則直接將a指向這個地址因此a值的改變不會影響 到b的值

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

  String是一個特殊的包裝類數據可以用

  Java代碼

  String str = new String(abc);

  String str = abc;

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

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

  Java代碼

  String str = abc;

  String str = abc;

  Systemoutprintln(str==str); //true

  可以看出str和str是指向同一個對象的

  Java代碼

  String str =new String (abc);

  String str =new String (abc);

  Systemoutprintln(str==str); // false

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

  因此用第二種方式創建多個abc字符串在內存中 其實只存在一個對象而已 這種寫法有利與節省內存空間 同時它可以在一定程度上提高程序的運行速度因為JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象而對於String str = new String(abc)的代碼則一概在堆中創建新對象而不管其字符串值是否相等是否有必要創建新對象從而加重了程序的負擔

  另 一方面 要注意: 我們在使用諸如String str = abc的格式定義類時總是想當然地認為創建了String類的對象str擔心陷阱!對象可能並沒有被創建!而可能只是指向一個先前已經創建的 對象只有通過new()方法才能保證每次都創建一個新的對象

  由於String類的immutable性質當String變量需要經常變換 其值時應該考慮使用StringBuffer類以提高程序效率

   首先String不屬於種基本數據類型String是一個對象因為對象的默認值是null所以String的默認值也是null但它又是一種特殊的對象有其它對象沒有的一些特性

   new String()和new String()都是申明一個新的空字符串是空串不是null

   String str=kvillString str=new String (kvill)的區別

  示例

  Java代碼

  String s=kvill;

  String s=kvill;

  String s=kv + ill;

  Systemoutprintln( s==s );

  Systemoutprintln( s==s );

  結果為

  true

  true

  首先我們要知結果為道Java 會確保一個字符串常量只有一個拷貝

  因為例子中的 s和s中的kvill都是字符串常量它們在編譯期就被確定了所以s==s為truekvill也都是字符串常量當一個字 符串由多個字符串常量連接而成時它自己肯定也是字符串常量所以s也同樣在編譯期就被解析為一個字符串常量所以s也是常量池中 kvill的一個引用所以我們得出s==s==s用new String() 創建的字符串不是常量不能在編譯期就確定所以new String() 創建的字符串不放入常量池中它們有自己的地址空間

  示例

  Java代碼

  String s=kvill;

  String s=new String(kvill);

  String s=kv + new String(ill);

  Systemoutprintln( s==s );

  Systemoutprintln( s==s );

  Systemoutprintln( s==s );

  結果為

  false

  false

  false

  例中s還是常量池 中kvill的應用s因為無法在編譯期確定所以是運行時創建的新對象kvill的引用s因為有後半部分 new String(ill)所以也無法在編譯期確定所以也是一個新創建對象kvill的應用;明白了這些也就知道為何得出此結果了

   Stringintern()

  再補充介紹一點存在於class文件中的常量池在運行期被JVM裝載並且可以擴充String的 intern()方法就是擴充常量池的 一個方法當一個String實例str調用intern()方法時Java 查找常量池中 是否有相同Unicode的字符串常量如果有則返回其的引用如果沒有則在常 量池中增加一個Unicode等於str的字符串並返回它的引用看示例就清楚了

  示例

  Java代碼

  String s= kvill;

  String s=new String(kvill);

  String s=new String(kvill);

  Systemoutprintln( s==s );

  Systemoutprintln( ********** );

  sintern();

  s=sintern(); //把常量池中kvill的引用賦給s

  Systemoutprintln( s==s);

  Systemoutprintln( s==sintern() );

  Systemoutprintln( s==s );

  結果為

  false

  false //雖然執行了sintern()但它的返回值沒有賦給s

  true //說明sintern()返回的是常量池中kvill的引用

  true

  最後我再破除一個錯誤的理解有人說使用 Stringintern() 方法則可以將一個 String 類的保存到一個全局 String 表中 如果具有相同值的 Unicode 字符串已經在這個表中那麼該方法返回表中已有字符串的地址如果在表中沒有相同值的字符串則將自己的地址注冊到表中如果我把他說的這個全局的 String 表理解為常量池的話他的最後一句話如果在表中沒有相同值的字符串則將自己的地址注冊到表中是錯的

  示例

  Java代碼

  String s=new String(kvill);

  String s=sintern();

  Systemoutprintln( s==sintern() );

  Systemoutprintln( s+ +s );

  Systemoutprintln( s==sintern() );

  結果

  false

  kvill kvill

  true

  在這個類中我們沒有聲名一個kvill常量所以常量池中一開始是沒有kvill當我們調用sintern()後就在常量池中新添加了一 個kvill常量原來的不在常量池中的kvill仍然存在也就不是將自己的地址注冊到常量池中

  s==sintern() 為false說明原來的kvill仍然存在s現在為常量池中kvill的地址所以有s==sintern()為true

   關於equals()和==:

  這個對於String簡單來說就是比較兩字符串的Unicode序列是否相當如果相等返回true;而==是 比較兩字符串的地址是否相同也就是是否是同一個字符串的引用

   關於String是不可變的

  這一說又要說很多大家只 要知道String的實例一旦生成就不會再改變了比如說String str=kv+ill+ +ans; 就是有個字符串常量首先kvill生成了kvill存在內存中然後kvill又和 生成 kvill 存在內存中最後又和生成了kvill ans;並把這個字符串的地址賦給了str就是因為String的不可變產生了很多臨時變量這也就是為什麼建議用StringBuffer的原 因了因為StringBuffer是可改變的

  下面是一些String相關的常見問題

  String中的final用法和理解

  Java代碼

  final StringBuffer a = new StringBuffer();

  final StringBuffer b = new StringBuffer();

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

  final StringBuffer a = new StringBuffer();

  aappend();// 編譯通過

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

  String常量池問題的幾個例子

  下面是幾個常見例子的比較分析和理解

  Java代碼

  String a = a;

  String b = a + ;

  Systemoutprintln((a == b)); //result = true  String a = atrue;

  String b = a + true;

  Systemoutprintln((a == b)); //result = true  String a = a;

  String b = a + ;

  Systemoutprintln((a == b)); //result = true

  分析JVM對於字符串常量的+號連接將程序編譯期JVM就將常量字符串的+連接優化為連接後的值a + 來說經編譯器優化後在class中就已經是a在編譯期其字符串常量的值就確定下來故上面程序最終的結果都為true

  Java代碼

  String a = ab;

  String bb = b;

  String b = a + bb;

  Systemoutprintln((a == b)); //result = false

  分析JVM對於字符串引用由於在字符串的+連接中有字符串引用存在而引用的值在程序編譯期是無法確定的a + bb無法被編譯器優化只有在程序運行期來動態分配並將連接後的新地址賦給b所以上面程序的結果也就為false

  Java代碼

  String a = ab;

  final String bb = b;

  String b = a + bb;

  Systemoutprintln((a == b)); //result = true

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

  Java代碼

  String a = ab;

  final String bb = getBB();

  String b = a + bb;

  Systemoutprintln((a == b)); //result = false

  private static String getBB() {  return b;   }

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

  通過上面個例子可以得出得知

  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();

  tempappend(a)append(b)append(c);

  String s = temptoString();

  由上面的分析結果可就不難推斷出String 采用連接運算符(+)效率低下原因分析形如這樣的代碼

  Java代碼

  public class Test {

  public static void main(String args[]) {

  String s = null;

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

  s += a;

  }

  }

  }

  每做一次 + 就產生個StringBuilder對象然後append後就扔掉下次循環再到達時重新產生個StringBuilder對象然後 append 字符串如此循環直至結束如果我們直接采用 StringBuilder 對象進行 append 的話我們可以節省 N 次創建和銷毀對象的時間所以對於在循環中要進行字符串連接的應用一般都是用StringBuffer或StringBulider對象來進行 append操作

  String對象的intern方法理解和分析

  Java代碼

  public class Test {

  private static String a = ab;

  public static void main(String[] args){

  String s = a;

  String s = b;

  String s = s + s;

  Systemoutprintln(s == a);//false

  Systemoutprintln(sintern() == a);//true

  }

  }

  這裡用到Java裡面是一個常量池的問題對於s+s操作其實是在堆裡面重新創建了一個新的對象s保存的是這個新對象在堆空間的的內容所 以s與a的值是不相等的而當調用sintern()方法卻可以返回s在常量池中的地址值因為a的值存儲在常量池中故sintern和a的值相等

  總結

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

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

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


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26299.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.