面向GC的Java编程

2016/3/4 posted in  Java

GC分代的基本假设

GC分代的基本假设是:

绝大部分对象的生命周期都非常短暂,存活时间短。

基于这个前提,在编码过程中,我们应该尽可能地缩短对象的生命周期。

结论:

  • 分配小对象的开销非常小,不要吝啬去创建
  • GC最喜欢这种小而短命的对象
  • 让对象的生命周期尽可能短,例如在方法体内创建,使其能尽快地在YoungGC中被回收,不会晋升到年老代(Old Generation

对象分配的优化

基于大部分对象都是小而短命,并且不存在多线程的数据竞争。这些小对象的分配,会优先在线程私有的TLAB中分配,TLAB中创建的对象,不存在锁甚至是CAS的开销。

TLAB占用的空间在Eden Generation

当对象比较大,TLAB的空间不足以放下,而JVM又认为当前线程占用的TLAB剩余空间还足够时,就会直接在Eden Generation上分配,此时是存在并发竞争的,所以会有CAS的开销,但是也还好。

当对象大到Eden Generation放不下时,JVM只能尝试去Old Generation分配,这种情况需要尽可能避免,因为一旦在Old Generation上分配,这个对象只能被Old GenerationGC或者是Full GC回收了。

不可变对象的好处

GC算法在扫描存活对象时通常需要从ROOT节点开始,扫描所有存活对象的引用,构建出对象图。

不可变对象对GC的优化,主要体现在Old Generation中。

如果存在Old Generation的对象引用了Young Generation的对象,那么在每次YoungGC的过程中,就必须考虑到这种情况。

卡表(Card Table)

Old Generation中的对象发生对Young Generation中的对象产生新的引用关系或释放引用时,都会在卡表中相应的标记上标为脏(dirty);而当YoungGC时,只需要扫描这些dirty的项就可以了。

可变对象对其他对象的引用关系可能会频繁变化,并且有可能在运行过程中持有越来越多的引用,特别是容器。这些都会导致对应的卡表项被频繁标记为dirty

而不可变对象的引用关系非常稳定,在扫描卡表时就不会扫到它们对应的项了。

这里的不可变对象,不是指仅仅自身引用不可变的final对象,而是真正的Immutable Objects

指定容器初始化大小

如果采用默认无参构造函数,构建一个ArrayList,不断增加元素知道OOM,那么在此过程中会导致:

  • 多次数组扩容,重新分配更大空间的数组
  • 多次数组拷贝
  • 内存碎片

对象池

为了减少对象分配开销,提高性能,可能有人会采用对象池的方式来缓存对象集合,作为复用的手段。

但是对象池中的对象由于在运行期长期存活,大部分会晋升到Old Generation,因此无法通过YoungGC回收。

可以借助成熟的开源框架,例如Apache Commons Pool

对象作用域

尽可能缩小对象的作用域,即生命周期

  • 如果可以在方法内声明的局部变量,就不要声明为实例变量
  • 除非你的对象是单例的或者不变的,否则尽可能少地声明static变量

Reference

http://coolshell.cn/articles/11541.html