`
san_yun
  • 浏览: 2662960 次
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

编写内存效率的java代码-面向GC

 
阅读更多

参考两个PPT

http://www.slideshare.net/cnbailey/memory-efficient-java
http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java-tutorial.pdf

 

原文: 沐剑

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虚拟机结构与机制 这篇文章,本文就不再赘述了,本文也并不关注具体的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的性能,类似下面的代码:

List<String> list = new ArrayList<String>();
// some code
list = null; // help GC

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

我记得几年前撒迦在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(),可以参考毕玄的几篇文章:

六、指定容器初始化大小

Java容器的一个特点就是可以动态扩展,所以通常我们都不会去考虑初始大小的设置,不够了反正会自动扩容呗。

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

例如一些基于数组的数据结构,例如StringBuilderStringBufferArrayListHashMap等等,在扩容的时候都需要做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更好地合作。

分享到:
评论

相关推荐

    JAVA中的面向对象与内存解析

    在Java编程语言中,面向对象(Object-...同时,理解内存解析,特别是堆和栈内存的工作原理,对于编写高效、无内存泄漏的代码至关重要。在实际编程中,结合这些知识,我们可以设计出更加健壮和高效的Java应用程序。

    《精通JAVA----JDK》

    14. **垃圾回收(GC)与内存管理**:理解Java如何自动管理内存,以及如何使用内存分析工具监控和优化内存使用,是防止内存泄漏和提升性能的重要环节。 以上就是《精通JAVA----JDK》这本书可能涵盖的部分关键知识点...

    JAVA-JVM-全面/发展史/GC.zip

    Java是一种广泛使用的面向对象的编程语言,以其“一次编写,到处运行”的特性闻名。JVM(Java虚拟机)是Java程序执行的核心,它允许开发者在任何支持Java的平台上运行代码,而无需关心底层硬件的差异。这个压缩包...

    Android代码-Java-Interview

    学习Java内存模型,理解堆和栈的区别,以及如何避免内存泄漏和减少GC压力。在Android中,合理地管理内存可以提升应用性能和用户体验。 IO流是数据输入输出的关键,包括文件操作和网络通信。理解流的分类(如字节流...

    [JAVA·初级]GC垃圾回收机制编程开发技术共14页.p

    在Java中,内存管理主要依赖于垃圾回收器(Garbage Collector,简称GC),它自动回收不再使用的对象所占用的内存空间,以避免内存泄漏。 垃圾回收机制的核心目标是识别并清理不再被程序引用的对象。GC通过一系列...

    java面试java-interview-guide-master.zip

    - 异常分类:了解Exception和Error的区别,以及如何编写异常处理代码。 - 自定义异常:如何定义和抛出自定义异常。 7. **JVM内存管理**: - 内存区域:熟悉堆、栈、方法区、本地方法栈和程序计数器的划分。 - ...

    java面试-BAT个人真实java面试精华题-面经

    - 学习如何调整JVM参数以优化性能,如内存分配、GC策略选择。 - 了解JVM故障排查工具,如jmap、jstat、jvisualvm等。 通过深入学习以上知识点,并结合实际面试经验,可以有效提高Java面试的成功率,为在BAT或其他...

    java代码-03 李志清

    8. **JVM和内存管理**:了解Java虚拟机(JVM)的工作原理,包括类加载、垃圾回收机制(GC),以及如何优化内存使用,有助于提升程序性能。 9. **标准库和API**:Java提供丰富的标准库,如JavaFX用于图形用户界面,...

    【电子版】校招面试题库(附答案与解析)java篇-破解密码.pdf

    - 关键字:Java的关键字是预定义的,具有特殊含义的标识符,如`public`, `private`, `protected`, `final`, `static`, `void`等,了解并能熟练运用这些关键字是编写高效代码的关键。 - 面向对象:Java是一种面向...

    java代码转c#

    标题"java代码转c#"指的就是这个过程,即把用Java编写的程序转换成C#语言。这个过程可以手动进行,也可以借助一些自动化工具,如Demo_Java_to_CSharp_Converter这样的工具,它可能是一个能够帮助开发者进行代码转换...

    Java语言程序设计(第二版)-源代码-贾振华

    《Java语言程序设计(第二版)》是贾振华教授编写的一本深入讲解Java编程的教材,这本书旨在帮助读者掌握Java编程的基础与进阶技能。源代码是学习编程书籍的重要辅助资源,它允许读者直接查看并运行书中示例,加深对...

    Java面经-百度准入职老哥整理(八股文)

    在准备面试时,除了理论知识,还需要结合实践,通过编写代码和解决实际问题来提高自己的技术水平。对于“Java面经-百度准入职老哥整理(八股文之四).pdf”这样的资料,建议仔细阅读,模拟面试场景进行自我测试,以便...

    java-virtual-machine-neutral

    - **替代的垃圾收集器**:随着Java版本的发展,引入了更多高级的垃圾收集器,例如Parallel Copy GC和Concurrent Mark-Sweep GC。这些收集器旨在减少垃圾回收过程中对应用程序性能的影响。 #### 三、内存管理与Java...

    JAVA 面经--JVM,spring框架,分布式,数据库

    理解IoC容器如何管理对象及其生命周期,以及AOP如何实现横切关注点,将帮助你在实际开发中编写更简洁、可维护的代码。Spring MVC、Spring Boot、Spring Data JPA等模块也是面试中的常见问题,你需要熟悉它们的功能和...

    java代码编写建议

    编写Java代码时,应考虑到最终用户或调用者的需求。这意味着代码应该易于理解和使用,避免不必要的复杂性。同时,应提供清晰的文档和示例,帮助用户正确地使用API或库。 #### 六、字符串处理 在处理字符串时,应...

    java代码-51. 3-2

    7. **JVM与内存管理**:Java虚拟机(JVM)是运行Java代码的平台,它负责垃圾回收(GC),自动管理内存空间,防止内存泄漏。 8. **反射**:Java反射API允许在运行时检查类的信息,如类名、方法、字段等,并能动态...

    java代码-06 赵搏辉

    此外,它可能包含对其他类或库的引用,展示了Java面向对象编程的特点,如继承、封装和多态。学习这个文件,我们可以深入理解Java的基础语法、类设计原则和实际编程技巧。 2. `README.txt`: 这是一个文本文件,通常...

    java-SE-马士兵笔记word

    - **垃圾回收机制(GC)**:自动管理内存,减少程序员因手动管理内存带来的错误。 - **Java编译和运行流程**: - 源程序(`.java`文件)通过Java编译器编译成字节码(`.class`文件)。 - 字节码文件由类装载器...

    java-sample-5_java_

    8. **垃圾回收(GC)**:Java的自动内存管理机制,负责清理不再使用的对象,防止内存泄漏。 9. **标准库**:Java标准库(Java API)提供了大量的预定义类和接口,涵盖了从基本类型操作到网络编程的各种功能。 10. ...

Global site tag (gtag.js) - Google Analytics