摘要:是一款用来分析应用的图形工具,能够对应用程序做性能分析和调优。计算特征点特征点根据特征点,计算对应的如果只有一个元素,说明没有特征值优化二背景接下来解决第二个函数。成果经过这两个主要的优化,就解决了代码中的性能问题,成果如下图所示
1 Visual VM本文作者:CODING 工程师 Tan He
项目中的某一个接口,在某一场景下(数据量大),性能让人难以忍受。
那么如何有什么工具可以定位引发性能问题的代码呢?其实有很多,这里我们使用 Visual VM。
Visual VM 是一款用来分析 Java 应用的图形工具,能够对 Java 应用程序做性能分析和调优。如果你使用的 java 7 或者 java 8,那么可以直接在 JDK 的 bin 目录找到该工具,名称为 jvisualvm。当然也可以在官网上自行下载。
使用 Visual VM 分析某个接口的性能的方法如下:
结果显示如下:
通过上图,我们可以看到比较耗时的方法为 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 可以证实我们的推论,通过点击快照,查看更详细的方法调用耗时。
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),并对单元格编号,得到下表:
j | u | s | t | 一 | 个 | t | e | s | t | |||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
bytes | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | chars | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
可以得出下面对应关系
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) { ArrayList3 优化二 3.1 背景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]; } }
接下来解决第二个函数 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 分别对 first、rest、foreach 等功能进行了实现。
class JsonArrayWrapper implements Iterable{ 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 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 4 成果 经过这两个主要的优化,就解决了代码中的性能问题,成果如下图所示:
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/65717.html
摘要:资源获取方式根据下面的索引,大家可以选择自己需要的资源,然后在松哥公众号牧码小子后台回复对应的口令,就可以获取到资源的百度云盘下载地址。公众号二维码如下另外本文会定期更新,松哥有新资源的时候会及时分享给大家,欢迎各位小伙伴保持关注。 没有一条路是容易的,特别是转行计算机这条路。 松哥接触过很多转行做开发的小伙伴,我了解到很多转行人的不容易,记得松哥大二时刚刚决定转行计算机,完全不知道这...
摘要:昨天有个小学弟给我发来微信,说他现在有点后悔选择开发了,月月光不说,还加班特别严重,平时也没有属于自己的时间去学习,问我刚毕业的时候是不是这样。每天回到出租屋都是倒头就睡,非常累,也没有其他时间提升自己的技术。 昨天有个小学弟给我发来微信,说他现在有点后悔选择Android开发了,月月光不说...
摘要:注册流程是从小程序简称,以下替代获取用户的,给到服务器,服务器会用还有自己的等信息一起去微信服务器请求用户数据,注意每一个所对应的用户都是不一样的。 本博客 猫叔的博客,转载请申明出处阅读本文约 5分钟适读人群:Java后端、Java初级、小程序前端 前后端项目的地址 ShareBookServer ShareBookClient 小程序前端 showImg(https://seg...
阅读 2772·2021-10-14 09:42
阅读 826·2021-10-11 10:57
阅读 773·2019-08-30 15:54
阅读 1913·2019-08-30 13:50
阅读 1684·2019-08-30 11:19
阅读 932·2019-08-29 12:38
阅读 1424·2019-08-26 11:51
阅读 1388·2019-08-26 10:48