jvm学习笔记二(减少GC开销的建议)

时间:2024-01-05 14:41:26

一:触发主GC(Garbage Collector)的条件

  JVM进行次GC的频率很高,但因为这种GC占用时间极短,所以对系统产生的影响不大。更值得关注的是主GC的触发条件,因为它对系统影响很明显。总的来说,有两个条件会触发主GC:

  1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。

  2)Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。

  由于是否进行主GC由JVM根据系统环境决定,而系统环境在不断的变化当中,所以主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。

二:减少GC开销的5个编程技巧

  根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:

1、避免隐式的String字符串

  String字符串是我们管理的每一个数据结构中不可分割的一部分。它们在被分配好了之后不可以被修改。比如"+"操作就会分配一个链接两个字符串的新的字符串。更糟糕的是,这里分配了一个隐式的StringBuilder对象来链接两个String字符串。

  例如:

 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被隐式分配的可能,可以减少分配的短期的对象的数量,尤其在有大量代码的位置。

2、计划好List的容量

像ArrayList这样的动态集合用来存储一些长度可变化数据的基本结构。ArrayList和一些其他的集合(如HashMap、TreeMap),底层都是通过使用Object[]数组来实现的。而String数组(它们自己包装在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初始化,有足够的容量,所有这样可以减少内部数组在运行时不必要的分配和释放。如果你不知道确定的大小,最好估算一下这个值的平均值,添加一些缓冲,防止意外溢出。

3、使用高效的原始的集合

当前Java编译器的版本支持数组和Map类型(基本的key/value),这些都是通过“boxing”来支持的 - “boxing”包装标准的可被GC分配和回收利用的对象的原始value值。

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

一种常见的情况就是在一个原始值(如id)和一个对象之间的映射。由于Java的HashMap被创建是为了持有对象的类型(vs 原始),这意味着,每个map的插入操作都可能分配一个额外的对象来存储原始的value值("boxing" it)。

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本地的serialization和Google的Protocol Buffers等,这样大多数的持久化的库被创建用来反序列化来自于文件或网络流的数据,不需要保存到内存中,也不需要分配新的byte数组来容纳增长的数据。如果可以的话,你可以将这种方法和加载数据到内存的方法比较一下,相信GC会很感谢你的。

5、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的一个大大的优惠。

三、 减少GC开销的措施

根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:

  (1)不要显式调用System.gc()

  此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

  (2)尽量减少临时对象的使用

  临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

  (3)对象不用时最好显式置为Null

  一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

  (4)尽量使用StringBuffer,而不用String来累加字符串

  由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

  (5)能用基本类型如Int,Long,就不用Integer,Long对象

  基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

  (6)尽量少用静态对象变量

  静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

  (7)分散对象创建或删除的时间

  集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

四、垃圾回收的几个特点

(1)垃圾收集发生的不可预知性:由于实现了不同的垃圾回收算法和采用了不同的收集机制,所以它有可能是定时发生,有可能是当出现系统空闲CPU资源时发生,也有可能是和原始的垃圾收集一样,等到内存消耗出现极限时发生,这与垃圾收集器的选择和具体的设置都有关系。
  (2)垃圾收集的精确性:主要包括2

个方面:(a)垃圾收集器能够精确标记活着的对象;(b)垃圾收集器能够精确地定位对象之间的引用关系。前者是完全地回收所有废弃对象的前提,否则就可能造成内存泄漏。而后者则是实现归并和复制等算法的必要条件。所有不可达对象都能够可靠地得到回收,所有对象都能够重新分配,允许对象的复制和对象内存的缩并,这样就有效地防止内存的支离破碎。
  (3)现在有许多种不同的垃圾收集器,每种有其算法且其表现各异,既有当垃圾收集开始时就停止应用程序的运行,又有当垃圾收集开始时也允许应用程序的线程运行,还有在同一时间垃圾收集多线程运行。
  (4)垃圾收集的实现和具体的JVM
以及JVM的内存模型有非常紧密的关系。不同的JVM 可能采用不同的垃圾收集,而JVM
的内存模型决定着该JVM可以采用哪些类型垃圾收集。现在,HotSpot
系列JVM中的内存系统都采用先进的面向对象的框架设计,这使得该系列JVM都可以采用最先进的垃圾收集。
  (5)随着技术的发展,现代垃圾收集技术提供许多可选的垃圾收集器,而且在配置每种收集器的时候又可以设置不同的参数,这就使得根据不同的应用环境获得最优的应用性能成为可能。
  针对以上特点,我们在使用的时候要注意:
  (1)不要试图去假定垃圾收集发生的时间,这一切都是未知的。比如,方法中的一个临时对象在方法调用完毕后就变成了无用对象,这个时候它的内存就可以被释放。
  (2)Java中提供了一些和垃圾收集打交道的类,而且提供了一种强行执行垃圾收集的方法--调用System.gc(),但这同样是个不确定的方法。Java
中并不保证每次调用该方法就一定能够启动垃圾收集,它只不过会向JVM发出这样一个申请,到底是否真正执行垃圾收集,一切都是个未知数。
  (3)挑选适合自己的垃圾收集器。一般来说,如果系统没有特殊和苛刻的性能要求,可以采用JVM的缺省选项。否则可以考虑使用有针对性的垃圾收集器,比如增量收集器就比较适合实时性要求较高的系统之中。系统具有较高的配置,有比较多的闲置资源,可以考虑使用并行标记/清除收集器。
  (4)关键的也是难把握的问题是内存泄漏。良好的编程习惯和严谨的编程态度永远是最重要的,不要让自己的一个小错误导致内存出现大漏洞。
  (5)尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null,暗示垃圾收集器来收集该对象,还必须注意该引用的对象是否被监听,如果有,则要去掉监听器,然后再赋空值。

参考:

http://blog.csdn.net/zsuguangh/article/details/6429592

http://blog.csdn.net/tayanxunhua/article/details/21752781