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

HelloJava微信公众账号网站

面向GC的Java编程

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

Java程序员在编码过程中通常不需要考虑内存问题,JVM经过高度优化的GC机制大部分情况下都能够很好地处理堆(Heap)的清理问题。以至于许多Java程序员认为,我只需要关心何时创建对象,而回收对象,就交给GC来做吧!甚至有人说,如果在编程过程中频繁考虑内存问题,是一种退化,这些事情应该交给编译器,交给虚拟机来解决。

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

> 过早优化是万恶之源。

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

> 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的频率。

但我认为,**优秀程序员的价值,不在于其所掌握的几招屠龙之术,而是在细节中见真著**,就像前面说的,**如果我们可以一次把事情做对,并且做好,在允许的范围内尽可能追求卓越,为什么不去做呢**?

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

大部分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**。

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

早期的很多Java资料中都会提到在方法体中将一个变量置为null能够优化GC的性能,类似下面的代码:

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

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

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

> 在一个非常大的方法体内,对一个较大的对象,将其引用置为null,某种程度上可以帮助GC。

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

所以,还是早点放弃这种“优化”方式吧。

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 的几篇文章:

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

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

– [java.lang.OutOfMemoryError:Map failed]

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

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,那么在此过程中会导致:
> – 多次数组扩容,重新分配更大空间的数组
> – 多次数组拷贝
> – 内存碎片

### 七、对象池 ###

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

但是对象池中的对象由于在运行期长期存活,大部分会晋升到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更好地合作。

——————————————-
对于编写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 *

Name *

Email *

Website

验证码*

Comment

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>

欢迎关注微信公众号

Search for: 

RECENT POSTS

RECENT COMMENTS

ARCHIVES

CATEGORIES

TAGS

面向GC的Java编程的更多相关文章

  1. 面向GC的Java编程(转)

    转自:http://blog.hesey.net/2014/05/gc-oriented-java-programming.html Java程序员在编码过程中通常不需要考虑内存问题,JVM经过高度优 ...

  2. 【转】面向GC的Java编程

    Java程序员在编码过程中通常不需要考虑内存问题,JVM经过高度优化的GC机制大部分情况下都能够很好地处理堆(Heap)的清理问题.以至于许多Java程序员认为,我只需要关心何时创建对象,而回收对象, ...

  3. 来测试下你的Java编程能力

    上篇整理了下后面准备更系统化写的Java编程进阶的思路,如果仅看里面的词,很多同学会觉得都懂,但我真心觉得没有多少人是真懂的,所以简单的想了一些题目,感兴趣的同学们可以来做做看,看看自己的Java编程 ...

  4. JAVA编程思想——分析阅读

    需要源码.JDK1.6 .编码风格参考阿里java规约 7/12开始 有点意识到自己喜欢理论大而泛的模糊知识的学习,而不喜欢实践和细节的打磨,是因为粗心浮躁导致的么? cron表达式使用 设计能力.领 ...

  5. Java编程中“为了性能”尽量要做到的一些地方

    最近的机器内存又爆满了,除了新增机器内存外,还应该好好review一下我们的代码,有很多代码编写过于随意化,这些不好的习惯或对程序语言的不了解是应该好好打压打压了. 下面是参考网络资源总结的一些在Ja ...

  6. JAVA编程“性能说”(java编程需要做的26件事)

    转载于 http://www.csdn.net/article/2012-06-01/2806249 最近的机器内存又爆满了,除了新增机器内存外,还应该好好review一下我们的代码,有很多代码编写过 ...

  7. Java编程思想——初始化与清理

    PS:最近一直忙于项目开发..所以一直没有写博客..趁着空闲期间来一发.. 学习内容: 1.初始化 2.清理 1.初始化   虽然自己的Java基础还是比较良好的..但是在解读编程思想的时候还是发现了 ...

  8. java 编程时候的性能调优

    一.避免在循环条件中使用复杂表达式 在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快. 例子: import java.util ...

  9. 完成《Java编程入门》初稿

    Java编程入门 现在的运维工程师不但要懂得集合网络.系统管理而且要和开发人员一起调试系统,社会上也需要"复合性"的运维人员,所以需要做运维的也要懂一些开发,知道软件系统接口的调试 ...

随机推荐

  1. 一个初学者对CLSA.NET框架的使用心得

    什么是CSLA.NET框架? 今天在一个群里,有人问我什么是CSLA.NET,CSLA是Component-based, Scalable, Logical Architecture的简写,CSLA ...

  2. CSS布局模型思考

    flow模型:默认布局模型,元素从左向右.从上到下依次排列,块状元素独占一行.Position属性对应值static. float模型:主要效果是让本来独占一行的块状元素变成内联-块状元素,并到一排显 ...

  3. js浏览器键盘事件控制(转自新浪微博)

    js键盘事件全面控制 主要分四个部分第一部分:浏览器的按键事件第二部分:兼容浏览器第三部分:代码实现和优化第四部分:总结 第一部分:浏览器的按键事件 用js实现键盘记录,要关注浏览器的三种按键事件类型 ...

  4. ie8 background css没有显示?——都是空格惹的祸

    ie8 background css没有显示?——都是空格惹的祸

  5. nginx 环境搭建使用之入门

    1.http://nginx.org/下载最新的nginx 现在最新的版本是nginx-1.9.1   下载.tar.gz包 ,解压. timeless@timeless-HP-Pavilion-g4 ...

  6. python【第三篇】函数

    内容大纲: 1.函数基本语法与特性 2.参数与局部变量 3.返回值 4.递归 5.匿名函数lambda 6.函数式编程介绍 7.高阶函数 8.内置函数 1.函数基本语法与特性 函数的定义:函数是指将一 ...

  7. Execl DataTime Format Number

    Excel 中日期类型所保存的值是数值型.只是设置了为日期格式,通过公式转换从而得出我们平时常用的日期内容.也很好理解这公式所要说明的意思.数值是个浮点型可以分成2部分看.整数部分:年月日(日期)小数 ...

  8. uboot总结:uboot配置和启动过程3(config.mk分析)

    说明:文件位置:在uboot的目录下,文件名为:config.mk.是一个makefile文件,以后会被主Makefile调用. 它的主要作用的是: (1)具体的设置交叉编译工具链接(主Makefil ...

  9. windows phone 8学习 - 选择器

    1照相机 CameraCaptureTask cct=new CameraCaptureTask(); cct.Completed+=new EventHandler<PhotoResult&g ...

  10. 【Java】Java垃圾回收机制

    Java垃圾回收机制 说到垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它和Java联系起来.在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给 ...