Linkedin 工程师如何优化他们的 Java 代码

最近在刷各大公司的技术博客的时候,我在Linkedin的技术博客上面发现了一篇很不错博文。这篇博文介绍了Linkedin信息流中间层Feed Mixer,它为Linkedin的Web主页,大学主页,公司主页以及客户端等多个分发渠道提供支撑(如下图所示)。

在Feed Mixer里面用到了一个叫做SPR(念“super”)的库。博文讲的就是如何优化SPR的java代码。下面就是他们总结的优化经验。

1. 谨慎对待Java的循环遍历

Java中的列表遍历可比它看起来要麻烦多了。就以下面两段代码为例:

A:

private final List<Bar> _bars;
for(Bar bar : _bars) {
    //Do important stuff
}

B:

private 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):

优化前的代码:

HashMap<String,Foo> _map;
void addObjects(List<Foo> input)
{
  _map = new HashMap<String, Foo>();
  for(Foo f: input)
  {
    _map.put(f.getId(), f);
  }
}

优化后的代码

HashMap<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 这个属性了:

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:

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和内存的开销。

优化前:

private String transform(String term) {
return outputTerm = term.replaceAll(_regex, _replacement);
}

优化后:

private 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 (具体原因见这里) 大致代码如下:

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%。

文章转载自 开源中国社区 [http://www.oschina.net]

时间: 2025-01-31 01:44:55

Linkedin 工程师如何优化他们的 Java 代码的相关文章

Linkedin工程师是如何优化他们的Java代码的(转)

  英文原文:LinkedIn Feed: Faster with Less JVM Garbage 最近在刷各大公司的技术博客的时候,我在Linkedin的技术博客上面发现了一篇很不错博文.这篇博文介绍了Linkedin信息流中间层Feed Mixer,它为Linkedin的Web主页,大学主页,公司主页以及客户端等多个分发渠道提供支撑(如下图所示). 在Feed Mixer里面用到了一个叫做SPR(念"super")的库.博文讲的就是如何优化SPR的java代码.下面就是他们总结的

优化-这段java代码如何优雅的实现?

问题描述 这段java代码如何优雅的实现? for(AbstractResponse resp : responseList) { if (resp instanceof AResponse) { AResponse areponse = (AResponse)resp; //1 //2 //3 }else if (resp instanceof BResponse) { BResponse breponse = (BResponse)resp; //1 //2 //3 }else if (re

请教优化一段java代码

问题描述 请教各位,一下代码哪里能优化或者代码风格能优化的地方么,然后给个修改理由,非常感谢,初学java,希望知道哪样的才是工作中写的代码importorg.apache.commons.logging.LogFactory;publicclassAccountClass{privatefinaltransientstaticint[]values=newint[4];publicvoidaddValues(inti,intv){try{if(isOK(i)){values[i]=v;}}ca

java也能写出点点算法-像C++一样去优化核心并发的代码例子1

java其实更多用来写业务代码,代码写得好不好,关键看抽象能力如何,不过如果你要用java写很核心的插件和高并发的片段,那么可能还是需要注意一些写法,那种写法可能会更好,才能使得并发量提高,而且更少的使用CPU和内存:我最近在一段采集系统访问的java代码,通过过滤器切入到应用中,遇到的一些小细节的调整,感觉还有点意思,以下为收集信息中碰到的两个需要判定的地方(对java优化没有任何要求的,本文纯属扯淡,呵呵): 原始代码片段1(用于判定是否为静态资源): if(servletPath == n

优化-java代码中有个循环,数据量一大直接网页报500错误

问题描述 java代码中有个循环,数据量一大直接网页报500错误 求大神帮忙优化一下,10000数据还可以,但是数据一多网页就崩溃了,就是代码中的row++,加上去就不行了 解决方案 结果是内存益处了 那就是你的程序太耗费资源了. 那个获取page的方法: 1--Page对象村的东西太多了,很多何你写入单元格的数据没关系, 既然这块逻辑是大数据量的,为什么不拆分针对这块逻辑的结构呢? 使用Page造成了很大的内存浪费 在循环外部定义这个结构 List list = null 2--将方法的参数精

使用 IBM 静态工具优化 Java 代码,第 2 部分: 分析错误报告

概述 BEAM 报告的结果文件是通过 build.xml 中 --beam::complaint_file 所定义的,在这 里,本文假设其为 BEAM-messages.BEAM-messages 记录着报出的所有代码缺陷,这些缺陷 分为 ERROR,MISTAKE 和 WARNING 三大类,严重程度依次递减.每一个具体的 ERROR, MISTAKE 和 WARNING 都代表着一个错误模式,本文接下来就通过实例分析理解其中的某些 重要错误模式,告诉读者在写 Java 代码时如何避免这些错误

使用 IBM 静态工具优化 Java 代码,第 1 部分: 工具入门

什么是 BEAM? 关于缩写 BEAM 的声明 处于表达简洁的目的,本文采用了工具名称的缩写 - BEAM,这只是工具全称 "Checking Tool for Bugs Errors and Mistakes"的文字缩写,而不是工具的名称. IBM Checking Tool for Bugs Errors and Mistakes(本文后面将采用其文字缩写 BEAM )是 IBM 开发的一个静态分析工具,可以用于分析并查找出 C, C++ 和 Java 代码中的一 些不容易发现的潜

听阿里巴巴JVM工程师为你分析常见Java故障案例

本文根据12月23日阿里巴巴技术保障部JVM组软件工程师陆传胜老师的主题分享整理!小编特别整理出其中精华内容,供大家学习交流. 目录 HotSpot常识 Java故障排查方法论 Java故障案例分析 Part 1    HotSpot常识    HotSpot是目前最常见的开源JVM(GPL协议),用来运行Java应用和applet,本次讨论基本都是基于这一软件来进行的.   所有的Java对象都是分配在Java堆上的,Java代码中看到的引用,在JVM的实现中就是一个指针,指向一段被表示成对象

诊断Java代码: 在规范钢丝上行走

要构建可靠的软件,程序规范很关键.没有良好定义的规范,很难诊断软件系统的异常行为.但是很多软件系统的程序规范定义得很差劲.而且更糟的,是许多软件系统根本就没有规范. 直观的看,程序规范是对程序行为的一种描述.它可以采取许多形式,但无论采取何种形式,都有一条主线贯穿所有实例:必须有某种类型的系统规范,因为您得依靠它来判断系统是否运转正常. 规范可以形式化也可以松散地定义,这取决于开发中系统的稳定性和危险程度,还与开发完毕后修改系统的容易程度有关. 我们将通过讨论规范为什么重要.为什么会经常被忽略以