减小 GC 开销的 5 个编码技巧

在这篇文章中,咱们来了解一下让代码变得高效的五种技巧,这些技巧可使咱们的垃圾收集器(GC)在分配内存以及释放内存上面,占用更少的CPU时间,减小GC的开销。当内存被回收的时候,GC处理很长时间常常会致使咱们的代码中断(又叫作”stop the world”)。html

 

背景web

 

GC用来处理大量的短时间的对象的分配(试想打开一个web页面,一旦页面被加载以后,被分配内存的大部分对象都会被废弃)。数组

 

GC使用一个被称做”新生代”堆空间来完成这件事情。”新生代”是用来存放新建对象的堆内存。每个对象都有一个”age”(存储在对象的头信息中),用来定义存放不少没有被回收的垃圾集合。一旦一个肯定的”age”到达,对象就会被复制到堆中的另外一块空间,这个空间被称做”幸存者空间”或者”老年代空间”。(译者注:实际上幸存者空间位于新生代空间中,原文有误,不过这里暂时按照原文来翻译,更详细的内容请点击成为JavaGC专家Part I — 深刻浅出Java垃圾回收机制缓存

 

http://www.importnew.com/1993.html服务器

 

虽然这样颇有效,可是仍是有很大代价的。减小临时分配的数量确实能够帮助咱们增长吞吐量,尤为是在大规模数据的环境下,或者资源有限制的app中。网络

 

下面的五种代码方式能够更加有效的利用内存,而且不须要花费不少的时间,也不会下降代码可读性。数据结构

 

一、避免隐式的String字符串app

 

String字符串是咱们管理的每个数据结构中不可分割的一部分。它们在被分配好了以后不能够被修改。好比”+”操做就会分配一个连接两个字符串的新的字符串。更糟糕的是,这里分配了一个隐式的StringBuilder对象来连接两个String字符串。工具

 

例如:ui

 

a = a + b; // a and b are Strings

 

编译器在背后就会生成这样的一段儿代码:

 

StringBuilder temp = new StringBuilder(a).

temp.append(b);

a = temp.toString(); // 一个新的 String 对象被分配

// 第一个对象 “a” 如今能够说是垃圾了

 

它变得更糟糕了。

 

让咱们来看这个例子:

 

String result = foo() + arg;

result += boo();

System.out.println(“result = “ + result);

 

在这个例子中,背后有三个StringBuilders 对象被分配 – 每个都是”+”的操做所产生,和两个额外的String对象,一个持有第二次分配的result,另外一个是传入到print方法的String参数,在看似很是简单的一段语句中有5个额外的对象。

 

试想一下在实际的代码场景中会发生什么,例如,经过xml或者文件中的文本信息生成一个web页面的过程。在嵌套循环结构,你将会发现有成百上千的对象被隐式的分配了。尽管VM有处理这些垃圾的机制,但仍是有很大代价的 – 代价也许由你的用户来承担。

 

解决方案:

 

减小垃圾对象的一种方式就是善于使用StringBuilder 来建对象,下面的例子实现了与上面相同的功能,然而仅仅生成了一个StringBuilder 对象,和一个存储最终result 的String对象。

 

StringBuilder value = new StringBuilder(“result = “);

value.append(foo()).append(arg).append(boo());

System.out.println(value);

 

经过留心String和StringBuilder被隐式分配的可能,能够减小分配的短时间的对象的数量,尤为在有大量代码的位置。

 

二、计划好List的容量

 

像ArrayList这样的动态集合用来存储一些长度可变化数据的基本结构。ArrayList和一些其余的集合(如HashMap、TreeMap),底层都是经过使用Object[]数组来实现的。而String(它们本身包装在char[]数组中),char数组的大小是不变的。那么问题就出现了,若是它们的大小是不变的,咱们怎么能放item记录到集合中去呢?答案显而易见:分配更多的数组。

 

看下面的例子:

 

List<Item> items = new ArrayList<Item>();

  

for (int i = 0; i < len; i++)

{

Item item = readNextItem();

items.add(item);

}

 

len的值决定了循环结束时items 最终的大小。然而,最初,ArrayList的构造器并不知道这个值的大小,构造器会分配一个默认的Object数组的大小。一旦内部数组溢出,它就会被一个新的、而且足够大的数组代替,这就使以前分配的数组成为了垃圾。

 

若是执行数千次的循环,那么就会进行更屡次数的新数组分配操做,以及更屡次数的旧数组回收操做。对于在大规模环境下运行的代码,这些分配和释放的操做应该尽量从CPU周期中剔除。

 

解决方案:

 

不管何时,尽量的给List或者Map分配一个初始容量,就像这样:

 

List<MyObject> items = new ArrayList<MyObject>(len);

 

由于List初始化,有足够的容量,全部这样能够减小内部数组在运行时没必要要的分配和释放。若是你不知道肯定的大小,最好估算一下这个值的平均值,添加一些缓冲,防止意外溢出。

 

三、使用高效的含有原始类型的集合

 

当前版本的Java编译器对于含有基本数据类型的键的数组以及Map的支持,是经过“装箱”来实现的 – 自动装箱就是将原始数据装入一个对应的对象中,这个对象可被GC分配和回收。

 

这个会有一些负面的影响。Java能够经过使用内部数组实现大多数的集合。对于每一条被添加到HashMap中的key/value记录,都会分配一个存储key和value的内部对象。当处理map的时候很是可怕,这意味着,每当你放一条记录到map中的时候,就会有一次额外的分配和释放操做发生。这极可能致使数量过大,而不得不从新分配新的内部数组。当处理有成百上千条甚至更多记录的Map时,这些内部分配的操做将会使GC的成本增长。

 

一种常见的状况就是保存一个原始类型(如id)和一个对象之间的映射。因为Java的HashMap设计只能包含对象类型(而非原始类型),这意味着,每一个map的插入操做均可能分配一个额外的对象来存储原始类型(即装箱)。

 

Integer.valueOf 方法缓存在-128 – 127之间的数值,可是对于范围以外的每个数值,除了内部的key/value记录对象以外,一个新的对象也将会分配。这极可能超过了GC对于map三倍的开销。对于一个C++开发者来讲,这真是让人不安的消息,在C++中,STL 模板能够很是高效地解决这样的问题。

 

很幸运,这个问题将会在Java的下一个版本获得解决。到那时,这将会被一些提供基本的树形结构(Tree)、映射(Map),以及List等Java的基本类型的库迅速处理。我强力推荐Trove,我已经使用很长时间了,而且它在处理大规模的代码时真的能够减少GC的开销。

 

四、使用数据流(Streams)代替内存缓冲区(in-memory buffers)

 

在服务器应用程序中,咱们操做的大多数的数据都是以文件或者是来自另外一个web服务器或DB的网络数据流的形式呈现给咱们。大多数状况下,传入的数据都是序列化的形式,在咱们使用它们以前须要被反序列化成Java对象。这个过程很是容易产生大量的隐式分配。

 

最简单的作法就是经过ByteArrayInputStream,ByteBuffer 把数据读入内存中,而后再进行反序列化。

 

这是一个糟糕的举动,由于完整的数据在构造新的对象的时候,你须要为其分配空间,而后马上又释放空间。而且,因为数据的大小你又不知道,你只能猜想 – 当超过初始化容量的时候,不得不分配和释放byte[]数组来存储数据。

 

解决方案很是简单。像Java自带的序列化工具以及Google的Protocol Buffers等,它们能够未来自于文件或网络流的数据进行反序列化,而不须要保存到内存中,也不须要分配新的byte数组来容纳增加的数据。若是能够的话,你能够将这种方法和加载数据到内存的方法比较一下,相信GC会很感谢你的。

 

五、List集合

 

不变性是很美好的,可是在大规模情境下,它就会有严重的缺陷。当传入一个List对象到方法中的情景。

 

当方法返回一个集合,一般会很明智的在方法中建立一个集合对象(如ArrayList),填充它,并以不变的集合的形式返回。

 

有些状况下,这并不会获得很好的效果。最明显的就是,当来自多个方法的集合调用一个final集合。由于不变性,在大规模数据状况下,会分配大量的临时集合。

 

这种状况的解决方案将不会返回新的集合,而是经过使用单独的集合当作参数传入到那些方法代替组合的集合。

 

例子1(低效率):

 

List<Item> items = new ArrayList<Item>();

for (FileData fileData : fileDatas)

{

// 每一次调用都会建立一个存储内部临时数组的临时的列表

items.addAll(readFileItem(fileData));

}

 

例子2:

 

List<Item> items =

new ArrayList<Item>(fileDatas.size() * avgFileDataSize * 1.5);

  

for (FileData fileData : fileDatas)

{

readFileItem(fileData, items); // 在内部添加记录

}

 

在例子2中,当违反不变性规则的时候(这一般应该被遵照),能够节省N个list的分配(以及任何临时数组的分配)。这将是对你GC的一个大大的优惠。

相关文章
相关标签/搜索