在本文中,我们将介绍五种方法,这些方法可以使用有效的编码来帮助垃圾回收器减少分配和释放内存的CPU时间,并减少GC开销。 较长的GC通常会导致我们的代码在回收内存时被停止(也称为“停止世界”)。
一些背景
GC的建立是为了处理大量短期对象的分配(例如渲染网页等,其中大部分分配的对象在服务页面后就已过时)。
GC使用所谓的“年轻”来完成此工作–分配新对象的堆段。 每个对象都有一个“年龄”(放置在对象的标题位中),该年龄定义了它在没有回收的情况下“生存”了多少个集合。 一旦达到一定年龄,该对象将被复制到堆中称为“幸存者”或“旧”世代的另一部分。
该过程虽然有效,但仍然要付出代价。 能够减少临时分配的数量确实可以帮助我们提高吞吐量,尤其是在大规模应用程序中。
以下是五种我们可以编写日常代码的方法,这些代码可以提高内存效率,而不必花费大量时间或降低代码的可读性。
1.避免隐式字符串
字符串几乎是我们管理的每个数据结构不可或缺的一部分。 它们比其他原始值重得多,它们对内存使用量的影响更大。
要注意的最重要的事情之一是字符串是不可变的 。 分配后不能修改它们。 用于连接的运算符(例如“ +”)实际上分配了一个新的String,其中包含要连接的字符串的内容。 更糟糕的是,有一个隐式的StringBuilder对象被分配来实际完成组合它们的工作。
例如 -
a = a + b; // a and b are Strings
编译器在后台生成可比较的代码:
StringBuilder temp = new StringBuilder(a).
temp.append(b);
a = temp.toString(); // a new String is allocated here.// The previous “a” is now garbage.
但情况变得更糟。
让我们看这个例子–
String result = foo() + arg;
result += boo();
System.out.println(“result = “ + result);
在此示例中,我们在后台分配了3个StringBuilder-每个加号操作一个,另外两个Strings-一个用于保存第二个赋值的结果,另一个用于保存传递给print方法的字符串。 那是另外5个对象 ,否则看起来很简单。
考虑一下在现实世界中的代码场景中会发生什么,例如生成网页,使用XML或从文件中读取文本。 嵌套在循环结构中,您可能正在查看成百上千个隐式分配的对象。 尽管VM具有处理此问题的机制, 但它是有代价的 –由用户支付。
解决方案:减少这种情况的一种方法是主动使用StringBuilder分配。 下面的示例获得与上面的代码相同的结果,同时仅分配一个StringBuilder和一个String来保存最终结果,而不是原来的五个对象。
StringBuilder value = new StringBuilder(“result = “);
value.append(foo()).append(arg).append(boo());
System.out.println(value);
通过注意隐式分配Strings和StringBuilders的方式,可以从实质上减少大规模代码位置中的短期分配量。
2.计划清单的能力
诸如ArrayLists之类的动态集合是保存动态长度数据的最基本的结构之一。 ArrayList和其他集合(例如HashMaps和TreeMaps)是使用基础Object []数组实现的。 像字符串(char []数组本身包装)一样,数组也是不可变的。 显而易见的问题变成了-如果基础数组的大小是不变的,我们如何在集合中添加/放置项目? 答案也很明显–通过分配更多的数组 。
让我们看这个例子–
List<Item> items = new ArrayList<Item>();for (int i = 0; i < len; i++)
{Item item = readNextItem();items.add(item);
}
len的值确定循环结束后项目的最终长度。 但是,此值对于ArrayList的构造函数是未知的,该构造函数分配具有默认大小的新Object数组。 每当超出内部阵列的容量时,就会用足够长的新阵列替换内部阵列的容量,从而使以前的阵列成为垃圾。
如果要执行数千次循环,则可能会强制分配一个新数组,并多次收集前一个数组。 对于在大规模环境中运行的代码,这些分配和取消分配都从计算机的CPU周期中扣除。
解决方案:尽可能分配具有初始容量的列表和地图,如下所示:
List<MyObject> items = new ArrayList<MyObject>(len);
这样可以确保在运行时不会发生内部数组的不必要分配和取消分配,因为列表现在具有足够的容量开始。 如果您不知道确切的大小,最好对平均大小进行估算(例如1024、4096),并添加一些缓冲区以防止意外溢出。
3.使用有效的原始集合
Java编译器的当前版本通过使用“装箱”来支持具有原始键或值类型的数组或映射-将原始值包装在可由GC分配和回收的标准对象中。
这可能会带来一些负面影响 。 Java使用内部数组实现大多数集合。 对于添加到HashMap的每个键/值条目,分配一个内部对象来容纳两个值。 这在处理地图时是必不可少的,这意味着您每次将商品放入地图时都会进行额外的分配和可能的重新分配。 还可能会增加容量并不得不重新分配新的内部阵列。 当处理包含数千个或更多条目的大型地图时,这些内部分配可能会增加GC的成本。
一种非常常见的情况是在原始值(例如Id)和对象之间保留映射。 由于Java的HashMap是为保存对象类型(相对于基元)而构建的,因此这意味着映射中的每个插入都可以潜在地分配另一个对象来保存基元值(“装箱”它)。
标准的Integer.valueOf方法缓存0到255之间的值,但是对于每个大于0的数字,除了内部键/值输入对象之外,还将分配一个新对象。 这可能会使映射的GC开销增加三倍以上。 对于那些来自C ++背景的人来说,这确实是令人不安的消息,因为STL模板可以非常有效地解决此问题。
幸运的是,此问题正在Java的下一版本中进行。 在此之前,一些出色的库已经对其进行了相当有效的处理,这些库为Java的每种原始类型提供了原始树,映射和列表。 我强烈推荐Trove ,我已经使用了很长时间 ,发现它确实可以减少大规模代码中的GC开销。
4.使用流而不是内存缓冲区
我们在服务器应用程序中处理的大多数数据都是通过文件或数据从另一个Web服务或数据库通过网络流式传输给我们的。 在大多数情况下,传入的数据是序列化的,在我们开始对其进行操作之前,需要将其反序列化为Java对象。 这个阶段很容易出现大量的隐式分配 。
通常最简单的方法是使用ByteArrayInputStream,ByteBuffer将数据读取到内存中,然后将其传递给反序列化代码。
这可能是一个错误的举动 ,因为您需要在构造出新的对象时为整个数据分配和释放空间。 而且,由于数据的大小可能是未知的,您猜到了–您必须分配和取消分配内部byte []数组,以便在数据超出初始缓冲区的容量时容纳它们。
解决方案非常简单。 大多数持久性库(例如Java的本机序列化,Google的协议缓冲区等)都可以直接从传入的文件或网络流中反序列化数据,而不必将其保留在内存中,也不必分配新的内部字节数组来保存数据。随着数据的增长。 如果可以的话,请采用这种方法,而不是将数据加载到内存。 您的GC将感谢您。
5.汇总列表
不变性是一件美丽的事情,但是在某些大规模情况下,它可能会存在一些严重的缺点。 一种情况是在方法之间传递List对象。
从函数返回集合时,通常建议在方法内创建集合对象(例如ArrayList),将其填充并以不可变的Collection接口的形式返回。
在某些情况下这不能很好地工作 。 最引人注目的是将集合从多个方法调用中聚合到最终集合中。 虽然不变性提供了更高的清晰度,但在大规模情况下,这也可能意味着临时集合的大量分配。
在这种情况下,解决方案不是返回新的集合,而是将值聚合到单个集合中,该集合作为参数传递到这些方法中。
示例1(效率低下)–
List<Item> items = new ArrayList<Item>();for (FileData fileData : fileDatas)
{// Each invocation creates a new interim list with possible// internal interim arraysitems.addAll(readFileItem(fileData));
}
示例2 –
List<Item> items =new ArrayList<Item>(fileDatas.size() * avgFileDataSize * 1.5);for (FileData fileData : fileDatas)
{readFileItem(fileData, items); // fill items inside
}
示例2在遵守不变性规则(通常应遵守该规则)的同时,可以保存N个列表分配(以及任何临时数组分配)。 在大规模情况下,这可以为您的GC带来福音。
补充阅读
- 字符串实习– http://plumbr.eu/blog/reducing-memory-usage-with-string-intern
- 高效的包装器– http://vanillajava.blogspot.co.il/2013/04/low-gc-coding-efficient-listeners.html
- 使用Trove – http://java-performance.info/primitive-types-collections-trove-library/
此帖子也可以在Speaker Deck上找到
翻译自: https://www.javacodegeeks.com/2013/07/5-coding-hacks-to-reduce-gc-overhead.html