面向GC的Java编程

转自http://hellojava.info/?p=341

 

HelloJava微信公众帐号网站

javaString

面向GC的Java编程

这是内部一个同事(沐剑)写的文章,国外有一家专门作Java性能优化的公司,而且它主要关注Java内存使用的优化,重点是数据结构的选择,优化效果很是明显,因此其实一个优秀的Java程序员和一个普通的Java程序员产出的东西差距是至关大的。php

Java程序员在编码过程当中一般不须要考虑内存问题,JVM通过高度优化的GC机制大部分状况下都可以很好地处理堆(Heap)的清理问题。以致于许多Java程序员认为,我只须要关心什么时候建立对象,而回收对象,就交给GC来作吧!甚至有人说,若是在编程过程当中频繁考虑内存问题,是一种退化,这些事情应该交给编译器,交给虚拟机来解决。html

这话其实也没有太大问题,的确,大部分场景下关心内存、GC的问题,显得有点“杞人忧天”了,高老爷说过:java

> 过早优化是万恶之源。程序员

但另外一方面,**什么才是“过早优化”?**算法

> If we could do things right for the first time, why not?数据库

事实上**JVM的内存模型**( [JMM])理应是Java程序员的基础知识,处理过几回JVM线上内存问题以后就会很明显感觉到,**不少系统问题,都是内存问题**。编程

对JVM内存结构感兴趣的同窗能够看下 [浅析Java虚拟机结构与机制](http://blog.hesey.net/2011/04/introduction-to-java-virtual-machine.html) 这篇文章,本文就再也不赘述了,本文也并不关注具体的GC算法,相关的文章汗牛充栋,随时可查。数组

另外,不要期望GC优化的这些技巧,能够对应用性能有成倍的提升,特别是对I/O密集型的应用,或是实际落在YoungGC上的优化,可能效果只是帮你减小那么一点YoungGC的频率。缓存

但我认为,**优秀程序员的价值,不在于其所掌握的几招屠龙之术,而是在细节中见真著**,就像前面说的,**若是咱们能够一次把事情作对,而且作好,在容许的范围内尽量追求卓越,为何不去作呢**?tomcat

### 1、GC分代的基本假设 ###

大部分GC算法,都将堆内存作分代(Generation)处理,可是为何要分代呢,又为何不叫内存分区、分段,而要用面向时间、年龄的“代”来表示不一样的内存区域?

GC分代的**基本假设**是:

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

而这些短命的对象,偏偏是GC算法须要首先关注的。因此在大部分的GC中,YoungGC(也称做MinorGC)占了绝大部分,对于负载不高的应用,可能跑了数个月都不会发生FullGC。

基于这个前提,在编码过程当中,咱们应该**尽量地缩短对象的生命周期**。在过去,分配对象是一个比较重的操做,因此有些程序员会尽量地减小new对象的次数,尝试减少堆的分配开销,减小内存碎片。

可是,短命对象的建立在JVM中比咱们想象的性能更好,因此,不要吝啬new关键字,大胆地去new吧。

固然前提是不作无谓的建立,对象建立的速率越高,那么GC也会越快被触发。

结论:

> 分配小对象的开销分享小,不要吝啬去建立。

> GC最喜欢这种小而短命的对象。

> 让对象的生命周期尽量短,例如在方法体内建立,使其能尽快地在YoungGC中被回收,不会晋升(romote)到年老代(Old Generation)。

### 2、对象分配的优化 ###

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

TLAB占用的空间在Eden Generation。

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

当对象大到Eden Generation放不下时,JVM只能尝试去Old Generation分配,这种状况须要尽量避免,由于一旦在Old Generation分配,这个对象就只能被Old Generation的GC或是FullGC回收了。

### 3、不可变对象的好处 ###

GC算法在扫描存活对象时一般须要从ROOT节点开始,扫描全部存活对象的引用,构建出对象图。

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

能够想象一下,若是存在Old Generation的对象引用了Young Generation的对象,那么在每次YoungGC的过程当中,就必须考虑到这种状况。

Hotspot JVM为了提升YoungGC的性能,避免每次YoungGC都扫描Old Generation中的对象引用,采用了 **卡表(Card Table)** 的方式。

简单来讲,当Old Generation中的对象发生对Young Generation中的对象产生新的引用关系或释放引用时,都会在卡表中响应的标记上标记为脏(dirty),而YoungGC时,只须要扫描这些dirty的项就能够了。

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

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

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

### 4、引用置为null的传说 ###

早期的不少Java资料中都会提到在方法体中将一个变量置为null可以优化GC的性能,相似下面的代码:

“`java
List list = new ArrayList();
// some code
list = null; // help GC
“`

事实上这种作法对GC的帮助微乎其微,有时候反而会致使代码混乱。

我记得几年前 @rednaxelafx 在HLL VM小组中详细论述过这个问题,原帖我没找到,结论基本就是:

> 在一个很是大的方法体内,对一个较大的对象,将其引用置为null,某种程度上能够帮助GC。

> 大部分状况下,这种行为都没有任何好处。

因此,仍是早点放弃这种“优化”方式吧。

GC比咱们想象的更聪明。

### 5、手动档的GC ###

在不少Java资料上都有下面两个奇技淫巧:

> 经过`Thread.yield()`让出CPU资源给其它线程。

> 经过`System.gc`()触发GC。

事实上JVM从不保证这两件事,而`System.gc`()在JVM启动参数中若是容许显式GC,则会**触发FullGC**,对于响应敏感的应用来讲,几乎等同于自杀。

So,让咱们牢记两点:

> Never use `Thread.yield()`。

> Never use `System.gc`()。除非你真的须要回收Native Memory。

第二点有个Native Memory的例外,若是你在如下场景:

– 使用了NIO或者NIO框架(Mina/Netty)

– 使用了DirectByteBuffer分配字节缓冲区

– 使用了MappedByteBuffer作内存映射

因为**Native Memory只能经过FullGC(或是CMS GC)回收**,因此除非你很是清楚这时真的有必要,不然不要轻易调用`System.gc`(),且行且珍惜。

另外为了防止某些框架中的`System.gc`调用(例如NIO框架、Java RMI),建议在启动参数中加上`-XX:+DisableExplicitGC`来禁用显式GC。

这个参数有个巨大的坑,若是你禁用了`System.gc`(),那么上面的3种场景下的内存就没法回收,可能形成OOM,若是你使用了CMS GC,那么能够用这个参数替代:`-XX:+ExplicitGCInvokesConcurrent`。

关于`System.gc`(),能够参考 @bluedavy 的几篇文章:

– [CMS GC会不会回收Direct ByteBuffer的内存]

– [说说在Java启动参数上我犯的错]

– [java.lang.OutOfMemoryError:Map failed]

### 6、指定容器初始化大小 ###

Java容器的一个特色就是能够动态扩展,因此一般咱们都不会去考虑初始大小的设置,不够了反正会自动扩容呗。

可是扩容不意味着没有代价,甚至是很高的代价。

例如一些基于数组的数据结构,例如`StringBuilder`、`StringBuffer`、`ArrayList`、`HashMap`等等,在扩容的时候都须要作ArrayCopy,对于不断增加的结构来讲,通过若干次扩容,会存在大量无用的老数组,而回收这些数组的压力,全都会加在GC身上。

这些容器的构造函数中一般都有一个能够指定大小的参数,若是对于某些大小能够预估的容器,建议加上这个参数。

但是由于容器的扩容并非等到容器满了才扩容,而是有必定的比例,例如`HashMap`的扩容阈值和负载因子(loadFactor)相关。

Google Guava框架对于容器的初始容量提供了很是便捷的工具方法,例如:

“`java
Lists.newArrayListWithCapacity(initialArraySize);

Lists.newArrayListWithExpectedSize(estimatedSize);

Sets.newHashSetWithExpectedSize(expectedSize);

Maps.newHashMapWithExpectedSize(expectedSize);
“`

这样咱们只要传入预估的大小便可,容量的计算就交给Guava来作吧。

反例:

> 若是采用默认无参构造函数,建立一个ArrayList,不断增长元素直到OOM,那么在此过程当中会致使:
> – 屡次数组扩容,从新分配更大空间的数组
> – 屡次数组拷贝
> – 内存碎片

### 7、对象池 ###

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

可是对象池中的对象因为在运行期长期存活,大部分会晋升到Old Generation,所以没法经过YoungGC回收。

而且一般……没有什么效果。

对于对象自己:

> 若是对象很小,那么分配的开销原本就小,对象池只会增长代码复杂度。

> 若是对象比较大,那么晋升到Old Generation后,对GC的压力就更大了。

从线程安全的角度考虑,一般池都是会被并发访问的,那么你就须要处理好同步的问题,这又是一个大坑,而且**同步带来的开销,未必比你从新建立一个对象小**。

对于对象池,惟一合适的场景就是**当池中的每一个对象的建立开销很大**时,缓存复用才有意义,例如每次new都会建立一个链接,或是依赖一次RPC。

好比说:

> – 线程池
> – 数据库链接池
> – TCP链接池

即便你真的须要实现一个对象池,也请使用成熟的开源框架,例如Apache Commons Pool。

另外,使用JDK的ThreadPoolExecutor做为线程池,不要重复造轮子,除非当你看过AQS的源码后认为你能够写得比Doug Lea更好。

### 8、对象做用域 ###

尽量缩小对象的做用域,即生命周期。

> 若是能够在方法内声明的局部变量,就不要声明为实例变量。

> 除非你的对象是单例的或不变的,不然尽量少地声明static变量。

### 9、各种引用 ###

`java.lang.ref.Reference`有几个子类,用于处理和GC相关的引用。JVM的引用类型简单来讲有几种:

– Strong Reference,最多见的引用
– Weak Reference,当没有指向它的强引用时会被GC回收
– Soft Reference,只当临近OOM时才会被GC回收
– Phantom Reference,主要用于识别对象被GC的时机,一般用于作一些清理工做

当你须要实现一个缓存时,能够考虑优先使用`WeakHashMap`,而不是`HashMap`,固然,更好的选择是使用框架,例如Guava Cache。

最后,再次提醒,以上的这些未必能够对代码有多少性能上的提高,可是熟悉这些方法,是为了帮助咱们写出更卓越的代码,和GC更好地合做。

——————————————-
对于编写GC友好或者更好的说法是memory efficient的Java应用,个人建议是:
1.从咱们的应用来看,内存分配的主要浪费是集中在了自增加数据结构上,因此在使用自增加数据结构时,要尽量设置合理的初始化大小,感兴趣的同窗能够去查下jrockit的StringMaker,这个是一个优化的典型case;

2.数据结构的选择,不一样数据结构对内存的占用差异是很是大的,这个你们能够去google一个ppt(Building Memory-efficient Java Applications),还有一个JavaOne 2013上面的Memory Efficient Java的PPT也建议你们看看;

3.还有不少的小技巧,例如尽量避免autobox形成的内存浪费,例如int–>Integer形成的new对象,这个感兴趣的能够去找下JavaOne 2010年时候的一个关于auto box/unbox的case代码。

最后推荐下同事的blog,有不少料:
http://hesey.net

=============================
题图来源于上面推荐的PPT中的内容。
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和你们一块儿讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。

This entry was posted in  Java and tagged  Memory Efficient on  2014-05-07.

Post navigation

← 阿里版JVM高压力下表现不够好的Java应用 

Leave a Reply

Your email address will not be published. Required fields are marked *

验证码图片
看不清,换一个

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

相关文章
相关标签/搜索