减少GC开销的5个编码技巧
在这篇文章中,我们来了解一下让代码变得高效的五种技巧,这些技巧可以使我们的垃圾收集器(GC)在分配内存以及释放内存上面,占用更少的CPU时间,减少GC的开销。当内存被回收的时候,GC处理很长时间经常会导致我们的代码中断(又叫做”stop the world”)。
背景
GC用来处理大量的短期的对象的分配(试想打开一个web页面,一旦页面被加载之后,被分配内存的大部分对象都会被废弃)。
GC使用一个被称作”新生代”堆空间来完成这件事情。”新生代”是用来存放新建对象的堆内存。每一个对象都有一个”age”(存储在对象的头信息中),用来定义存放很多没有被回收的垃圾集合。一旦一个确定的”age”到达,对象就会被复制到堆中的另一块空间,这个空间被称作”幸存者空间”或者”老年代空间”。(译者注:实际上幸存者空间位于新生代空间中,原文有误,不过这里暂时按照原文来翻译,更详细的内容请点击成为JavaGC专家Part I — 深入浅出Java垃圾回收机制)
虽然这样很有效,但是还是有很大代价的。减少临时分配的数量确实可以帮助我们增加吞吐量,尤其是在大规模数据的环境下,或者资源有限制的app中。
下面的五种代码方式可以更加有效的利用内存,并且不需要花费很多的时间,也不会降低代码可读性。
1、避免隐式的String字符串
String字符串是我们管理的每一个数据结构中不可分割的一部分。它们在被分配好了之后不可以被修改。比如”+”操作就会分配一个链接两个字符串的新的字符串。更糟糕的是,这里分配了一个隐式的StringBuilder对象来链接两个String字符串。
例如:
1
|
a = a + b; // a and b are Strings |
编译器在背后就会生成这样的一段儿代码:
1
2
3
4
|
StringBuilder temp = new StringBuilder(a). temp.append(b); a = temp.toString(); // 一个新的 String 对象被分配 // 第一个对象 “a” 现在可以说是垃圾了 |
它变得更糟糕了。
让我们来看这个例子:
1
2
3
|
String result = foo() + arg; result += boo(); System.out.println(“result = “ + result); |
在这个例子中,背后有三个StringBuilders 对象被分配 – 每一个都是”+”的操作所产生,和两个额外的String对象,一个持有第二次分配的result,另一个是传入到print方法的String参数,在看似非常简单的一段语句中有5个额外的对象。
试想一下在实际的代码场景中会发生什么,例如,通过xml或者文件中的文本信息生成一个web页面的过程。在嵌套循环结构,你将会发现有成百上千的对象被隐式的分配了。尽管VM有处理这些垃圾的机制,但还是有很大代价的 – 代价也许由你的用户来承担。
解决方案:
减少垃圾对象的一种方式就是善于使用StringBuilder 来建对象,下面的例子实现了与上面相同的功能,然而仅仅生成了一个StringBuilder 对象,和一个存储最终result 的String对象。
1
2
3
|
StringBuilder value = new StringBuilder(“result = “); value.append(foo()).append(arg).append(boo()); System.out.println(value); |
通过留心String和StringBuilder被隐式分配的可能,可以减少分配的短期的对象的数量,尤其在有大量代码的位置。
2、计划好List的容量
像ArrayList这样的动态集合用来存储一些长度可变化数据的基本结构。ArrayList和一些其他的集合(如HashMap、TreeMap),底层都是通过使用Object[]数组来实现的。而String(它们自己包装在char[]数组中),char数组的大小是不变的。那么问题就出现了,如果它们的大小是不变的,我们怎么能放item记录到集合中去呢?答案显而易见:分配更多的数组。
看下面的例子:
1
2
3
4
5
6
7
|
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分配一个初始容量,就像这样:
1
|
List<MyObject> items = new ArrayList<MyObject>(len); |
因为List初始化,有足够的容量,所有这样可以减少内部数组在运行时不必要的分配和释放。如果你不知道确定的大小,最好估算一下这个值的平均值,添加一些缓冲,防止意外溢出。
3、使用高效的含有原始类型的集合
当前版本的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的开销。
4、使用数据流(Streams)代替内存缓冲区(in-memory buffers)
在服务器应用程序中,我们操作的大多数的数据都是以文件或者是来自另一个web服务器或DB的网络数据流的形式呈现给我们。大多数情况下,传入的数据都是序列化的形式,在我们使用它们之前需要被反序列化成Java对象。这个过程非常容易产生大量的隐式分配。
最简单的做法就是通过ByteArrayInputStream,ByteBuffer 把数据读入内存中,然后再进行反序列化。
这是一个糟糕的举动,因为完整的数据在构造新的对象的时候,你需要为其分配空间,然后立刻又释放空间。并且,由于数据的大小你又不知道,你只能猜测 – 当超过初始化容量的时候,不得不分配和释放byte[]数组来存储数据。
解决方案非常简单。像Java自带的序列化工具以及Google的Protocol Buffers等,它们可以将来自于文件或网络流的数据进行反序列化,而不需要保存到内存中,也不需要分配新的byte数组来容纳增长的数据。如果可以的话,你可以将这种方法和加载数据到内存的方法比较一下,相信GC会很感谢你的。
5、List集合
不变性是很美好的,但是在大规模情境下,它就会有严重的缺陷。当传入一个List对象到方法中的情景。
当方法返回一个集合,通常会很明智的在方法中创建一个集合对象(如ArrayList),填充它,并以不变的集合的形式返回。
有些情况下,这并不会得到很好的效果。最明显的就是,当来自多个方法的集合调用一个final集合。因为不变性,在大规模数据情况下,会分配大量的临时集合。
这种情况的解决方案将不会返回新的集合,而是通过使用单独的集合当做参数传入到那些方法代替组合的集合。
例子1(低效率):
1
2
3
4
5
6
|
List<Item> items = new ArrayList<Item>(); for (FileData fileData : fileDatas) { // 每一次调用都会创建一个存储内部临时数组的临时的列表 items.addAll(readFileItem(fileData)); } |
例子2:
1
2
3
4
5
6
7
|
List<Item> items = new ArrayList<Item>(fileDatas.size() * avgFileDataSize * 1.5 ); for (FileData fileData : fileDatas) { readFileItem(fileData, items); // 在内部添加记录 } |
在例子2中,当违反不变性规则的时候(这通常应该被遵守),可以节省N个list的分配(以及任何临时数组的分配)。这将是对你GC的一个大大的优惠。
减少GC开销的5个编码技巧的更多相关文章
- jvm学习笔记二(减少GC开销的建议)
一:触发主GC(Garbage Collector)的条件 JVM进行次GC的频率很高,但因为这种GC占用时间极短,所以对系统产生的影响不大.更值得关注的是主GC的触发条件,因为它对系统影响很明显.总 ...
- 减少GC开销的措施
程序的运行会直接影响系统环境的变化,从而影响GC的触发.若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响.为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销.具体 ...
- 代码优化:Java编码技巧之高效代码50例
出处: Java编码技巧之高效代码50例 1.常量&变量 1.1.直接赋值常量值,禁止声明新对象 直接赋值常量值,只是创建了一个对象引用,而这个对象引用指向常量值. 反例: Long i = ...
- CSS编码技巧
前面的话 本文将从DRY.currentColor.inherit和合理使用简写这几方面来详细介绍CSS编码技巧 DRY DRY,即don`t repeat yourself,尽量减少代码重复 在软件 ...
- [css 揭秘]:CSS编码技巧
CSS编码技巧 我的github地址:https://github.com/FannieGirl/ifannie 喜欢的给我一个星吧 尽量减少代码重复 尽量减少改动时需要编辑的地方 当某些值相互依赖时 ...
- JVM的分区+查看GC对象是否存活+3种GC算法+7种垃圾收集器+如何减少GC次数
一.JVM的分区: 1.程序计数器(私有) 程序计数器是一块较小的内存分区,你可以把它看做当前线程所执行的字节码的指示器. 在虚拟机的概念模型里,字节码解释器工作时,就是通过改变计数器的值来选择下 ...
- Java编码技巧与代码优化
本文参考整理自https://mp.weixin.qq.com/s/-u6ytFRp-ZAqdLBsMmuDMw 对于在本文中有所疑问的点可以去该文章查看详情 常量&变量 直接赋值常量值, 禁 ...
- 01--css编码技巧--css揭秘
一 尽量减少代码重复 1.按钮 #btn { padding: .3em .8em; border: 1px solid #446d88; background: #58a linear-gradie ...
- PHP编码技巧
原则 正确实现功能 执行速度与快 占系统资源少 后期维护方便 编程注意 1.命名很重要 2.适当的使用注释 3.使用一个变量,需要初始化 4.优先使用单引号 $row['id']的效率是$row[id ...
随机推荐
- 马士兵Servlet&Jsp学习
Servlet&JSP 1>http常见错误信息: 404--url地址找不找, 403--禁止访问 500--服务器内部错误 2>Servlet的生命周期: *生命全过 ...
- pyqt记录内容(音乐播放器)
#这是UI文件 # -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'AudioPlayerDia ...
- 如何在android上去控制开发进度
这次android的壁纸软件1.0版本终于可以上线了,软件的功能基本上实现了,但是用户体验不太好.在整个开发阶段和测试阶段,出现了很多预料之外的事情,比如size是1M多的json文件解析.高清图片导 ...
- Multithreading: How to Use the Synchronization Classes
(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu 转载请标明来源) 翻译文章来源: msdn - Multithreading: How to Use t ...
- [Node.js] Use "prestart" in scripts
Usually we run : npm start to start an app, then we we might call other script to do something: npm ...
- myeclipse自动补全设置
第一步: windows -->preference -->java -->editor -->content Assist --> auto activation -- ...
- [CSAPP笔记][第十二章并发编程]
第十二章 并发编程 如果逻辑控制流在时间上是重叠,那么它们就是并发的(concurrent).这种常见的现象称为并发(concurrency). 硬件异常处理程序,进程和Unix信号处理程序都是大家熟 ...
- oracle之case when
oracle case when 的用法 http://www.cnblogs.com/xiaowu/archive/2011/08/17/2143445.html(转) http://www.cnb ...
- 虚拟现实,增强现实,VR,AR
现在的热点不止VR,还有AR和披着MR.HR.CR外衣的各种高级AR们,所以比较着一起说.以下知乎上一网友观点,放几条结论:1.近期(未来两三年)看,VR能火,AR尚待成熟: 2.VR设备中,插片式是 ...
- 【JAVA编码专题】总结
第一部分:编码基础 为什么需要编码:用计算机看得懂的语言(二进制数)表示各种各样的字符. 一.基本概念 ASCII.Unicode.big5.GBK等为字符集,它们只定义了这个字符集内有哪些字符,以及 ...