Java theory and practice: Garbage collection and p

Java theory and practice: Garbage collection and performance
这是我第一次翻译文章,实践一下,先说一下背景,这篇文章是Brian Goetz写的关于垃圾回收的一系列文章的最后一篇,前面还有两篇。
本文的原文链接: [url]http://www.ibm.com/developerworks/java/library/j-jtp01274.html[/url]
正文:
[color=green]垃圾回收的代价(How expensive is allocation?)[/color]
JDK1.0和1.1使用标记交换的方式进行内存收集,在采用这种方式时,有时JDK使用了内存压缩的技术,而有的并没有,因此,这就意味着我们的堆

可能在一次垃圾回收后产生大量的碎片。与C或C++使用的探索法相比,java回收方式是一种代价极高的方式,因为在每一次回收过程中标记交换的方

式总是交换整个内存。
在JDK1.2之后,Sun JDK引入了分代回收的机制,因为对于年轻的对象(young generation)采用了复制回收机制,这使得在堆中的空闲内存总是连

续的,所以当分配一个新的对象时只用做一个简单的指针加法(清单-1),通过这种方式使得在java中为对象分配一个空间时明显的比C廉价,这可

能第一次对于开发者来说是难以想象的,同时,复制收集不会访问已不可使用的对象,这使得在堆中有大量的临时对象,再一次回收中复制收集只是

简单的跟踪和复制活跃的对象到空闲的空间中并且回收整个堆。这里不会再有空闲列表,和成块的结合体,不要压缩,只是擦除堆并且从新开始。这

就使得在jdk1.2之后对象的收集和释放变得非常的廉价。
[color=orange]清单 1. Fast allocation in a contiguous heap[/color]
void *malloc(int n) { 
synchronized (heapLock) {
if (heapTop - heapStart > n)
doGarbageCollection();

void *wasStart = heapStart;
heapStart += n;
return wasStart;
}
}

处于性能的建议对象应该有一个短的生命期,不过这是在以前分配内存十分昂贵的时代了,现在不需要再考虑这种情况了。事实上,避免分配内存

已不再是一个性能应该考虑的问题了。Sun公司分配一次内存只需要十条机器指令,这是相当的廉价的,这也不需要我们为了避免内存的分配而是我

们的编程工作变得十分的复杂。
当然,分配只是内存存储的一半工作,大多数的对象被分配后是要通过垃圾回收的,这也是需要代价的,同样这里也有好消息,在大多数的Java程

序中,几乎所有的对象都会在下一次被回收时变为垃圾,在年轻的对象代中为对象分配一块内存和当前堆中活跃的对象是成比例的,而不是与上一次

回收中分配的对象有关,只是因为只有非常少数的年轻对象会在上一次回收结束后存活下来,为每一次分配的开销而分摊的内存收集是非常小的(如

果内存允许,可以把堆的大小设置的更大)
等等,变得更好(But wait, it gets better)
Java的编译器JIT通过额外的选项可以将分配一个对象的代价减少到零,考虑列表2中的代码,方法
getPostion()创建了一个用于保存一个点的坐标临时的对象Point,并且调用它的方法只是短暂的使用了
它一次然后就放弃了它,当JIT调用到getPostion()方法时,它可能采用了一种被称作escape analysis
的技术,通过这种技术JIT会认识到当Point对象离开doSomething()方法后,再不会有Point对象的引
用存在。当知道这些后,JIT可能不会在堆中为Point对象分配空间,而是可能在栈中分配,甚至可能在
寄存器中分配。目前Sun的 JVM还不支持这种方式,未来的JVM可能会,但事实是在将来分配内存的代价
会越来越小,你不必为了避免为对象分配内存而改变你的代码。

[color=orange]清单 2. Escape analysis can eliminate many temporary allocations entirely [/color]
void doSomething() { 
Point p = someObject.getPosition();
System.out.println("Object is at (" + p.x, + ", " + p.y + ")");
}

...

Point getPosition() {
return new Point(myX, myY);
}


[color=green]这会是一个可测量的分配瓶颈么?(Isn't the allocator a scalability bottleneck? )[/color]
清单1展示了快速分配内存的方式,但是对于跨线程的程序,它必须同步。那么同步会使分配内存的代
价提高么?对于JVM有很多聪明的策略去减少同步的代价,IBM的JVM使用了一种被称作thread-local
heaps的技术,通过这种技术每一个线程通过分配器请求一小块内存(通常是顺序的1K),这样对于小的对
象存在这里就可以了,如果程序要求分配的块比这一块大的话,那么全局分配器被直接使用,然后为其分
配可以满足它的空间,通过这种技术通过这种技术,内存分配的很大的比例会被直接满足,而不需要一个
用于竞争的堆共享锁。(Sun的JVM也使用了同样的技术,叫做Local Allocation Blocks)。
Finalizers不是你的朋友 (Finalizers are not your friend )
那些显式实现了finalize()方法的对象比起一般的对象有明显的性能劣势,如果对象是finalizable的,
那么不管是为这个对象分配内存还是回收这个对象都会比一般的对象要慢。在分配该对象的时候,JVM会向垃圾回收器注册这个对象,并且为该对象

分配内存会比一般的对象慢,同样的,在回收该对象时一样会
很慢,在回收该对象时,垃圾回收器至少会做两个垃圾回收循环,并且还会额外的调用finalize()方法。
由于不可到达的finalizeable对象会被保留很长的时间,所以导致了分配和回收这种对象会花费更多的时
间同时也会为垃圾回收机制带来更多的压力。结合实际,JVM不会保证finalize()方法会在一个可预见的
时间被调用,事实上,没有什么情景下应该使用finalize。
如果你必须使用finalize,那么有一些警戒线是不可越过的,第一,应该是你的对象保持最小,这回减
少分配和回收时的代价。同时,也不要让你的对象保存别的对象,这回使你的对象在不可达时,保持最小
因为他们比实际回收时间要延迟很长时间,实际上,应该避免使用finalize。
帮助垃圾回收器或者不(Helping the garbage collector . . . not )
因为Java的垃圾回收曾经一度是影响Java程序性能的罪魁祸首,所以有很多聪明的策略被开发出来,
包括对象池,显示的置空等技术。然而,在许多情况下这些技术不但不会帮助回收器,还会危害他们。

对象池 (Object pooling)
使用对象池是最简单的一种观点——它把经常使用的对象保持在池中,等我们需要使用的时候再从池中
获取,从而代替了创建新的对象。这种理论将创建对象的代价进行了分散。当创建对象的代价非常高时,
使用这种技术是非常明智的,比如创建一个与数据库的链接,或者是使用大量的线程时,再或者创建的对
象是一种需要很高代价或需要限制的。然而,以上情况的发生通常是非常小的。
此外,对象池有非常严重的副作用。因为对象池常被在许多线程之间使用,使用这就可能产生由于线程
同步引起的瓶颈。同时,对象池会强迫你回收你分配的对象,这常常会引起指针倒置的问题。而池的大小
也需要与实际情况相适应,如果太小,则不会阻止对象的分配,而如果太大,将会产生大量的空闲的对象。
同时也可能加剧垃圾回收的压力,由于长时间占据内存。总之,写出一个高效的对象池是一个非常困难的
工作。
在JavaOne 2003大会上,Dr. Cliff Click在他的文章"Performance Myths Exposed"(见引
用资源)中,通过一系列的基础数据,阐述了在现代的JVMs中,除了那些重量级的对象,使用对象
池常会引起性能的下降而不是提升。同时通过一些列的分配可能引起指针的倒置。总之,除了适合使
用池技术的情景,通常我们应该避免使用这种技术。

显式置空 (Explicit nulling)
显式的置空是一种简单的实现,它将我们不再使用的对象进行显式的置空,这一技术实际上暗示着它帮
助垃圾回收器将不可到达的对象标记的更早。或者这至少是一种理论。
这有一个例子说明了使用显式的置空,不仅是有帮助的,而且是必须的,这里的对象的引用的作用域比它用到的要宽泛或者这是编程规范所需的。

这些例子包括了诸如使用一个静态域或者一个实例域存储一个
引用到一个临时的缓存中,而不是使用一个本地的变量(参见资源"Eye on performance: Referencing objects" ),或者是使用一个数组来存储引

用,这些引用可能是由JVM确定其可达性,而不是由程序实现的。考虑清单3中的类,它是一个通过数组实现的简单的弹出式栈。当pop()被调用,如
果没有显式的置空操作,可能引起内存泄漏(或者更适合叫做 "unintentional object retention," 或
"object loitering" ),因为对于stack[top+1]中的引用程序本身是认为不可达的,而垃圾回收器确认为是可达的。
[color=orange]清单 3 Avoiding object loitering in a stack implementation[/color]
public class SimpleBoundedStack {
private static final int MAXLEN = 100;
private Object stack[] = new Object[MAXLEN];
private int top = -1;

public void push(Object p) { stack [++top] = p;}

public Object pop() {
Object p = stack [top];
stack [top--] = null; // explicit null
return p;
}
}

在1997年9月 "Java Developer Connection Tech Tips" 中,Sun就警告对于上述列子中的显式置空时必须的。然而,不幸的是,编程者常常远离

这种忠告。在这种情况下使用显式的置空有助于垃圾回收器。
然而,有大量的实例,使用显式置空,不但不能帮助垃圾回收器,而且还可能对你的程序产生危害。
考虑清单4,它结合了多种坏的策略。清单中的程序实现了LinkedList,并且在程序的最后显式的实现
了finalize(),并在其中遍历了链表,并将链表中的元素设置为null。我们已经讨论了finalizer的危害。
这个例子其实更糟,因为它做了许多额外的工作,表面上是帮助了垃圾回收器,但实际上并没有帮助,甚至是危害了垃圾回收。遍历列表将引起一个

循环操作,同时由于是在finalizer中,那些对象可能已经死亡,而访问已经死亡的对象,将导致JVM将他们复制到缓存中。而如果是让垃圾回收器去

处理,可能会完
全避免这种操作,因为垃圾回收器根本不会访问死对象。显式的置空也不会帮助垃圾回收器的任何跟踪,如果一个列表的头指针被置空,则这个列表

的余下部分将不会被跟踪。
[color=orange]清单4 Combining finalizers and explicit nulling for a total performance disaster -- don't do this! [/color]
public class LinkedList {

private static class ListElement {
private ListElement nextElement;
private Object value;
}

private ListElement head;

...

public void finalize() {
try {
ListElement p = head;
while (p != null) {
p.value = null;
ListElement q = p.nextElement;
p.nextElement = null;
p = q;
}
head = null;
}
finally {
super.finalize();
}
}
}

显式的置空应该被用于那些由于破坏通常的作用域规则而引起的性能问题的情况。例如在清单3中栈的
例子(虽然正确,但是性能非常低,这种实现方可能会引起重新分配和当栈发生变化时会从新复制所有的元
素)

[color=green]显式的垃圾回收 (Explicit garbage collection) [/color]
第三种策略是开发者常错误的认为使用System.gc()是帮助垃圾回收器,因为当调用System.gc()时会触
发垃圾回收(事实上,那仅是一个建议,告诉JVM这是一个合适的时间进行垃圾回收)。不幸的是,System.gc()会触发一个全回收,它包括对所有对

象代的回收。那是非常大的一项工作。一般的,让系统决定是仅回收堆中的对象还是全回收,要比你自己做要好。大多数时间,进行一次小范围的回

收就会满足我
们的工作。更糟的是,调用System.gc()的操作被深深的掩埋,以至于开发者无法感觉到。如果你担心你的程序隐式的调用了System.gc()。你可以在

使用JVM时追加-XX:+DisableExplicitGC选项从而阻止通过
调用System.gc()方法触发垃圾回收。
永恒,又一次 (immutability, again)
如果没有永恒,那么我将不会完成Java theory and practice 系列文章,使对象不可变将会消除编程中整体类的错误。不使用不可变对象一个最

普遍的原因是通过这种方式可能会影响我们的程序性能。然
而,这种认识有些情况下是正确的,而有些情况却不是,使用不可变的对象不但不会影响程序的性能,还
会提高它们。
许多对象的功能是保存另一个对象。当保存的对象发生变化时,我们有两种选择:在我们的对象中更新这个对象的引用(像使用一个可变的容器类

)或者是重建一个对象用于存放新对象(像使用一个不可变的
容器类)。清单5展示了两种方式实现的简单的holder类。假设被包含的类是小的,这和我们大多数情况
一致(比如一个在Map中的Map.Entry元素或是LinkedList中的元素),为一个新的不可变对象分配内存
常隐含着性能上的优势,只是由于垃圾回收的策略引起的。

[color=orange]清单5 Mutable and immutable object holders[/color]
 public class MutableHolder {
private Object value;
public Object getValue() { return value; }
public void setValue(Object o) { value = o; }
}

public class ImmutableHolder {
private final Object value;
public ImmutableHolder(Object o) { value = o; }
public Object getValue() { return value; }
}


大多数情况下,当一个holder对象更新它包含的对象时,这个对象常常是一个新的对象,也就是我们
的年轻对象。如果我们使用MutableHolder类,并通过调用它的setValue()方法,我们将创建这样一种情
景,用一个老的对象去引用一个年轻的对象。另一方面,如果我们创建一个新的ImmutableHolder对象,
那么将会是用一个年轻的对象去引用一个老的对象。对于第二种情况,众多的新对象指向老的对象,只将
对垃圾回收器产生一种优雅的方式。如果一个位于老一代的MutableHolder对象是可变化的,那么所有被
包含在MutableHolder类的卡片中的对象必须在一次小的收集中被扫描。这就会在收集时刻增加垃圾回收
器的工作(参见上一期文章中关于卡片标记的介绍)。
当好的性能建议变成坏的 (When good performance advice goes bad )
在2003年7月Java Developer's Journal 中的封面故事阐述了当使用好的性能建议,但却未满足其
使用条件时,好的性能建议是如何变坏的。同时这篇文章还包含了一些有用的分析说明危害大于收益,(不
幸得是,有太多的面向性能的建议失足调入了同样的陷阱)。
这篇文章以一系列的实时的需求为开始,在文章的环境中一个不可预知的垃圾回收时间点是不能被接受
的。然后这篇文章的作者建议使用一些列的技术达到以上的目标,例如显式置空,对象池和一个计划的显
式的垃圾回收。到目前为止,还算好——他们有一个问题,并且他们指出了它,并去解决它。不幸的是,
这篇文章的题目("Avoid Bothersome Garbage Collection Pauses" )似乎说明在它之中的建议是
适用于所有的Java程序的,但事实并非如此,这些性能忠告恰恰可能是危险的。
对于大多数的程序,那些通过影响垃圾回收机制的技术常常会危害你程序的性能,更不用说你把它们
加入你的设计中了。实际情况是,这些建议往往依赖于特定的环境(如实时系统,嵌入式系统),如果不是在这些特定的环境中,它们将不会起到作

用。
总之,在你要为了提高性能时修改你的程序时,请先确定你的程序的确存在性能方面的问题。
总结
Java的垃圾回收已经走过了很长的路,现代的JVM通过更短的停等时间,来使它的垃圾回收工作更加有效。因为分配对象和垃圾回收代价已经极大

的被降低了,诸如像显式置空,对象池,这些以前被认为是可以提高性能的策略已经不再是那么的必要和有帮助的了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值