最近在自學java基礎,由於嵌入式以后的就業前景不是很好,加上自己本學期學習了51單片機發現自己對硬件不是很在行,可能是因為初中以來物理一直不是很好吧,導致自己現在一看到電路板,電壓電阻電流都會產生一種恐懼感,就像大三現在的我對與數據結構也有一種畏難情緒(不願意花很多時間去研究,很多時候數據結構都和數學邏輯有關,但數據結構真的很重要,大家在大學一定要好好學數據結構,以后對你自己編程會有很大幫助)。由於自己學了很長時間的C(記得那時候對指針很感興趣花了很多課外時間去研究)加上自己本學期學過匯編,涉及到底層。所以每學一門新的語言就會分析它的內存分配問題。
好吧,閑扯了幾句,我最近碰到一個很有趣的問題,關於String str = “123” 和String str =new String(“123”) 分析它的內存分配。
在分析上面那個問題之前我們先來補充下JAVA的一些預備知識:
JAVA中的內存區划分:
棧區:保存基本數據類型(引用),對象的引用
堆區:保存每個對象的具體屬性
全局數據區:保存static類型的屬性
全局代碼區:保存所有方法的定義
常量區:字符常量區(“111”、”112”...)和基本數據常量區(1、2.2...)及其他
接下來舉個簡單的例子,貼代碼:
Class Test { Public static int num = 0; //統計實例化了多少該類的對象 Private int a; Public Test(int a){ This.a = a; Num++; } Public void printA(){ System.out.println(a); } } Public class TestDemo { Public static void main(String arg[]) { int a = 1; int b = 1; Test t1= new Test(1); t1.printA(); } }
為了更直觀我畫了幾個圖,可能比文字描述更直觀。。
在畫圖之前,我們先來說下java文件的編譯過程。。
.java(java文件)經過編譯(在dos界面通過javac.exe借助於jdk編譯,或者一些IDE自帶的編譯器編譯) ----》》》》.class字節碼文件 在通過jdk中的java虛擬機(jvm)對.class字節碼文件在不同的操作系統上 ------》》》運行(關於虛擬機jvm的作用這里不做具體解釋,自行百度哈哈哈)
好了上圖吧(由於是畫圖板請點擊放大查看)
好了,認真看完上文(扯了這么多。。)
就來分析下String str = “123” 和String str =new String(“123”)的內存分配問題吧。
想要了解一個問題,我們得進入到String內中看它的原型(關於如何看源碼一種是看API還一種是自己在調試的時候進入String內后面這種方式得自行百度有詳細的步驟)
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
{ /*進入到這是不是有點想起C了,莫名的親切 */ private final char value[];//加了final關鍵字說明不可string字符串不可改變 private int hash;// Default to 0 private static final long serialVersionUID = -6849794470754667710L; private static final ObjectStreamField[]serialPersistentFields = new ObjectStreamField[0]; public String() { this.value ="".value; } public String(Stringoriginal) { this.value =original.value; this.hash =original.hash; } public String(char value[]) { this.value = Arrays.copyOf(value,value.length); } }
這里重點說兩個問題:
一個是private final char value[]說明了String類中並沒有給字符串的內存分配空間只是分配了一個final char* 指針
二是String類的構造函數 public String(String original)
public String(char value[]),當String str = new String(“xxx”);時是調用那個構造函數呢。
接下來上代碼
按F11進行調試,F5進入String構造函數
注意看調用了public String(Stringoriginal)構造函數,而且是value值之間的相互賦值...
所以到了這里可以簡單畫一個內存分析圖了
分析:
string str1 =“123”,如果string池之前沒有”abc”對象就現在str池實例化一個”abc”對象,然后str1引用指向它(假設這時候在來一句string copyStr1=”abc”, 就同時指向了對象”abc”了)
思考一下這樣做的目的其實是java為了節約內存,只在第一次的時候分配內存,后面如果有其他的引用指向相同的字符串對象就只需要在棧區分配一個引用的空間了。是不是很贊(可以用System.out.println(str1==copyStr1))驗證為True,說明指向的是同一塊空間
String str2 = new String(“abc”);在堆區分配對象的內存然后在調用構造函數將”abc”.value的值賦值給str2對象的value,它們是不同的對象(可以用System.out.println(str1==str2)驗證為False),但它們的value值是相同的都是指向同一個字符串常量”abc”;
綜上所述:String類雖然叫字符類,但String只是一個包裝類,並沒有為字符串分配空間去存取它們,只留了一個value值去指向字符串常量
所以:雖然string str1 = “123” 和String str2 = new String(“abc”)能達到相同的目的,但是出於內存節約的原則,建議用前者,給堆區節省內存
補充一點關於String池:這部分內存應該在堆區jvm自動分配的,有些書上會寫成匿名(String類)對象,這部分內存應該是有java自己的(GC- java回收機制)自己去回收。。。
留一個問題:假如你去面試java有關的工作,技術官問你
string str1 = “123” 和String str2 = new String(“abc”);分別分配了幾次內存???仔細想想
最后關於補充兩個小問題:
1> JAVA中int a = 1和int b = 1的內存分配我發表一下自己看法(可能不正確)
我們在C 語言中我們知道這肯定是分配了兩個int的內存然后值都為1,而且我們輸出a和b的地址 並且a和b的地址不相同。(自己用vs、vc++試驗一下)這里就不截圖了
但是由於java中沒有指針的概念不能與內存直接溝通,所以這里沒法驗證地址。
但是這里有一個很有趣問題或許可以給我們啟發:在C語言中比如直接定義int a;然后輸出a結果是個垃圾值。。。但是在java中如果沒有賦初值輸出的時候會編譯報錯
我們是不是可以這樣想,既然要輸出a的值,編譯器可能要去常量區找值,結合開頭我們提到,a指向常量值(保存某個常量值的地址)這里未初始化,也就是說a沒有存地址,這時候編譯運行的時候發現a沒有地址就不知道怎么去常量區取值了(這只是個人猜想,有待我自己翻閱資料驗證)。
2> 關於str1 = str1 +“xxx” 及字符串比較的問題
我們不是說String類一旦確定就不能改變的么(string語言中有一個value加了final關鍵字,代表value的指向字符串不可以改變)
上一個代碼:
輸出結果如下,(自己可以嘗試畫畫內存圖,分析一下)
我們進入"123abc".equals(str1)的源碼:
紅色框框里面的是不是很熟悉?其實就是我們C語言里很簡單的字符串比較。
但是為什么第一個輸出為false呢?我們可以推測如果直接==的話應該是比較對象地址的值,也就是引用的值,你想一下str1和匿名對象”123abc”在堆棧不同的內存塊中,肯定不相等是吧。
附上我自己畫的很丑的圖。
基於上圖的內存分析圖:str1 = str1 + “abc”,在堆中又重新分配了一塊堆內存(雖然后面GC會自動回收但還是浪費了堆內存)。
所以我們不建議這么做(自己可以看java書,用StringBuffer類比較合適)。
結尾:
由於這是本人的第一篇博客,語言組織可能有點雜亂(個人覺得寫博客是一件很酷的事情,可以把自己想法傳到博客和別人分享)所以一定會有很多地方寫的不夠專業,如果各位讀者看到了有什么錯誤,或者覺得有疑問,和自己平時的認知有沖突的,可以在下面留言板給我留言,大家一起交流,一起進步 -小蝸牛 2017年