Java內(nèi)存模型及原理分析
推薦 + 挑錯(cuò) + 收藏(0) + 用戶評(píng)論(0)
一、Java內(nèi)存模型
按照官方的說(shuō)法:Java 虛擬機(jī)具有一個(gè)堆,堆是運(yùn)行時(shí)數(shù)據(jù)區(qū)域,所有類實(shí)例和數(shù)組的內(nèi)存均從此處分配。
JVM主要管理兩種類型內(nèi)存:堆和非堆,堆內(nèi)存(Heap Memory)是在 Java 虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,非堆內(nèi)存(Non-heap Memory)是在JVM堆之外的內(nèi)存。
簡(jiǎn)單來(lái)說(shuō),非堆包含方法區(qū)、JVM內(nèi)部處理或優(yōu)化所需的內(nèi)存(如 JITCompiler,Just-in-time Compiler,即時(shí)編譯后的代碼緩存)、每個(gè)類結(jié)構(gòu)(如運(yùn)行時(shí)常數(shù)池、字段和方法數(shù)據(jù))以及方法和構(gòu)造方法的代碼。
Java的堆是一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),類的(對(duì)象從中分配空間。這些對(duì)象通過(guò)new、newarray、 anewarray和multianewarray等指令建立,它們不需要程序代碼來(lái)顯式的釋放。堆是由垃圾回收來(lái)負(fù)責(zé)的,堆的優(yōu)勢(shì)是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器,因?yàn)樗窃谶\(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)。但缺點(diǎn)是,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,存取速度較慢。
棧的優(yōu)勢(shì)是,存取速度比堆要快,僅次于寄存器,棧數(shù)據(jù)可以共享。但缺點(diǎn)是,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本類型的變量數(shù)據(jù)(int, short, long, byte, float, double, boolean, char)和對(duì)象句柄(引用)。
虛擬機(jī)必須為每個(gè)被裝載的類型維護(hù)一個(gè)常量池。常量池就是該類型所用到常量的一個(gè)有序集合,包括直接常量(string,integer和 floating point常量)和對(duì)其他類型,字段和方法的符號(hào)引用。
對(duì)于String常量,它的值是在常量池中的。而JVM中的常量池在內(nèi)存當(dāng)中是以表的形式存在的, 對(duì)于String類型,有一張固定長(zhǎng)度的CONSTANT_String_info表用來(lái)存儲(chǔ)文字字符串值,注意:該表只存儲(chǔ)文字字符串值,不存儲(chǔ)符號(hào)引用。說(shuō)到這里,對(duì)常量池中的字符串值的存儲(chǔ)位置應(yīng)該有一個(gè)比較明了的理解了。在程序執(zhí)行的時(shí)候,常量池會(huì)儲(chǔ)存在Method Area,而不是堆中。常量池中保存著很多String對(duì)象; 并且可以被共享使用,因此它提高了效率
具體關(guān)于JVM和內(nèi)存等知識(shí)請(qǐng)參考:
JVM 基礎(chǔ)知識(shí)
Java 內(nèi)存模型及GC原理
二、案例解析 public static void main(String[] args) { /** * 情景一:字符串池 * JAVA虛擬機(jī)(JVM)中存在著一個(gè)字符串池,其中保存著很多String對(duì)象; * 并且可以被共享使用,因此它提高了效率。 * 由于String類是final的,它的值一經(jīng)創(chuàng)建就不可改變。 * 字符串池由String類維護(hù),我們可以調(diào)用intern()方法來(lái)訪問(wèn)字符串池。 */String s1 = “abc”; //↑ 在字符串池創(chuàng)建了一個(gè)對(duì)象 String s2 = “abc”; //↑ 字符串pool已經(jīng)存在對(duì)象“abc”(共享),所以創(chuàng)建0個(gè)對(duì)象,累計(jì)創(chuàng)建一個(gè)對(duì)象 System.out.println( “s1 == s2 : ”+(s1==s2)); //↑ true 指向同一個(gè)對(duì)象,System.out.println( “s1.equals(s2) : ”+ (s1.equals(s2))); //↑ true 值相等 //↑------------------------------------------------------over /** * 情景二:關(guān)于new String(“”) * */String s3 = newString(“abc”); //↑ 創(chuàng)建了兩個(gè)對(duì)象,一個(gè)存放在字符串池中,一個(gè)存在與堆區(qū)中; //↑ 還有一個(gè)對(duì)象引用s3存放在棧中 String s4 = newString( “abc”); //↑ 字符串池中已經(jīng)存在“abc”對(duì)象,所以只在堆中創(chuàng)建了一個(gè)對(duì)象 System.out.println( “s3 == s4 : ”+(s3==s4)); //↑false s3和s4棧區(qū)的地址不同,指向堆區(qū)的不同地址; System.out.println( “s3.equals(s4) : ”+(s3.equals(s4))); //↑true s3和s4的值相同 System.out.println( “s1 == s3 : ”+(s1==s3)); //↑false 存放的地區(qū)多不同,一個(gè)棧區(qū),一個(gè)堆區(qū) System.out.println( “s1.equals(s3) : ”+(s1.equals(s3))); //↑true 值相同 //↑------------------------------------------------------over /** * 情景三: * 由于常量的值在編譯的時(shí)候就被確定(優(yōu)化)了。 * 在這里,“ab”和“cd”都是常量,因此變量str3的值在編譯時(shí)就可以確定。 * 這行代碼編譯后的效果等同于: String str3 = “abcd”; */String str1 = “ab”+ “cd”; //1個(gè)對(duì)象 String str11 = “abcd”; System.out.println( “str1 = str11 : ”+ (str1 == str11)); //↑------------------------------------------------------over /** * 情景四: * 局部變量str2,str3存儲(chǔ)的是存儲(chǔ)兩個(gè)拘留字符串對(duì)象(intern字符串對(duì)象)的地址。 * * 第三行代碼原理(str2+str3): * 運(yùn)行期JVM首先會(huì)在堆中創(chuàng)建一個(gè)StringBuilder類, * 同時(shí)用str2指向的拘留字符串對(duì)象完成初始化, * 然后調(diào)用append方法完成對(duì)str3所指向的拘留字符串的合并, * 接著調(diào)用StringBuilder的toString()方法在堆中創(chuàng)建一個(gè)String對(duì)象, * 最后將剛生成的String對(duì)象的堆地址存放在局部變量str3中。 * * 而str5存儲(chǔ)的是字符串池中“abcd”所對(duì)應(yīng)的拘留字符串對(duì)象的地址。 * str4與str5地址當(dāng)然不一樣了。 * * 內(nèi)存中實(shí)際上有五個(gè)字符串對(duì)象: * 三個(gè)拘留字符串對(duì)象、一個(gè)String對(duì)象和一個(gè)StringBuilder對(duì)象。 */String str2 = “ab”; //1個(gè)對(duì)象 String str3 = “cd”; //1個(gè)對(duì)象 String str4 = str2+str3; String str5 = “abcd”; System.out.println( “str4 = str5 : ”+ (str4==str5)); // false //↑------------------------------------------------------over /** * 情景五: * JAVA編譯器對(duì)string + 基本類型/常量 是當(dāng)成常量表達(dá)式直接求值來(lái)優(yōu)化的。 * 運(yùn)行期的兩個(gè)string相加,會(huì)產(chǎn)生新的對(duì)象的,存儲(chǔ)在堆(heap)中 */String str6 = “b”; String str7 = “a”+ str6; String str67 = “ab”; System.out.println( “str7 = str67 : ”+ (str7 == str67)); //↑str6為變量,在運(yùn)行期才會(huì)被解析。 finalString str8 = “b”; String str9 =“a”+ str8; String str89 = “ab”; System.out.println( “str9 = str89 : ”+ (str9 == str89)); //↑str8為常量變量,編譯期會(huì)被優(yōu)化 //↑------------------------------------------------------over }
總結(jié):
1.String類初始化后是不可變的(immutable)
這一說(shuō)又要說(shuō)很多,大家只要知道String的實(shí)例一旦生成就不會(huì)再改變了,比如說(shuō):String str=”kv”+”ill”+” “+”ans”; 就是有4個(gè)字符串常量,首先”kv”和”ill”生成了”kvill”存在內(nèi)存中,然后”kvill”又和” ” 生成 “kvill “存在內(nèi)存中,最后又和生成了”kvill ans”;并把這個(gè)字符串的地址賦給了str,就是因?yàn)镾tring的”不可變”產(chǎn)生了很多臨時(shí)變量,這也就是為什么建議用StringBuffer的原 因了,因?yàn)镾tringBuffer是可改變的。
下面是一些String相關(guān)的常見(jiàn)問(wèn)題:
String中的final用法和理解
final StringBuffer a = new StringBuffer(“111”);
final StringBuffer b = new StringBuffer(“222”);
a=b;//此句編譯不通過(guò) final StringBuffer a = new StringBuffer(“111”);
a.append(“222”);// 編譯通過(guò)
可見(jiàn),final只對(duì)引用的”值”(即內(nèi)存地址)有效,它迫使引用只能指向初始指向的那個(gè)對(duì)象,改變它的指向會(huì)導(dǎo)致編譯期錯(cuò)誤。至于它所指向的對(duì)象的變化,final是不負(fù)責(zé)的。
2.代碼中的字符串常量在編譯的過(guò)程中收集并放在class文件的常量區(qū)中,如”123”、”123”+”456”等,含有變量的表達(dá)式不會(huì)收錄,如”123”+a。
3.JVM在加載類的時(shí)候,根據(jù)常量區(qū)中的字符串生成常量池,每個(gè)字符序列如”123”會(huì)生成一個(gè)實(shí)例放在常量池里,這個(gè)實(shí)例是不在堆里的,也不會(huì)被GC,這個(gè)實(shí)例的value屬性從源碼的構(gòu)造函數(shù)看應(yīng)該是用new創(chuàng)建數(shù)組置入123的,所以按我的理解此時(shí)value存放的字符數(shù)組地址是在堆里,如果有誤的話歡迎大家指正。
4.使用String不一定創(chuàng)建對(duì)象
在執(zhí)行到雙引號(hào)包含字符串的語(yǔ)句時(shí),如String a = “123”,JVM會(huì)先到常量池里查找,如果有的話返回常量池里的這個(gè)實(shí)例的引用,否則的話創(chuàng)建一個(gè)新實(shí)例并置入常量池里。如果是 String a = “123” + b (假設(shè)b是”456”),前半部分”123”還是走常量池的路線,但是這個(gè)+操作符其實(shí)是轉(zhuǎn)換成[SringBuffer].Appad()來(lái)實(shí)現(xiàn)的,所以最終a得到是一個(gè)新的實(shí)例引用,而且a的value存放的是一個(gè)新申請(qǐng)的字符數(shù)組內(nèi)存空間的地址(存放著”123456”),而此時(shí)”123456”在常量池中是未必存在的。
要注意: 我們?cè)谑褂弥T如String str = “abc”;的格式定義類時(shí),總是想當(dāng)然地認(rèn)為,創(chuàng)建了String類的對(duì)象str。擔(dān)心陷阱!對(duì)象可能并沒(méi)有被創(chuàng)建!而可能只是指向一個(gè)先前已經(jīng)創(chuàng)建的對(duì)象。只有通過(guò)new()方法才能保證每次都創(chuàng)建一個(gè)新的對(duì)象
5.使用new String,一定創(chuàng)建對(duì)象
在執(zhí)行String a = new String(“123”)的時(shí)候,首先走常量池的路線取到一個(gè)實(shí)例的引用,然后在堆上創(chuàng)建一個(gè)新的String實(shí)例,走以下構(gòu)造函數(shù)給value屬性賦值,然后把實(shí)例引用賦值給a:
publicString(String original) { intsize = original.count; char[] originalValue = original. value;char[] v; if(originalValue.length 》 size) { // The array representing the String is bigger than the new// String itself. Perhaps this constructor is being called// in order to trim the baggage, so make a copy of the array.intoff = original.offset; v = Arrays.copyOfRange(originalValue, off, off+size); } else{ // The array representing the String is the same// size as the String, so no point in making a copy.v = originalValue; }this.offset = 0; this.count = size; this. value= v; }
從中我們可以看到,雖然是新創(chuàng)建了一個(gè)String的實(shí)例,但是value是等于常量池中的實(shí)例的value,即是說(shuō)沒(méi)有new一個(gè)新的字符數(shù)組來(lái)存放”123”。
如果是String a = new String(“123”+b)的情況,首先看回第4點(diǎn),”123”+b得到一個(gè)實(shí)例后,再按上面的構(gòu)造函數(shù)執(zhí)行。
6.String.intern()
String對(duì)象的實(shí)例調(diào)用intern方法后,可以讓JVM檢查常量池,如果沒(méi)有實(shí)例的value屬性對(duì)應(yīng)的字符串序列比如”123”(注意是檢查字符串序列而不是檢查實(shí)例本身),就將本實(shí)例放入常量池,如果有當(dāng)前實(shí)例的value屬性對(duì)應(yīng)的字符串序列”123”在常量池中存在,則返回常量池中”123”對(duì)應(yīng)的實(shí)例的引用而不是當(dāng)前實(shí)例的引用,即使當(dāng)前實(shí)例的value也是”123”。
publicnativeString intern();
存在于.class文件中的常量池,在運(yùn)行期被JVM裝載,并且可以擴(kuò)充。String的 intern()方法就是擴(kuò)充常量池的 一個(gè)方法;當(dāng)一個(gè)String實(shí)例str調(diào)用intern()方法時(shí),Java 查找常量池中 是否有相同Unicode的字符串常量,如果有,則返回其的引用,如果沒(méi)有,則在常 量池中增加一個(gè)Unicode等于str的字符串并返回它的引用;看示例就清楚了
/** * Java學(xué)習(xí)交流QQ群:589809992 我們一起學(xué)Java! */publicstaticvoidmain(String[] args) { String s0 = “kvill”; String s1 = newString( “kvill”); String s2 = newString( “kvill”); System.out.println( s0 == s1 ); //falseSystem.out.println( “**********”); s1.intern(); //雖然執(zhí)行了s1.intern(),但它的返回值沒(méi)有賦給s1s2 = s2.intern(); //把常量池中“kvill”的引用賦給s2System.out.println( s0 == s1); //flaseSystem.out.println( s0 == s1.intern() ); //true//說(shuō)明s1.intern()返回的是常量池中“kvill”的引用System.out.println( s0 == s2 ); //true}
最后我再破除一個(gè)錯(cuò)誤的理解:有人說(shuō),“使用 String.intern() 方法則可以將一個(gè) String 類的保存到一個(gè)全局 String 表中 ,如果具有相同值的 Unicode 字符串已經(jīng)在這個(gè)表中,那么該方法返回表中已有字符串的地址,如果在表中沒(méi)有相同值的字符串,則將自己的地址注冊(cè)到表中”如果我把他說(shuō)的這個(gè)全局的 String 表理解為常量池的話,他的最后一句話,”如果在表中沒(méi)有相同值的字符串,則將自己的地址注冊(cè)到表中”是錯(cuò)的:
publicstaticvoidmain(String[] args) { String s1 = newString( “kvill”); String s2 = s1.intern(); System. out.println( s1 == s1.intern() ); //falseSystem. out.println( s1 + “ ”+ s2 ); //kvill kvillSystem. out.println( s2 == s1.intern() ); //true}
在這個(gè)類中我們沒(méi)有聲名一個(gè)”kvill”常量,所以常量池中一開(kāi)始是沒(méi)有”kvill”的,當(dāng)我們調(diào)用s1.intern()后就在常量池中新添加了一 個(gè)”kvill”常量,原來(lái)的不在常量池中的”kvill”仍然存在,也就不是“將自己的地址注冊(cè)到常量池中”了。
s1==s1.intern() 為false說(shuō)明原來(lái)的”kvill”仍然存在;s2現(xiàn)在為常量池中”kvill”的地址,所以有s2==s1.intern()為true。
StringBuffer與StringBuilder的區(qū)別,它們的應(yīng)用場(chǎng)景是什么?
jdk的實(shí)現(xiàn)中StringBuffer與StringBuilder都繼承自AbstractStringBuilder,對(duì)于多線程的安全與非安全看到StringBuffer中方法前面的一堆synchronized就大概了解了。
這里隨便講講AbstractStringBuilder的實(shí)現(xiàn)原理:我們知道使用StringBuffer等無(wú)非就是為了提高java中字符串連接的效率,因?yàn)橹苯邮褂?進(jìn)行字符串連接的話,jvm會(huì)創(chuàng)建多個(gè)String對(duì)象,因此造成一定的開(kāi)銷。AbstractStringBuilder中采用一個(gè)char數(shù)組來(lái)保存需要append的字符串,char數(shù)組有一個(gè)初始大小,當(dāng)append的字符串長(zhǎng)度超過(guò)當(dāng)前char數(shù)組容量時(shí),則對(duì)char數(shù)組進(jìn)行動(dòng)態(tài)擴(kuò)展,也即重新申請(qǐng)一段更大的內(nèi)存空間,然后將當(dāng)前char數(shù)組拷貝到新的位置,因?yàn)橹匦路峙鋬?nèi)存并拷貝的開(kāi)銷比較大,所以每次重新申請(qǐng)內(nèi)存空間都是采用申請(qǐng)大于當(dāng)前需要的內(nèi)存空間的方式,這里是2倍
【
StringBuffer 始于 JDK 1.0
StringBuilder 始于 JDK 1.5
從 JDK 1.5 開(kāi)始,帶有字符串變量的連接操作(+),JVM 內(nèi)部采用的是
StringBuilder 來(lái)實(shí)現(xiàn)的,而之前這個(gè)操作是采用 StringBuffer 實(shí)現(xiàn)的。
】
我們通過(guò)一個(gè)簡(jiǎn)單的程序來(lái)看其執(zhí)行的流程:
/** * Java學(xué)習(xí)交流QQ群:589809992 我們一起學(xué)Java! */publicclassBuffer{publicstaticvoidmain(String[] args) { String s1 = “aaaaa”; String s2 =“bbbbb”; String r = null; inti = 3694; r = s1 + i + s2; for( intj= 0;i《 10;j++){ r+= “23124”; } } }
使用命令javap -c Buffer查看其字節(jié)碼實(shí)現(xiàn):
將清單1和清單2對(duì)應(yīng)起來(lái)看,清單2的字節(jié)碼中l(wèi)dc指令即從常量池中加載“aaaaa”字符串到棧頂,istore_1將“aaaaa”存到變量1中,后面的一樣,sipush是將一個(gè)短整型常量值(-32768~32767)推送至棧頂,這里是常量“3694”。
讓我們直接看到13,13~17是new了一個(gè)StringBuffer對(duì)象并調(diào)用其初始化方法,20 ~ 21則是先通過(guò)aload_1將變量1壓到棧頂,前面說(shuō)過(guò)變量1放的就是字符串常量“aaaaa”,接著通過(guò)指令invokevirtual調(diào)用StringBuffer的append方法將“aaaaa”拼接起來(lái),后續(xù)的24 ~ 30同理。最后在33調(diào)用StringBuffer的toString函數(shù)獲得String結(jié)果并通過(guò)astore存到變量3中。
看到這里可能有人會(huì)說(shuō),“既然JVM內(nèi)部采用了StringBuffer來(lái)連接字符串了,那么我們自己就不用用StringBuffer,直接用”+“就行了吧!“。是么?當(dāng)然不是了。俗話說(shuō)”存在既有它的理由”,讓我們繼續(xù)看后面的循環(huán)對(duì)應(yīng)的字節(jié)碼。
37~ 42都是進(jìn)入for循環(huán)前的一些準(zhǔn)備工作,37,38是將j置為1。44這里通過(guò)if_icmpge將j與10進(jìn)行比較,如果j大于10則直接跳轉(zhuǎn)到73,也即return語(yǔ)句退出函數(shù);否則進(jìn)入循環(huán),也即47~66的字節(jié)碼。這里我們只需看47到51就知道為什么我們要在代碼中自己使用StringBuffer來(lái)處理字符串的連接了,因?yàn)槊看螆?zhí)行“+”操作時(shí)jvm都要new一個(gè)StringBuffer對(duì)象來(lái)處理字符串的連接,這在涉及很多的字符串連接操作時(shí)開(kāi)銷會(huì)很大。
非常好我支持^.^
(0) 0%
不好我反對(duì)
(0) 0%