1. 程式人生 > >拾人牙慧 StringBuilder.toString()的問題

拾人牙慧 StringBuilder.toString()的問題

原文地址:http://www.blogjava.net/xylz/archive/2012/03/16/371966.html#372029

線上伺服器負載過高發生了報警,同事找我求救。
我看到機器的負載都超過20了,檢視java程序執行緒棧,找到了出問題的程式碼。

下面是其程式碼片段,實際情況錯誤處理比這更壞。
 1 package demo;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.InputStream;
 5 import java.io.InputStreamReader;
 6 import java.net.HttpURLConnection;
 7
 import java.net.URL;
 8 import java.net.URLConnection;
 9 import org.apache.commons.lang.StringUtils;
10 
11 /**12  * @author adyliu (imxylz#gmail.com)
13  * @since 2012-3-15
14 */
15 public class FaultDemo {
16 
17     /**18      * @param args
19 */
20     public static void main(String[] args) throws Exception {
21
         final String tudou = "http://v.youku.com/v_playlist/f17170661o1p9.html";
22 
23         URL url = new URL(tudou);
24         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
25         conn.connect();
26         try {
27             InputStream in = conn.getInputStream();
28             BufferedReader br = new
 BufferedReader(new InputStreamReader(in, "utf-8"));
29             StringBuilder buf = new StringBuilder();
30             String line = null;
31             while ((line = br.readLine()) != null) {
32                 if (StringUtils.isNotEmpty(buf.toString())) {
33                     buf.append("\r\n");
34                 }
35                 buf.append(line);
36             }
37             //do something with 'buf'38 
39         } finally {
40             conn.disconnect();
41         }
42 
43     }
44 
45 }
46 
思考下,這段程式碼有什麼致命問題麼?(這裡不追究業務邏輯處理的正確性以及細小的瑕疵)
.
..
...
現在回來。
我發現執行緒棧裡面的執行緒都RUNNABLE在32行。
這一行看起來有什麼問題呢?StringBuilder.toString()不是轉換成String麼?Apache commons-lang裡面的StringUtils.isNotEmpty使用也沒問題啊?
看程式碼,人家的邏輯其實是判斷是否是第一行,如果不是第一行那麼就增加一個換行符。

既然CPU在這裡執行,那麼就說明這個地方一定存在非常耗費CPU的操作,導致CPU非常繁忙,從而系統負載過高。
看詳細堆疊,其實CPU在進行記憶體的拷貝動作。
看下面的原始碼。
java.lang.StringBuilder.toString()
    public String toString() {
        // Create a copy, don't share the array    return new String(value, 0, count);
    } 接著看java.lang.String的建構函式:
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        // Note: offset or count might be near -1>>>1.        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.offset = 0;
        this.count = count;
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
看出來了麼?
問題的關鍵在於String建構函式的最後一行,value並不是直接指向的,而是重新生成了一個新的字串,使用系統拷貝函式進行記憶體複製。
java.util.Arrays.copyOfRange(char[], int, int)     public static char[] copyOfRange(char[] original, int from, int to) {
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);
        char[] copy = new char[newLength];
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    }
好了,再回頭看邏輯程式碼32行。
if (StringUtils.isNotEmpty(buf.toString())) {
    buf.append("\r\n");
} 這裡有問題的地方在於每次迴圈一行的時候都生成一個新的字串。也就是說如果HTTP返回的結果輸入流中有1000行的話,將額外生成1000個字串(不算StringBuilder擴容生成的個數)。每一個字串還比前一個字串大。


我們來做一個簡單的測試,我們在原來的程式碼上增加幾行計數程式碼。
    int lines =0;
    int count = 0;
    int malloc = 0;
    while ((line = br.readLine()) != null) {
        lines++;
        count+=line.length();
        malloc += count;
        if (StringUtils.isNotEmpty(buf.toString())) {
            buf.append("\r\n");
        }
        buf.append(line);
    }
    System.out.println(lines+" -> "+count+" -> "+malloc); 我們記錄下行數lines以及額外發生的字串拷貝大小malloc。
這是一次輸出的結果。
1169 -> 66958 -> 39356387 也就是1169行的網頁,一共是66958位元組(65KB),結果額外生成的記憶體大小(不算StringBuilder擴容佔用的記憶體大小)為39356387位元組(37.5MB)!!!
試想一下,CPU一直頻繁於進行記憶體分配,機器的負載能不高麼?我們線上伺服器是2個CPU 16核,記憶體24G的Redhat Enterprise Linux 5.5,負載居然達到幾十。這還是隻有訪問量很低的時候。這就難怪服務頻繁宕機了。

事實上我們有非常完善和豐富的基於Apache commons-httpclient的封裝,操作起來也非常簡單。對於這種簡單的請求,只需要一條命令就解決了。
String platform.utils.HttpClientUtils.getResponse(String)
String platform.utils.HttpClientUtils.postResponse(String, Map<String, String>)
即使非要自造輪子,處理這種簡單的輸入流可以使用下面的程式碼,就可以很好的解決問題。
    InputStream in = 
    ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
    int len = -1;
    byte[] b = new byte[8192];//8k    while ((len = in.read(b)) > 0) {
        baos.write(b, 0, len);
    }
    baos.close();//ignore is ok    String response =  new String(baos.toByteArray(), encoding);
當然了,最後緊急處理線上問題最快的方式就是將有問題的程式碼稍微變通下即可。
    if (buf.length() > 0) {
        buf.append("\r\n");
    }