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