Java那些坑(1):Java7的substring

問題描述

應用服務器從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上面,自用時間特別長。

wKioL1eYIcrzrth-AASbXFyQWEs588.png

這個函數自用時間很長,看了下代碼如下:

  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抽樣如下:

wKiom1eYI8STh80tAAEmZ3DvZ1Q181.png

自用時間特別長,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);
	}
    }
}

對應的抽樣如下:

wKiom1eYJEzzpj_lAAEfiPWZ6fE156.png

試了好多次都這樣。然後我看了下它們的字節碼:

使用while(true)的

wKioL1eYJNOD9MJaAADO8B5Nxpk574.png


使用for循環的

wKioL1eYJMLi6jpTAAEkwFwRjwk259.png

沒有什麼大區別,使用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[]數組。

wKioL1eYQ1nBQ87jAACgocrux90742.jpg

這樣能提高性能。但是有風險,就是內存浪費(有人稱之爲”內存泄漏“,但是我覺得不能算內存泄露,因爲這段內存是可以回收的),怎麼浪費的呢?

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);  
}

wKioL1eYRPyCbqFNAACYlMe_Rfk702.jpg

這樣就不會有內存浪費的問題了。但是性能肯定不如共享char[]數組,不過也不會太差。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章