目錄
問題描述
因爲項目的需要,封裝的SM4的加密、解密工具包,最近出了問題,客戶反饋說現場有一個15M大小錄音文件,在進行加密和解密的時候,方法沒有反應,調用超時,失敗了。按照最初封裝時的考慮,沒想過需要加密的入參字符串會有那麼大,所以也沒有考慮這種情況,今天拿到測試樣例數據之後,通過讀文件和寫文件的方式進行了驗證,最終發現並不是加密的算法有問題,也不是方法不能正常執行,而是整個過程中部分代碼對String的處理效率低造成的,當對String處理的循環超過100w長度時,需要幾個小時才能處理完。通過Debug找到耗時點之後,下面記錄下改造內容。
String類型
String類型到底能存儲多長的內容?這取決於你怎麼用。編譯階段和運行階段String內存可存儲的內容長度是不一樣的。參考這篇博文的描述:
編譯階段:
在我們使用字符串字面量直接定義String的時候,會把字符串在常量池中存儲一份。常量池中的每一項常量都是一個表,都有自己對應的類型。String類型,有一張固定長度的CONSTANT_String_info表用來存儲文字字符串值,注意:該表只存儲文字字符串值,不存儲符號引用。
JVM的常量池最多可放65535個項。第0項不用。最後一項最多隻能是65534(下標值)。而每一項中,若是放一個UTF-8的常量串,其長度最長是:65535個字節(不是字符)。
運行時階段:
String內部是以char數組的形式存儲,數組的長度是int類型,那麼String允許的最大長度就是Integer.MAX_VALUE了,2147483647;又由於java中的字符是以16位存儲的,因此大概需要4GB的內存才能存儲最大長度的字符串。
也就是說,如果你定義一個String類型的常量,它的長度最多不能超過65535個字節。這裏插播一下不同編碼下英文字符和中文對應字節的個數不同,參考這篇博文。我們以UTF-8編碼爲例,全英文字符的話,最大隻能定義65535長度的字符。
但是如果是在運行階段,給String類型的變量賦值,例如:我們從文件裏讀取文件的內容,將文件的內容賦值給String變量,這時候屬於運行階段,運行階段String可存儲的內容就長很多,理論上可以存儲4G大小的內容。
拼接超長字符串
先看下出現耗時的問題代碼,代碼如下:
public static String sm4Eecode(String strs) throws Exception{
int mm1[] = getInput(strs);
mm1 = encode(mm1);
String rstr="";
for(int i=0;i< mm1.length;i++){
rstr+ = StringUtils.leftPad(Integer.toHexString(mm1[i]), 8, '0');
}
return rstr;
}
mm1是加密後的int數組,這個加密過程很快,我們重點看下for循環,乍一看,這段代碼沒什麼問題,就是通過循環把數組裏的內容轉換成16進制的字符串,然後拼接到一起。如果mm1的長度超過100w,這塊for循環的代碼就會執行好幾個小時。問題出在哪裏呢?rstr的內容每次都會重新改變和調整,隨着字符串長度的變化,每次都要在內存中重新創建,這是極其耗時的。對於字符串的拼接,我們一般使用StringBuffer或者StringBuilder。StringBuffer和StringBuilder最大區別在於StringBuffer是線程安全的,但是效率比StringBuilder低一些。這裏不涉及多線程,改造後的代碼如下:
public static String sm4Eecode(String strs) throws Exception{
int mm1[] = getInput(strs);
mm1 = encode(mm1);
StringBuilder sb = new StringBuilder();
for(int i=0;i< mm1.length;i++){
String str = StringUtils.leftPad(Integer.toHexString(mm1[i]), 8, '0');
sb.append(str);
}
return sb.toString();
}
改造之後,處理速度有非常明顯的提升,100w循環基本上2秒之內就能處理完。
截取超長字符串
在對加密後內容進行解密的時候,涉及到字符串的截取,每次取8位。通過調試發現這塊也是影響處理性能耗時的點之一,先看下問題代碼,如下:
private static int[] getOutPut(String contents) {
int len = contents.length()/8;
int pv = contents.length()%8;
int rt[];
if(pv>0){
rt = new int[len+1];
}else{
rt = new int[len];
}
for(int i=0;i<len;i++){
String st = contents.substring(0,8);
BigInteger bi = new BigInteger(st, 16);
rt[i] = bi.intValue();
contents=contents.substring(8);
}
if(!"".equals(contents) && contents.length()>0){
BigInteger bi = new BigInteger(contents, 16);
rt[len] = bi.intValue();
}
return rt;
}
問題也是出在for循環的地方,這個地方是通過substring的方式,每次調整字符串的長度,每次去掉已處理開頭的8個字符,將剩餘的字符串重新賦值給contents。這裏遇到的問題和上面基本上是一樣,我一開始以爲是substring的性能問題,其實不是,主要還是因爲String的內容每次都在調整,每次都會重新申請內存空間,這是極其耗時的。這塊可以通過調整substring的being和end下標,持續從contents裏取值,而不改變content內容的方式來做,改造後代碼如下:
private static int[] getOutPut(String contents) {
int len = contents.length()/8;
int pv = contents.length()%8;
int rt[];
if(pv>0){
rt = new int[len+1];
}else{
rt = new int[len];
}
int ib=0;
int ie=8;
for(int i=0;i<len;i++){
String st = contents.substring(ib,ie);
BigInteger bi = new BigInteger(st, 16);
rt[i] = bi.intValue();
ib=ib+8;
ie=ie+8;
}
if(rt.length>len){
String str = contents.substring(ib);
BigInteger bi = new BigInteger(str, 16);
rt[len] = bi.intValue();
}
return rt;
}
改造後,執行效率提升明顯。
總結
Java中對String的操作,在涉及到循環或者賦值頻率比較高的時候,一定要注意,String類型的變量內容發生變化後,每次都會進行新的內存空間申請,如果過於頻繁,這會嚴重影響性能。