深入理解Java虚拟机(二)面向GC的Java编程
基本观点:过早优化是万恶之源。
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 list =newArrayList(); // some code
list =null;// help GC
事实上这种做法对GC
的帮助微乎其微,有时候反而会导致代码混乱。
结论:
GC
比我们想象的更聪明。
在一个非常大的方法体内,对一个较大的对象,将其引用置为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
。
指定容器初始化大小
Java容器的一个特点就是可以动态扩展,所以通常我们都不会去考虑初始大小的设置,不够了反正会自动 扩容呗。
但是扩容不意味着没有代价,甚至是很高的代价。
对象池
为了减少对象分配开销,提高性能,可能有人会采取对象池的方式来缓存对象集合,作为复用的手段。
但是对象池中的对象由于在运行期长期存活,大部分会晋升到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
。
相关文章推荐
深入理解Java虚拟机(一)知识储备
https://blog.csdn.net/shang_xs/article/details/88122304
深入理解Java虚拟机(二)面向GC的Java编程
https://blog.csdn.net/shang_xs/article/details/88072885
深入理解Java虚拟机(三)GC优化实战
https://blog.csdn.net/shang_xs/article/details/88052906