Linkedin工程师是如何优化他们的Java代码的
http://greenrobot.me/devpost/java-faster-less-jvm-garbage/
Linkedin工程师是如何优化他们的Java代码的
最近在刷各大公司的技术博客的时候,我在Linkedin的技术博客上面发现了一篇很不错博文。这篇博文介绍了Linkedin信息流中间层Feed Mixer,它为Linkedin的Web主页,大学主页,公司主页以及客户端等多个分发渠道提供支撑(如下图所示)。

在Feed Mixer里面用到了一个叫做SPR(念“super”)的库。博文讲的就是如何优化SPR的java代码。下面就是他们总结的优化经验。
1. 谨慎对待Java的循环遍历
Java中的列表遍历可比它看起来要麻烦多了。就以下面两段代码为例:
- A:
1234private final List<Bar> _bars;for(Bar bar : _bars) {//Do important stuff}
- B:
12345private final List<Bar> _bars;for(int i = 0; i < _bars.size(); i++) {Bar bar = _bars.get(i);//Do important stuff}
代码A执行的时候 会为这个抽象列表创建一个迭代器,而代码B就直接使用 get(i) 来获取元素,相对于代码A省去了迭代器的开销。
实际上这里还是需要一些权衡的。代码A使用了迭代器,保证了在获取元素的时候的时间复杂度是 O(1) (使用了 getNext() 和 hasNext() 方法),最终的时间复杂度为 O(n) 。但是对于代码B,循环里每次在调用 _bars.get(i) 的时候花费的时间复杂度为 O(n) (假设这个list为一个 LinkedList),那么最终代码B整个循环的时间复杂度就是 O(n^2) (但如果代码B里面的list是 ArrayList, 那 get(i) 方法的时间复杂度就是 O(1)了)。所以在决定使用哪一种遍历的方式的时候,我们需要考虑列表的底层实现,列表的平均长度以及所使用的内存。最后因为我们需要优化内存,再加上 ArrayList 在大多数情况下查找的时间复杂度为 O(1) ,最后决定选择代码B所使用的方法。
2.在初始化的时候预估集合的大小
从Java的这篇 文档我们可以了解到: “一个HashMap 实例有两个影响它性能的因素:初始大小和加载因子(load factor)。 […] 当哈希表的大小达到初始大小和加载因子的乘积的时候,哈希表会进行 rehash操作 […] 如果在一个HashMap 实例里面要存储多个映射关系时,我们需要设置足够大的初始化大小以便更有效地存储映射关系而不是让哈希表自动增长让后rehash,造成性能瓶颈。”
在Linkedin实践的时候,常常碰到需要遍历一个 ArrayList 并将这些元素保存到 HashMap 里面去。将这个 HashMap 初始化预期的大小可以避免再次哈希所带来的开销。初始化大小可以设置为输入的数组大小除以默认加载因子的结果值(这里取0.7):
- 优化前的代码:
123456789HashMap<String,Foo> _map;void addObjects(List<Foo> input){_map = new HashMap<String, Foo>();for(Foo f: input){_map.put(f.getId(), f);}}
- 优化后的代码
123456789HashMap<String,Foo> _map;void addObjects(List<Foo> input){_map = new HashMap<String, Foo>((int)Math.ceil(input.size() / 0.7));for(Foo f: input){_map.put(f.getId(), f);}}
3. 延迟表达式的计算
在Java中,所有的方法参数会在方法调用之前,只要有方法参数是一个表达式的都会先这个表达式进行计算(从左到右)。这个规则会导致一些不必要的操作。考虑到下面一个场景:使用ComparisonChain比较两个 Foo 对象。使用这样的比较链条的一个好处就是在比较的过程中只要一个 compareTo 方法返回了一个非零值整个比较就结束了,避免了许多无谓的比较。例如现在这个场景中的要比较的对象最先考虑他们的score, 然后是 position, 最后就是 _bar 这个属性了:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Foo {
private float _score;
private int _position;
private Bar _bar;
public int compareTo (Foo other) {
return ComparisonChain.start().
compare(_score, other.getScore()).
compare(_position, other.getPosition()).
compare(_bar.toString(), other.getBar().toString()).
result;
}
}
|
但是上面这种实现方式总是会先生成两个 String 对象来保存bar.toString() 和other.getBar().toString() 的值,即使这两个字符串的比较可能不需要。避免这样的开销,可以为Bar 对象实现一个 comparator:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class Foo {
private float _score;
private int _position;
private Bar _bar;
private final BarComparator BAR_COMPARATOR = new BarComparator();
public int compareTo (Foo other) {
return ComparisonChain.start().
compare(_score, other.getScore()).
compare(_position, other.getPosition()).
compare(_bar, other.getBar(), BAR_COMPARATOR).
result();
}
private static class BarComparator implements Comparator<Bar> {
@Override
public int compare(Bar a, Bar b) {
return a.toString().compareTo(b.toString());
}
}
}
|
4. 提前编译正则表达式
字符串的操作在Java中算是开销比较大的操作。还好Java提供了一些工具让正则表达式尽可能地高效。动态的正则表达式在实践中比较少见。在接下来要举的例子中,每次调用 String.replaceAll() 都包含了一个常量模式应用到输入值中去。因此我们预先编译这个模式可以节省CPU和内存的开销。
- 优化前:
123private String transform(String term) {return outputTerm = term.replaceAll(_regex, _replacement);}
- 优化后:
1234private final Pattern _pattern = Pattern.compile(_regex);private String transform(String term) {String outputTerm = _pattern.matcher(term).replaceAll(_replacement);}
5. 尽可能地缓存Cache it if you can
将结果保存在缓存里也是一个避免过多开销的方法。但缓存只适用于在相同数据集撒花姑娘吗的相同数据操作(比如对一些配置的预处理或者一些字符串处理)。现在已经有多种LRU(Least Recently Used )缓存算法实现,但是Linkedin使用的是 Guava cache (具体原因见这里) 大致代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
private final int MAX_ENTRIES = 1000;
private final LoadingCache<String, String> _cache;
// Initializing the cache
_cache = CacheBuilder.newBuilder().maximumSize(MAX_ENTRIES).build(new CacheLoader<String,String>() {
@Override
public String load(String key) throws Exception {
return expensiveOperationOn(key);
}
}
);
//Using the cache
String output = _cache.getUnchecked(input);
|
6. String的intern方法有用,但是也有危险
String 的 intern 特性有时候可以代替缓存来使用。
从这篇文档,我们可以知道:
“A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned”.
这个特性跟缓存很类似,但有一个限制,你不能设置最多可容纳的元素数目。因此,如果这些intern的字符串没有限制(比如字符串代表着一些唯一的id),那么它会让内存占用飞速增长。Linkedin曾经在这上面栽过跟头——当时是对一些键值使用intern方法,线下模拟的时候一切正常,但一旦部署上线,系统的内存占用一下就升上去了(因为大量唯一的字符串被intern了)。所以最后Linkedin选择使用 LRU 缓存,这样可以限制最大元素数目。
最终结果
SPR的内存占用减少了75%,进而将feed-mixer的内存占用减少了 50% (如下图所示)。这些优化减少了对象的生成,进而减少了GC得频率,整个服务的延迟就减少了25%。
注:本文翻译自Linkedin 技术博客。
Linkedin工程师是如何优化他们的Java代码的的更多相关文章
- Linkedin工程师是如何优化他们的Java代码的(转)
英文原文:LinkedIn Feed: Faster with Less JVM Garbage 最近在刷各大公司的技术博客的时候,我在Linkedin的技术博客上面发现了一篇很不错博文.这篇博文介绍 ...
- Java 性能优化手册 — 提高 Java 代码性能的各种技巧
转载: Java 性能优化手册 - 提高 Java 代码性能的各种技巧 Java 6,7,8 中的 String.intern - 字符串池 这篇文章将要讨论 Java 6 中是如何实现 String ...
- 优化你的java代码性能
一.避免在循环条件中使用复杂表达式 在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快. 例子:import java.util. ...
- Java性能优化之高性能JAVA代码的若干个习惯
创建对象: 1.避免在循环体中创建对象,循环前应该创建对象,避免浪费更多内存空间和增加GC负担 这种情况在我们的实际应用中经常遇到,而且我们很容易犯类似的错误,例如下面的代码: for (int i ...
- ym——优化你的Java代码(新)
转载请注明本文出自Cym的博客(http://blog.csdn.net/cym492224103),谢谢支持! 1.面向对象的3要素. 2.面向对象开发的6大原则. 1.单一职责原则 应该有且仅有一 ...
- [大牛翻译系列]Hadoop(15)MapReduce 性能调优:优化MapReduce的用户JAVA代码
6.4.5 优化MapReduce用户JAVA代码 MapReduce执行代码的方式和普通JAVA应用不同.这是由于MapReduce框架为了能够高效地处理海量数据,需要成百万次调用map和reduc ...
- java代码实现队列的优化
package com.voole.queun; /** * @Decription 队列 * @author TMAC-J * */ public class Queun { /** * 初始化队列 ...
- java代码之美(11)---java代码的优化
java代码的优化 随着自己做开发时间的增长,越来越理解雷布斯说的: 敲代码要像写诗一样美.也能理解有一次面试官问我你对代码有洁癖吗? 一段好的代码会让人看就像诗一样,也像一个干净房间会让人看去很舒服 ...
- JVM性能优化系列-(1) Java内存区域
1. Java内存区域 1.1 运行时数据区 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.主要包括:程序计数器.虚拟机栈.本地方法栈.Java堆.方法区(运 ...
随机推荐
- 深入理解java虚拟机-01 走进java
第一章是对java的产生,历史的整体介绍 java的使用很广泛,安装jdk的时候会看到一句广告语runs in 10 billions machines.使用java的设备多达几十亿台 1.概述 优点 ...
- LeetCode(13):罗马数字转整数
Easy! 题目描述: 罗马数字包含以下七种字符:I, V, X, L,C,D 和 M. 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如, 罗马数字 2 写 ...
- SCU 4438:Censor
Censor frog is now a editor to censor so-called sensitive words (敏感词). She has a long text p . Her j ...
- MySql存储过程中limit传参
最近做项目用到了MySQL数据库,感觉还是蛮好用的,但是有同事前几天写存储过程的时候老调不通,我看了看后发现把limit语句后面带的参数随便改成一个数字就调试通过了,不知道是MySql当初就这么设计的 ...
- poj 1797 一条路径中的最小边 再找出最大的
Sample Input 1 // T3 3// n m1 2 3//u v w1 3 42 3 5Sample Output Scenario #1:4 # include <iostream ...
- 【LOJ】#2126. 「HAOI2015」数组游戏
题解 简单分析一下就知道\(\lfloor \frac{N}{i} \rfloor\)相同的\(i\)的\(sg\)函数相同 所以我们只要算\(\sqrt{n}\)个\(sg\)函数就好 算每一个\( ...
- PHP获取访问者公网IP
if(!empty($_SERVER["HTTP_CLIENT_IP"])){ $cip = $_SERVER["HTTP_CLIENT_IP"]; } el ...
- JavaScript 对象Array,Map,Set使用
for(int i = 0 :i < 3 ;i++ ){ //[重点说三遍] 在说明每个对象的用法之前,首先说明 JavaScript 对象的使用一定要注意浏览器的兼容性问题!尤其是IE的版本! ...
- JAVAEE——宜立方商城14:项目部署规划、Tomcat热部署、反向代理的配置
1. 学习计划 1.系统部署 2. 项目部署 2.1. 项目架构讲解 2.2. 网络拓扑图 2.3. 系统部署 2.3.1. 部署分析 e3-manager e3-manager-web e3-por ...
- HTTP协议-MIME类型
每一个 URL 都代表着一个资源对象,而当我们请求一个网页的时候,看似只请求了一个 URI(统一资源标识符),实际上这个网页可能包含多个 URI,例如图片资源的 URI 和视频资源的 URI 等.此时 ...