關於String相關知識都是老掉牙的東西了,但咱們常常可能在不經意的String 字符串拼接的狀況下浪費內存,影響性能,也經常會成爲觸發內存OOM的最後一步。
因此本文對String字符串進行深度解析,有助於咱們平常開發中提升程序的性能,解決因String 而致使的性能問題。javascript
首先咱們先回顧一下String類型的本質 php
先看一下String的頭部源碼java
/** Strings are constant; their values cannot be changed after they * are created. String buffers support mutable strings. * Because String objects are immutable they can be shared. * @see StringBuffer * @see StringBuilder * @see Charset * @since 1.0 */ public final class String implements Serializable, Comparable<String>, CharSequence { private static final long serialVersionUID = -6849794470754667710L; private static final char REPLACEMENT_CHAR = (char) 0xfffd;
打開String的源碼,類註釋中有這麼一段話「Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.」。 android
這句話總結概括了String的一個最重要的特色: 數組
String是值不可變(immutable)的常量,是線程安全的(can be shared)。
接下來,String類使用了final修飾符,代表了String類的第二個特色:String類是不可繼承的。緩存
String類表示字符串。java程序中的全部字符串,如「ABC」,是實現這個類的實例安全
字符串是常量,它們的值不能被建立後改變。支持可變字符串字符串緩衝區。由於字符串對象是不可改變的,因此它們能夠被共享。例如:性能優化
String str = "abc";
至關於markdown
String s = new String("abc");
這裏實際上建立了兩個String對象,一個是」abc」對象,存儲在常量空間中,一個是使用new關鍵字爲對象s申請的空間,存儲引用地址。app
在執行到雙引號包含字符串的語句時,JVM會先到常量池裏查找,若是有的話返回常量池裏的這個實例的引用,不然的話建立一個新實例並置入常量池裏,如上面所示,str 和 s 指向同一個引用.
String的定義方法概括起來總共爲如下四種方式:
在討論String的一些本質,先了解一下常量池的概念java中的常量池(constant pool)技術,是爲了方便快捷地建立某些對象而出現的,當須要一個對象時,就能夠從池中取一個出來(若是池中沒有則建立一個),則在須要重複重複建立相等變量時節省了不少時間。常量池其實也就是一個內存空間,不一樣於使用new關鍵字建立的對象所在的堆空間。
在編譯期被肯定,並被保存在已編譯的.class文件中的一些數據。它包括了關於類、方法、接口等中的常量,也包括字符串常量。常量池還具有動態性(java.lang.String.intern()),運行期間能夠將新的常量放入池中。
常量池是爲了不頻繁的建立和銷燬對象而影響系統性能,其實現了對象的共享。
java中基本類型的包裝類的大部分都實現了常量池技術,
即Byte,Short,Integer,Long,Character,Boolean;
JAVA中全部的對象都存放在堆裏面,包括String對象。字符串常量保存在JAVA的.class文件的常量池中,在編譯期就肯定好了。
好比咱們經過如下代碼塊:
String s = new String( "myString" );
其中字符串常量是」myString」,在編譯時被存儲在常量池的某個位置。在運行階段,虛擬機發現字符串常量」myString」,它會在一個內部字符串常量列表中查找,若是沒有找到,那麼會在堆裏面建立一個包含字符序列[myString]的String對象s1,而後把這個字符序列和對應的String對象做爲名值對( [myString], s1 )保存到內部字符串常量列表中。以下圖所示:
若是虛擬機後面又發現了一個相同的字符串常量myString,它會在這個內部字符串常量列表內找到相同的字符序列,而後返回對應的String對象的引用。維護這個內部列表的關鍵是任何特定的字符序列在這個列表上只出現一次。
例如,String s2 = 「myString」,運行時s2會從內部字符串常量列表內獲得s1的返回值,因此s2和s1都指向同一個String對象。可是String對象s在堆裏的一個不一樣位置,因此和s1不相同。
JAVA中的字符串常量能夠做爲String對象使用,字符串常量的字符序列自己是存放在常量池中,在字符串內部列表中每一個字符串常量的字符序列對應一個String對象,實際使用的就是這個對象。
這個目前網上闡述的最多關於這個String對象和字符串常量的關係,網上各有說法,可是這個猜測也是有問題的
引自感謝博主
http://blog.csdn.net/sureyonder/article/details/5569366
String 在 JVM 的存儲結構
通常而言,Java 對象在虛擬機的結構以下:
對象頭(object header):8 個字節
Java 原始類型數據:如 int, float, char 等類型的數據,各種型數據佔內存如 表 1. Java 各數據類型所佔內存.
引用(reference):4 個字節
填充符(padding)
若是對於 String(JDK 6)的成員變量聲明以下:
private final char value[]; private final int offset; private final int count; private int hash;
JDK6字符串內存佔用的計算方式:
首先計算一個空的 char 數組所佔空間,在 Java 裏數組也是對象,於是數組也有對象頭,故一個數組所佔的空間爲對象頭所佔的空間加上數組長度,即 8 + 4 = 12 字節 , 通過填充後爲 16 字節。
那麼一個空 String 所佔空間爲:
對象頭(8 字節)+ char 數組(16 字節)+ 3 個 int(3 × 4 = 12 字節)+1 個 char 數組的引用 (4 字節 ) = 40 字節。
所以一個實際的 String 所佔空間的計算公式以下:
8*( ( 8+12+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )
其中,n 爲字符串長度。
String 方法不少時候咱們移動客戶端經常使用於文本分析及大量字符串處理,
好比高頻率的拼接字符串,Log日誌輸出,會對內存性能形成一些影響。可能致使內存佔用太大甚至OOM。
頻繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,能夠在必定程度上避免OOM和內存抖動。
public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } char buf[] = new char[count + otherLen]; getChars(0, count, buf, 0); str.getChars(0, otherLen, buf, count); return new String(0, count + otherLen, buf); }
這是concat()的源碼,它看上去就是一個數字拷貝形式,咱們知道數組的處理速度是很是快的,可是因爲該方法最後是這樣的:return new String(0, count + otherLen, buf);這一樣也建立了10W個字符串對象,這是它變慢的根本緣由。
當調用 intern 方法時,若是池已經包含一個等於此 String 對象的字符串(該對象由 equals(Object) 方法肯定),則返回池中的字符串。不然,將此 String 對象添加到池中,而且返回此 String 對象的引用。
例如:
「abc」.intern()方法的返回值仍是字符串」abc」,表面上看起來好像這個方法沒什麼用處。但實際上,它作了個小動做:
檢查字符串池裏是否存在」abc」這麼一個字符串,若是存在,就返回池裏的字符串;若是不存在,該方法會把」abc」添加到字符串池中,而後再返回它的引用。
String s1 = new String("111"); String s2 = "sss111"; String s3 = "sss" + "111"; String s4 = "sss" + s1; System.out.println(s2 == s3); //true System.out.println(s2 == s4); //false System.out.println(s2 == s4.intern()); //true
過多得使用 intern()將致使 PermGen 過分增加而最後返回 OutOfMemoryError,由於垃圾收集器不會對被緩存的 String 作垃圾回收,因此若是使用不當會形成內存泄露。
更多詳細比較請查看這篇博文
http://blog.csdn.net/songylwq/article/details/9016609
在拼接動態字符串時,儘可能用 StringBuffer 或 StringBuilder的 append,這樣能夠減小構造過多的臨時 String 對象。可是如何正確的使用StringBuilder呢?
StringBuilder繼承AbstractStringBuilder,打開AbstractStringBuilder的源碼
/** * A modifiable {@link CharSequence sequence of characters} for use in creating * and modifying Strings. This class is intended as a base class for * {@link StringBuffer} and {@link StringBuilder}. * * @see StringBuffer * @see StringBuilder * @since 1.5 */ abstract class AbstractStringBuilder { static final int INITIAL_CAPACITY = 16; private char[] value; private int count; private boolean shared;
咱們能夠看到
StringBuilder的內部有一個char[], 不斷的append()就是不斷的往char[]裏填東西的過程。
new StringBuilder(),而且 時char[]的默認長度是16,
private void enlargeBuffer(int min) { int newCount = ((value.length >> 1) + value.length) + 2; char[] newData = new char[min > newCount ? min : newCount]; System.arraycopy(value, 0, newData, 0, count); value = newData; shared = false; }
而後若是StringBuilder的剩餘容量,沒法添加所有內容,若是要append第17個字符,怎麼辦?能夠看到enlargeBuffer函數,用System.arraycopy成倍複製擴容!致使內存的消耗,增長GC的壓力。
這要是在高頻率的回調或循環下,對內存和性能影響很是大,或者引起OOM。
同時StringBuilder的toString方法,也會形成char數組的浪費。
public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
咱們的優化方法是StringBuilder在append()的時候,不是直接往char[]裏塞東西,而是先拿一個String[]把它們都存起來,到了最後才把全部String的length加起來,構造一個合理長度的StringBuilder。
/** * 參考BigDecimal, 可重用的StringBuilder, 節約StringBuilder內部的char[] * * 參考下面的示例代碼將其保存爲ThreadLocal. * * <pre> * private static final ThreadLocal<StringBuilderHelper> threadLocalStringBuilderHolder = new ThreadLocal<StringBuilderHelper>() { * @Override * protected StringBuilderHelper initialValue() { * return new StringBuilderHelper(256); * } * }; * * StringBuilder sb = threadLocalStringBuilderHolder.get().resetAndGetStringBuilder(); * * </pre> */ public class StringBuilderHolder { private final StringBuilder sb; public StringBuilderHolder(int capacity) { sb = new StringBuilder(capacity); } /** * 重置StringBuilder內部的writerIndex, 而char[]保留不動. */ public StringBuilder resetAndGetStringBuilder() { sb.setLength(0); return sb; } }
這個作法來源於JDK裏的BigDecimal類
對於那些須要高頻率拼接打印Log的場景,封裝一個LogUtil,來控制日誌在真正須要輸出時候纔去作拼接。好比:
public void log(String msg ){ if (BuildConfig.DEBUG){ Log.e("TAG","Explicit concurrent mark sweep " + "GC freed 10477(686KB) AllocSpace objects, 0(0B) " + "LOS objects, 39% free, 9MB/15MB, paused 915us total 28.320ms"+msg); } }
String s1 = new String("s1") ; String s2 = new String("s1") ;
上面建立了幾個String對象?
答案:3個 ,編譯期Constant Pool中建立1個,運行期heap中建立2個.
String s1 = "s1"; String s2 = s1; s2 = "s2";
s1指向的對象中的字符串是什麼?
答案: 「s1」
關於String 性能優化,瞭解String 在 JVM 中的存儲結構,String 的 API 使用可能形成的性能問題以及解決方法,就總結到這。如有錯漏,歡迎補充。
參考文章
https://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/
更多Android 之美,請閱讀《Android 之美 從0到1 – 高手之路》系列文章
水平有限,如有錯漏,歡迎指正,批評,如需轉載,請註明出處–http://blog.csdn.net/vfush,謝謝!
做者:fuchenxuan
出處:http://blog.csdn.net/vfush
歡迎訪問個人我的站點:http://fuchenxuan.cn
轉載請註明出處–http://blog.csdn.net/vfush