转自:http://blog.hesey.net/2014/05/gc-oriented-java-programming.htmlhtml
Java程序员在编码过程当中一般不须要考虑内存问题,JVM通过高度优化的GC机制大部分状况下都可以很好地处理堆(Heap)的清理问题。以致于许多Java程序员认为,我只须要关心什么时候建立对象,而回收对象,就交给GC来作吧!甚至有人说,若是在编程过程当中频繁考虑内存问题,是一种退化,这些事情应该交给编译器,交给虚拟机来解决。java
这话其实也没有太大问题,的确,大部分场景下关心内存、GC的问题,显得有点“杞人忧天”了,高老爷说过:程序员
过早优化是万恶之源。算法
但另外一方面,什么才是“过早优化”?数据库
If we could do things right for the first time, why not?编程
事实上JVM的内存模型( JMM )理应是Java程序员的基础知识,处理过几回JVM线上内存问题以后就会很明显感觉到,不少系统问题,都是内存问题。数组
对JVM内存结构感兴趣的同窗能够看下 浅析Java虚拟机结构与机制 这篇文章,本文就再也不赘述了,本文也并不关注具体的GC算法,相关的文章汗牛充栋,随时可查。缓存
另外,不要期望GC优化的这些技巧,能够对应用性能有成倍的提升,特别是对I/O密集型的应用,或是实际落在YoungGC上的优化,可能效果只是帮你减小那么一点YoungGC的频率。安全
但我认为,优秀程序员的价值,不在于其所掌握的几招屠龙之术,而是在细节中见真著,就像前面说的,若是咱们能够一次把事情作对,而且作好,在容许的范围内尽量追求卓越,为何不去作呢?数据结构
大部分GC算法,都将堆内存作分代(Generation)处理,可是为何要分代呢,又为何不叫内存分区、分段,而要用面向时间、年龄的“代”来表示不一样的内存区域?
GC分代的基本假设是:
绝大部分对象的生命周期都很是短暂,存活时间短。
而这些短命的对象,偏偏是GC算法须要首先关注的。因此在大部分的GC中,YoungGC(也称做MinorGC)占了绝大部分,对于负载不高的应用,可能跑了数个月都不会发生FullGC。
基于这个前提,在编码过程当中,咱们应该尽量地缩短对象的生命周期。在过去,分配对象是一个比较重的操做,因此有些程序员会尽量地减小new对象的次数,尝试减少堆的分配开销,减小内存碎片。
可是,短命对象的建立在JVM中比咱们想象的性能更好,因此,不要吝啬new关键字,大胆地去new吧。
固然前提是不作无谓的建立,对象建立的速率越高,那么GC也会越快被触发。
结论:
分配小对象的开销很是小,不要吝啬去建立。
GC最喜欢这种小而短命的对象。
让对象的生命周期尽量短,例如在方法体内建立,使其能尽快地在YoungGC中被回收,不会晋升(romote)到年老代(Old Generation)。
基于大部分对象都是小而短命,而且不存在多线程的数据竞争。这些小对象的分配,会优先在线程私有的 TLAB 中分配,TLAB中建立的对象,不存在锁甚至是CAS的开销。
TLAB占用的空间在Eden Generation。
当对象比较大,TLAB的空间不足以放下,而JVM又认为当前线程占用的TLAB剩余空间还足够时,就会直接在Eden Generation上分配,此时是存在并发竞争的,因此会有CAS的开销,但也还好。
当对象大到Eden Generation放不下时,JVM只能尝试去Old Generation分配,这种状况须要尽量避免,由于一旦在Old Generation分配,这个对象就只能被Old Generation的GC或是FullGC回收了。
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。
早期的不少Java资料中都会提到在方法体中将一个变量置为null可以优化GC的性能,相似下面的代码:
List<String> list = new ArrayList<String>();
// some code
list = null; // help GC事实上这种作法对GC的帮助微乎其微,有时候反而会致使代码混乱。
我记得几年前 @rednaxelafx 在HLL VM小组中详细论述过这个问题,原帖我没找到,结论基本就是:
在一个很是大的方法体内,对一个较大的对象,将其引用置为null,某种程度上能够帮助GC。
大部分状况下,这种行为都没有任何好处。
因此,仍是早点放弃这种“优化”方式吧。
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 的几篇文章:
Java容器的一个特色就是能够动态扩展,因此一般咱们都不会去考虑初始大小的设置,不够了反正会自动扩容呗。
可是扩容不意味着没有代价,甚至是很高的代价。
例如一些基于数组的数据结构,例如StringBuilder、StringBuffer、ArrayList、HashMap等等,在扩容的时候都须要作ArrayCopy,对于不断增加的结构来讲,通过若干次扩容,会存在大量无用的老数组,而回收这些数组的压力,全都会加在GC身上。
这些容器的构造函数中一般都有一个能够指定大小的参数,若是对于某些大小能够预估的容器,建议加上这个参数。
但是由于容器的扩容并非等到容器满了才扩容,而是有必定的比例,例如HashMap的扩容阈值和负载因子(loadFactor)相关。
Google Guava框架对于容器的初始容量提供了很是便捷的工具方法,例如:
Lists.newArrayListWithCapacity(initialArraySize);
Lists.newArrayListWithExpectedSize(estimatedSize);
Sets.newHashSetWithExpectedSize(expectedSize);
Maps.newHashMapWithExpectedSize(expectedSize);这样咱们只要传入预估的大小便可,容量的计算就交给Guava来作吧。
反例:
若是采用默认无参构造函数,建立一个ArrayList,不断增长元素直到OOM,那么在此过程当中会致使:
屡次数组扩容,从新分配更大空间的数组
屡次数组拷贝
内存碎片
为了减小对象分配开销,提升性能,可能有人会采起对象池的方式来缓存对象集合,做为复用的手段。
可是对象池中的对象因为在运行期长期存活,大部分会晋升到Old Generation,所以没法经过YoungGC回收。
而且一般……没有什么效果。
对于对象自己:
若是对象很小,那么分配的开销原本就小,对象池只会增长代码复杂度。
若是对象比较大,那么晋升到Old Generation后,对GC的压力就更大了。
从线程安全的角度考虑,一般池都是会被并发访问的,那么你就须要处理好同步的问题,这又是一个大坑,而且同步带来的开销,未必比你从新建立一个对象小。
对于对象池,惟一合适的场景就是当池中的每一个对象的建立开销很大时,缓存复用才有意义,例如每次new都会建立一个链接,或是依赖一次RPC。
好比说:
· 线程池
· 数据库链接池
· TCP链接池即便你真的须要实现一个对象池,也请使用成熟的开源框架,例如Apache Commons Pool。
另外,使用JDK的ThreadPoolExecutor做为线程池,不要重复造轮子,除非当你看过AQS的源码后认为你能够写得比Doug Lea更好。
尽量缩小对象的做用域,即生命周期。
若是能够在方法内声明的局部变量,就不要声明为实例变量。
除非你的对象是单例的或不变的,不然尽量少地声明static变量。
java.lang.ref.Reference有几个子类,用于处理和GC相关的引用。JVM的引用类型简单来讲有几种:
· Strong Reference,最多见的引用
· Weak Reference,当没有指向它的强引用时会被GC回收
· Soft Reference,只当临近OOM时才会被GC回收
· Phantom Reference,主要用于识别对象被GC的时机,一般用于作一些清理工做当你须要实现一个缓存时,能够考虑优先使用WeakHashMap,而不是HashMap,固然,更好的选择是使用框架,例如Guava Cache。
最后,再次提醒,以上的这些未必能够对代码有多少性能上的提高,可是熟悉这些方法,是为了帮助咱们写出更卓越的代码,和GC更好地合做。