Java 项目优化实战
1 Visual VM
项目中的某一个接口,在某一场景下(数据量大),性能让人难以忍受。
那么如何有什么工具可以定位引发性能问题的代码呢?其实有很多,这里我们使用 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.4 方案
进行到这一步,高效的算法已经呼之欲出了。算法如下:
把字符 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.5 核心代码
该算法还有一个比较关键的问题要解决,即高效的计算一个 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 分别对 first、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 成果
经过这两个主要的优化,就解决了代码中的性能问题,成果如下图所示:
Java 项目优化实战的更多相关文章
- 【C#】项目优化实战
[C#]项目优化实战 一. 数据库设计 1. 常量的枚举值直接存中文不要存数字(注意是常量,如果显示值可变就不能) 例如:男女,在数据库中不要存1和0,直接存男和女. 这样的好处:读取数据的时候可以避 ...
- Java 性能优化实战记录(3)--JVM OOM的分析和原因追查
前言: C/C++的程序员渴望Java的自由, Java程序员期许C/C++的约束. 其实那里都是围城, 外面的人想进来, 里面的人想出去. 背景: 作为Java程序员, 除了享受垃圾回收机制带来的便 ...
- Java 性能优化实战记录(2)---句柄泄漏和监控
前言: Java不存在内存泄漏, 但存在过期引用以及资源泄漏. (个人看法, 请大牛指正) 这边对文件句柄泄漏的场景进行下模拟, 并对此做下简单的分析.如下代码为模拟一个服务进程, 忽略了句柄关闭, ...
- Java 性能优化实战记录(1)---定位并分析耗cpu最多的线程
1) jps 列出相关的java进程, 以及对应的pid 也可以使用如下命令来尝试 ps aux | grep java --color 2) top -Hp <pid> ...
- Github上可以涨薪30k的Java教程和实战项目终于可以免费下载了
写在前面 大家都知道 Github 是一个程序员福地,这里有各种厉害的开源框架.软件或者教程.这些东西对于我们学习和进步有着莫大的进步,所以我有了这个将 Github 上非常棒的 Java 开源项目整 ...
- java web项目优化记录:优化考试系统
考试系统在进行压力測试时发现,并发量高之后出现了button无反应.试题答案不能写到数据库的问题,于是针对这些核心问题,进行了优化. 数据库方面: Select语句:Select * from TEB ...
- Java秒杀系统实战系列~构建SpringBoot多模块项目
摘要:本篇博文是“Java秒杀系统实战系列文章”的第二篇,主要分享介绍如何采用IDEA,基于SpringBoot+SpringMVC+Mybatis+分布式中间件构建一个多模块的项目,即“秒杀系统”! ...
- Java生鲜电商平台-SpringCloud微服务架构高并发参数优化实战
Java生鲜电商平台-SpringCloud微服务架构高并发参数优化实战 一.写在前面 在Java生鲜电商平台平台中相信不少朋友都在自己公司使用Spring Cloud框架来构建微服务架构,毕竟现在这 ...
- Java最新学习线路(基础,源码,项目,实战)
如需获取以下学习资源请关注公众号:Java编程指南 我们为自学者编程的或初学java的小伙伴们准备了一整套完整的学习资源和文章,还有我自己在自学路上的一些总结和学习线路,希望能帮到小伙伴们,如果有什么 ...
随机推荐
- 移动端Viewport & 使用rem来开发移动端网站
Viewport大神 无双 的精彩解释 具体参数各型号是否支持参见: http://www.cnblogs.com/2050/p/3877280.html#commentform 摘录: 移动设备上的 ...
- java学习第16天(补充可变参数)
如果我们在写方法的时候,参数个数不明确,就应该定义可变参数. 格式: 修饰符 返回值类型 方法名(数据类型... 变量) {} 注意: A:该变量其实是一个数组名 B:如果一个方法有多个参数,并且有可 ...
- java二
一,面向对象 面向对象,似乎是太抽象了点,没人敢拍着胸脯说我面向对象学到了100%,纵然如此,了解面向对象的思想对于学好java等面向对象编程语言有着莫大的好处,因为一通百通,同样是面向对象,等你精通 ...
- [Nginx] 在Linux下的启动、停止和重加载
Nginx的启动 /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf 其中-c参数指定配置文件路径. Nginx的停止 ...
- DotNet Core 1.0 集成 CentOS 开发与运行环境部署
一. DotNet Core 1.0 开发环境部署 操作系统安装 我们使用CentOS 7.2.1511版本. 安装libunwind库 执行:sudo yum install libunwi ...
- vim 使用2 转载 为了打开方便
http://coolshell.cn/articles/5426.html vim的学习曲线相当的大(参看各种文本编辑器的学习曲线),所以,如果你一开始看到的是一大堆VIM的命令分类,你一定会对这个 ...
- linux 学习 软件工具
_____secureCRT 远程通信工具 第二行第二个快速连接,输入主机名,即ip地址,和用户名,root .其它默认:端口22,协议ssh2,防火墙 none. 保存会话,连接.需要输入用户密码. ...
- MediaBrowserService 音乐播放项目
MediaBrowserService 音乐播放项目,本项目主要有如下功能: (1):支持播放在线音乐 (2):按住home键退出页面后显示通知栏部分播放提示, (3) : 支持切换上下首歌曲 本项 ...
- Servlet学习二——doGet和doPost
1.get和post是http协议中的两种方法,还有其它,读写一般数据还能满足: 2.get只有一个流,参数附加在url后,且大小个数有严格限制,这个限制因浏览器而有所不同,get传递数据,实际上是将 ...
- google垂直搜索结果
佰年金融 所谓“垂直”很干净地分离出来,而不是在谷歌算作有机列表的结果,但该行已开始模糊.例如,现在许多视频结果似乎是直接整合为有机(万SERP的例子).我治疗的新的“有深度的文章”作为一个垂直的结果 ...