問題描述
應用服務器從Weblogic 10升級到了Weblogic 12,對應的jdk也從1.6升級到了1.7,結果原來的一個sql執行時間從1s變成了25s。
這個sql是使用Mybatis進行的動態拼裝,如下,省略了一些字段。
<insert id="insertBatchMap" parameterType="Map"> INSERT INTO BTT_LOG ( ID, DATATIME, CHANNEL, ORGAN_NO, USER_ID, ) VALUES <foreach collection="list" separator="," index="i" item="ls"> ( #{ls[ID],jdbcType=BIGINT}, #{ls[DATATIME],jdbcType=TIMESTAMP}, #{ls[CHANNEL],jdbcType=VARCHAR}, #{ls[ORGAN_NO],jdbcType=VARCHAR}, #{ls[USER_ID],jdbcType=VARCHAR}, ) </foreach> </insert>
傳入一個Map,裏面是個List。最終生成的sql是下面這樣的:
insert into BTT_LOG(ID,DATATIME,CHANNEL,ORGAN_NO,USER_ID) values (‘1’,’2016-07-22’,’0’,’000001’) (‘2’,’2016-07-23’,’0’,’000002’) ...... ......
這個sql的作用是將當前庫的數據遷移到歷史庫,一條sql遷移1000條,每天大約2000萬的數據,速度一下子慢了25倍,數據都導不完了。
問題分析
看數據庫中sql的執行時間,也就用了1s左右,因此肯定不是數據庫的問題。通過VisualVM查看CPU熱點,發現熱點在org.apache.ibatis.parsing.GenericTokenParser.parse上面,自用時間特別長。
這個函數自用時間很長,看了下代碼如下:
public String parse1(String text) { StringBuilder builder = new StringBuilder(); if (text != null) { String after = text; int start = after.indexOf(openToken); int end = after.indexOf(closeToken); while (start > -1) { if (end > start) { String before = after.substring(0, start); String content = after.substring(start + openToken.length(), end); String substitution; // check if variable has to be skipped if (start > 0 && text.charAt(start - 1) == '\\') { before = before.substring(0, before.length() - 1); substitution = new StringBuilder(openToken).append(content).append(closeToken).toString(); } else { substitution = handler.handleToken(content); } builder.append(before); builder.append(substitution); after = after.substring(end + closeToken.length()); } else if (end > -1) { String before = after.substring(0, end); builder.append(before); builder.append(closeToken); after = after.substring(end + closeToken.length()); } else { break; } start = after.indexOf(openToken); end = after.indexOf(closeToken); } builder.append(after); } return builder.toString(); }
感覺自用時間不應該那麼長,大部分都是調用其他函數的,所以我懷疑這個自用時間是有問題的,可能包含了substring的時間。
我寫了下面的代碼做實驗:
public static void test() { while(true){ for(int i = 0; i < 100000000; i++) { String xxxx = "jfkfsdfjskfjsdkfjsakjdkfjdskfjslfjslkfjsdkfjsdlfjsd".substring(2); } } }
這段代碼對應的CPU抽樣如下:
自用時間特別長,substring的時間很短。
然後我改了一下,把while(true)改成for(int i=0;i<100;i++),結果substring的時間很長。
public static void test() { for(int j=0; j < 100; j++){ for(int i = 0; i < 100000000; i++) { String xxxx = "jfkfsdfjskfjsdkfjsakjdkfjdskfjslfjslkfjsdkfjsdlfjsd".substring(2); } } }
對應的抽樣如下:
試了好多次都這樣。然後我看了下它們的字節碼:
使用while(true)的
使用for循環的
沒有什麼大區別,使用for循環時由於還需要做加法,判斷是否小於100,所以自用的指令應該更多。substring方法的字節碼遠遠多於這幾條字節碼,所以我覺得上面兩個函數的時間都耗在substring上了。
我們用的MyBatis版本是3.1.1,我對比了下3.2.0和3.1.1的代碼,發現瞭如下問題:
原來的代碼:
String after = text; before = after.substring(0,start); builder.append(before);
變成了下面這樣:
char[] src = text.toCharArray(); builder.append(src, offset, start - 1)
把所有原來的substring方法全部替換成了使用char數組加上起止位置。所以說substring一定有問題,在新版的MyBatis裏不用了。
爲什麼我們的應用這個問題會特別明顯,因爲每條sql 1000行,每行100個字符的話就是10萬個字符,parse這個函數需要對這個10萬字符的字符串做上萬次substring,所以出問題了。升級了新版的MyBatis,這個問題解決了。
那麼substring到底有啥問題呢?其實是substring在java7裏面做了修改,在java6裏面使用substring,和使用char數組加起止位置是一樣的,但是在java7裏就不一樣了。
String類主要有三個成員:
private final char value[]; private final int offset; private final int count;
在1.6裏面,substring以及它調用的構造函數如下:
// JDK6,共享 value數組提升速度 String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; } public String substring(int beginIndex, int endIndex) { // 如果範圍和自己一模一樣,則返回自身,否則用value字符數組構造一個新的對象 return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); }
使用substring產生的新對象,和原來的對象共用一個char[]數組。
這樣能提高性能。但是有風險,就是內存浪費(有人稱之爲”內存泄漏“,但是我覺得不能算內存泄露,因爲這段內存是可以回收的),怎麼浪費的呢?
String x = "一個非常非常長的字符,可能是從網上抓取的一段文字"; String y = x.substring(a,b); //比較短 x不用了,被回收 y的使用時間很長,短時間內不會被回收
問題就來了,原來x會有一個很長的char[]數組,使用substring後這個數組被y引用了,所以回收x的時候這個數組不會被回收,因爲被y引用着,但是y只會用其中很短的一部分,造成了內存的浪費。
在1.7中做了修改,構造函數變成下面這樣,每次都把自己的數組拷貝一份出來。
public String(char value[], int offset, int count) { this.offset = 0; this.count = count; this.value = Arrays.copyOfRange(value, offset, offset+count); }
這樣就不會有內存浪費的問題了。但是性能肯定不如共享char[]數組,不過也不會太差。