編輯:關於Android編程
關於String相關知識都是老掉牙的東西了,但我們經常可能在不經意的String 字符串拼接的情況下浪費內存,影響性能,也常常會成為觸發內存OOM的最後一步。所以本文對String字符串進行深度解析,有助於我們日常開發中提高程序的性能,解決因String 而導致的性能問題。
首先我們先回顧一下String類型的本質
先看一下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. * @see StringBuffer * @see StringBuilder * @see Charset * @since 1.0 */ public final class String implements Serializable, Comparable, 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.”。
這句話總結歸納了String的一個最重要的特點:
String是值不可變(immutable)的常量,是線程安全的(can be shared)。
接下來,String類使用了final修飾符,表明了String類的第二個特點:String類是不可繼承的。
String類表示字符串。java程序中的所有字符串,如“ABC”,是實現這個類的實例
字符串是常量,它們的值不能被創建後改變。支持可變字符串字符串緩沖區。因為字符串對象是不可改變的,所以它們可以被共享。例如:
String str = "abc";
相當於
String s = new String("abc");
這裡實際上創建了兩個String對象,一個是”abc”對象,存儲在常量空間中,一個是使用new關鍵字為對象s申請的空間,存儲引用地址。
在執行到雙引號包含字符串的語句時,JVM會先到常量池裡查找,如果有的話返回常量池裡的這個實例的引用,否則的話創建一個新實例並置入常量池裡,如上面所示,str 和 s 指向同一個引用.
String的定義方法歸納起來總共為以下四種方式:
直接使用”“引號創建; 使用new String()創建; 使用new String(“abcd”)創建以及其他的一些重載構造函數創建; 使用重載的字符串連接操作符+創建。在討論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 )保存到內部字符串常量列表中。如下圖所示:
vc/gzay1xNfWt/vQ8sHQo6zIu7rzt7W72LbU06a1xFN0cmluZ7bUz/O1xNL908Oho86su6TV4rj2xNqyv8HQse21xLnYvPzKx8jOus7M2LaotcTX1rf70PLB0NTa1eK49sHQse3Jz9a7s/bP1tK7tM6hozwvcD4NCjxwPsD9yOejrFN0cmluZyBzMiA9ICZsZHF1bztteVN0cmluZyZyZHF1bzujrNTL0NDKsXMyu+G008Tasr/X1rf7tK6zo8G/wdCx7cTatcO1vXMxtcS3tbvY1rWjrMv50tRzMrrNczG2vNa4z/LNrNK7uPZTdHJpbme21M/zoaO1q8rHU3RyaW5nttTP83PU2rbRwO+1xNK7uPayu82szrvWw6Osy/nS1LrNczGyu8/gzayhozwvcD4NCjxwPkpBVkHW0LXE19a3+7Sus6PBv7/J0tTX986qU3RyaW5nttTP88q508OjrNfWt/u0rrOjwb+1xNfWt/vQ8sHQsb7J7crHtOa3xdTas6PBv7PY1tCjrNTa19a3+7SuxNqyv8HQse3W0MO/uPbX1rf7tK6zo8G/tcTX1rf70PLB0LbU06bSu7j2U3RyaW5nttTP86OsyrW8ysq508O1xL7NysfV4rj2ttTP86GjPC9wPg0KPHA+1eK49sS/x7DN+MnPsvvK9rXE1+624LnY09rV4rj2U3RyaW5nttTP87rN19a3+7Sus6PBv7XEudjPtaOszfjJz7j309DLtbeoPC9wPg0KPGgyIGlkPQ=="string-在-jvm-的存儲結構">String 在 JVM 的存儲結構
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 做垃圾回收,所以如果使用不當會造成內存洩露。
在拼接動態字符串時,盡量用 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. * *
* private static final ThreadLocal*/ 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; } }threadLocalStringBuilderHolder = new ThreadLocal () { * @Override * protected StringBuilderHelper initialValue() { * return new StringBuilderHelper(256); * } * }; * * StringBuilder sb = threadLocalStringBuilderHolder.get().resetAndGetStringBuilder(); * *
這個做法來源於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 使用可能造成的性能問題以及解決方法,就總結到這。若有錯漏,歡迎補充。
谷歌在推出Android5.0的同時推出了全新的設計Material Design,谷歌為了給我們提供更加規范的MD設計風格的控件,在2015年IO大會上推出了Desig
先看下運行效果: 程序結構:MainActivity文件中代碼:復制代碼 代碼如下:package com.android.buttonpagefl
(轉載請注明出處:http://blog.csdn.net/buptgshengod) 1.背景 在android源碼中我們能看到各種以@開頭的字符,他們大
通常來說,在進行Android項目開發的時候可以通過MediaRecorder和AudioRecord這兩個工具來實現錄音的功能,MediaRecorder直接把麥克風的