Java 项目优化实战 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
CodingNET
V2EX    Java

Java 项目优化实战

  •  
  •   CodingNET
    Coding 2016-04-05 16:37:28 +08:00 4351 次点击
    这是一个创建于 3531 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文作者: CODING 工程师 Tan He

    1 Visual VM

    项目中的某一个接口,在某一场景下(数据量大),性能让人难以忍受。

    那么如何有什么工具可以定位引发性能问题的代码呢?其实有很多,这里我们使用 Visual VM 。

    bg1

    Visual VM 是一款用来分析 Java 应用的图形工具,能够对 Java 应用程序做性能分析和调优。如果你使用的 java 7 或者 java 8 ,那么可以直接在 JDK 的 bin 目录找到该工具,名称为 jvisualvm 。当然也可以在官网上自行下载。

    使用 Visual VM 分析某个接口的性能的方法如下:

    bg1

    结果显示如下:

    bg1

    通过上图,我们可以看到比较耗时的方法为 resolveBytePosition 和 rest , getFile 和 currentUser 是网络请求,暂不考虑。

    2 优化一

    2.1 背景

    首先拿 resolveBytePosition 方法开刀。为了能更容易的解释 resolveBytePosition 的用途,举个例子。

    给定一个字符串 chars 与该字符串的 UTF-8 二进制数组(空格用来隔开字符数据,实际并不存在):

    chars = "just 一个 test"; bytes = "6A 75 73 74 E4B880 E4B8AA 74 65 73 74"; 

    resolveBytePosition 用来解决给定一个 bytes 的偏移 bytePos 计算 chars 中的偏移 charPos 的问题。比如:

    bytePos = 0 (6A) 对应 charPos = 0 (j) bytePos = 1 (75) 对应 charPos = 1 (u) 

    如果使用 array[start:] 表示从下标 start 开始截取数组元素至末尾组成的新数组,那么则有:

    bytes[bytePos:] = chars[charPos:] 

    举例:

    bytes[0:] = chars[0:] bytes[1:] = chars[1:] bytes[10:] = chars[6:] 

    2.2 原实现

    明白了 resolveBytePosition 的作用,看一下它的实现

    public int resolveBytePosition(byte[] bytes, int bytePos) { return new String(slice(bytes, 0, bytePos)).length(); } 

    该解法简单粗暴,能够准确的计算出结果,但是缺点显而易见,频繁的构建字符串,对性能造成了极大的影响。通过 Visual VM 可以证实我们的推论,通过点击快照,查看更详细的方法调用耗时。

    bg

    2.3 剖析

    为了更方便的剖析问题,我们绘制如下表格,用来展示每一个字符的 UTF-8 以及 Unicode 的二进制数据:

    || j | u | s |t | 一 |个 | t|e|s|t| |:--:| :--: | :--: |:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| |UTF-8| 6A | 75 |73|74|E4B880|E4B8AA|74|65|73|74| |Unicode| 6A | 75 |73|74|4E00|4E2A|74|65|73|74|

    接着我们将字节数据转换为字节长度:

    || j | u | s |t | 一 |个 | t|e|s|t| |:--:| :--: | :--: |:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| |UTF-8| 1 | 1 |1|1|3|3|1|1|1|1| |Unicode| 1 | 1 |1|1|2|2|1|1|1|1|

    Java 中的使用 char 来表示 Unicode , char 的长度为 2 个字节,因此一个 char 足以表示示例中的任何一个字符。

    我们使用一个单元格表示一个 byte ( UTF-8 )或一个 char ( Unicode ),并对单元格编号,得到下表:

    <table><thead><tr><th style="text-align: center"></th><th style="text-align: center">j</th><th style="text-align: center">u</th><th style="text-align: center">s</th><th style="text-align: center">t</th><th style="text-align: center" colspan="3">一</th><th style="text-align: center" colspan="3">个</th><th style="text-align: center">t</th><th style="text-align: center">e</th><th style="text-align: center">s</th><th style="text-align: center">t</th></tr></thead><tbody><tr><tr><td style="text-align: center">bytes</td><td style="text-align: center">0</td><td style="text-align: center">1</td><td style="text-align: center">2</td><td style="text-align: center">3</td><td style="text-align: center">4</td><td style="text-align: center">5</td><td style="text-align: center">6</td><td style="text-align: center">7</td><td style="text-align: center">8</td><td style="text-align: center">9</td><td style="text-align: center">10</td><td style="text-align: center">11</td><td style="text-align: center">12</td><td style="text-align: center">13</td></tr><td style="text-align: center">chars</td><td style="text-align: center">0</td><td style="text-align: center">1</td><td style="text-align: center">2</td><td style="text-align: center">3</td><td style="text-align: center" colspan="3">4</td><td style="text-align: center" colspan="3">5</td><td style="text-align: center">6</td><td style="text-align: center">7</td><td style="text-align: center">8</td><td style="text-align: center">9</td></tr></tbody></table>

    可以得出下面对应关系

    bytes[0:] = chars[0:] bytes[1:] = chars[1:] bytes[2:] = chars[2:] bytes[3:] = chars[3:] bytes[4:] = chars[4:] bytes[7:] = chars[5:] bytes[10:] = chars[6:] ... ... 

    2.3 方案

    进行到这一步,高效的算法已经呼之欲出了。算法如下:

    把字符 UTF-8 数据的二进制长度不为 1 的称为特征点。除特征点外,每个字符都是一个字节长度。记下所有特征点的对应关系,对于给定的 bytePos ,都可以根据公式计算得到 charPos 。

    公式为:

    charPos = bytePos - preBytePos + preCharPos 

    举例:

    则本实例中有两个特征点 ,记作:

    bytes[6:] = chars[4:] bytes[9:] = chars[5:] 

    如果给定 bytePos 10, 首先找到前一个特征点的对应关系 9 ( preBytePos ) -> 5 ( preCharPos), 根据公式得出 (10 - 9) + 5 = 6 。

    2.4 核心代码

    该算法还有一个比较关键的问题要解决,即高效的计算一个 char 的字节长度。计算 char 的字节长度的算法参考了 StackOverflow

    // 计算特征点 private int[][] calcSpecialPos(String str) { ArrayList<int[]> specialPos = new ArrayList<>() specialPos.add(new int[] {0, 0}); int lastCharPost = 0; int lastBytePos = 0; Charset utf8 = Charset.forName("UTF-8"); CharsetEncoder encoder = utf8.newEncoder(); CharBuffer input = CharBuffer.wrap(str.toCharArray()); ByteBuffer output = ByteBuffer.allocate(10); int limit = input.limit(); while(input.position() < limit) { output.clear(); input.mark(); input.limit(Math.min(input.position() + 2, input.capacity())); if (Character.isHighSurrogate(input.get()) && !Character.isLowSurrogate(input.get())) { //Malformed surrogate pair lastCharPost++; } input.limit(input.position()); input.reset(); encoder.encode(input, output, false); int encodedLen = output.position(); lastCharPost++; lastBytePos += encodedLen; if (encodedLen != 1) { // 特征点 specialPos.add(new int[]{lastBytePos, lastCharPost}); } } return toArray(specialPos); } // 根据特征点,计算 bytePos 对应的 charPos private int calcPos(int[][] specialPos, int bytePos) { // 如果只有一个元素 {0, 0),说明没有特征值 if (specialPos.length == 1) return bytePos; int pos = Arrays.binarySearch(specialPos, new int[] {bytePos, 0}, (int[] a, int[] b) -> Integer.compare(a[0], b[0])); if (pos >= 0) { return specialPos[pos][1]; } else { // if binary search not fonund, will return (-(insertion point) - 1), // so here -2 is mean -1 to get insertpoint and then -1 to get previous specialPos int[] preSpecialPos = specialPos[-pos-2]; return bytePos - preSpecialPos[0] + preSpecialPos[1]; } } 

    3 优化二

    3.1 背景

    接下来解决第二个函数 rest 。该函数的功能是得到 JsonArray ( gson ) 的除第一个元素外的所有元素。

    由于 rest 是在一个递归函数中被调用且递归栈很深,因此如果 rest 实现的不够高效,其影响会被成倍放大。

    3.2 原实现

    private JsonArray rest(JsonArray arr) { JsonArray result = new JsonArray(); if (arr.size() > 1) { for (int i = 1; i < arr.size(); i++) { result.add(arr.get(i)); } } return result; } 

    3.3 剖析

    通过调试发现 JsonArray 中存储了相当大的数据,对于频繁调用的场景,每次都对其重新构建明显不是一个明智的选择。 通过查看返回的 JsonArray 使用情况,我们得到了另一条线索:仅仅使用里面的数据,而不涉及修改。

    考虑到 JsonArray 被实现成 final ,最后方案确定为实现一个针对 rest 这种需求定制的代理类。

    3.4 方案 & 代码

    代理类 JsonArrayWrapper 分别对 firs 、 rest 、 foreach 等功能进行了实现。

    class JsonArrayWrapper implements Iterable<JsonElement> { private JsonArray jsonArray; private int mark; public JsonArrayWrapper() { this.jsOnArray= new JsonArray(); this.mark = 0; } public JsonArrayWrapper(JsonArray jsonArray) { this.jsOnArray= jsonArray; this.mark = 0; } public JsonArrayWrapper(JsonArray jsonArray, int mark) { this.jsOnArray= jsonArray; this.mark = mark; } public JsonObject first() { return jsonArray.get(mark).getAsJsonObject(); } public JsonArrayWrapper rest() { return new JsonArrayWrapper(jsonArray, mark+1); } public int size() { return jsonArray.size() - mark; } public JsonElement get(int n) { return jsonArray.get(mark + n); } public void add(JsonElement jsonElement) { jsonArray.add(jsonElement); } public void addAll(JsonArrayWrapper jsonArrayWrapper) { jsonArrayWrapper.forEach(this.jsonArray::add); } @Override public Iterator<JsonElement> iterator() { JsonArray jsOnarray= new JsonArray(); this.forEach(e -> jsonarray.add(e)); return jsonarray.iterator(); } @Override public void forEach(Consumer<? super JsonElement> action) { for (int i=mark; i<jsonArray.size(); i++) { action.accept(jsonArray.get(i)); } } } 

    4 成果

    经过这两个主要的优化,就解决了代码中的性能问题,成果如下图所示:

    bg2

    6 条回复    2016-04-06 19:42:19 +08:00
    fwrq41251
        1
    fwrq41251  
       2016-04-05 16:45:04 +08:00
    生产环境很少能用到图形化的工具吧..
    zacard
        2
    zacard  
       2016-04-05 16:46:26 +08:00
    不错,顶
    dhyanshi
        3
    dhyanshi  
       2016-04-05 17:25:27 +08:00
    incompatible
        4
    incompatible  
       2016-04-05 17:56:08 +08:00   1
    @fwrq41251 你用 ssh 连接服务器时使用的 ssh 客户端多半也是跑在图形界面下的吧?

    jdk 本身提供了 jps 、 jstat 、 jmap 等命令行的工具,也提供了 JConsole 、 JVisualVM 这些图形化工具。后者可以通过远程协议连接远程 jvm 的。
    hrong
        5
    hrong  
       2016-04-06 19:40:54 +08:00 via Android
    也可以把前者的断片截下来让后者进行图形分析 :-)
    root787 /td>
        6
    root787  
       2016-04-06 19:42:19 +08:00
    有个叫 jProfile 的工具,特别好用。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2632 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 12:44 PVG 20:44 LAX 04:44 JFK 07:44
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86