做一个吝啬的Java程序员——面向GC的编程

时间:2023-02-11 16:27:24

原文链接:BAT直通车 ——做一个吝啬的Java程序员——面向GC的编程

PS:欢迎访问BAT直通车获取BAT老司机最新经验

前言

相比于C++开发Java开发要轻松的多,因为程序员不必关心比较trick的内存问题。JVM高度优化的GC机制能够保证在绝大多数的情况下,系统能够很好的处理内存问题。但完全依靠GC、编程中不注意相关使用细节,程序往往打不到理想的性能,所以要做一个吝啬的Java程序员,编程的时候好好思考内存、垃圾回收相关点,有助于提升程序的性能。下面会罗列在工作中经常考虑的点。


1.容器初始化要指定大小

看到大部分的程序在使用容器、StringBuilder、StringBuffer的时候都是直接使用,没有指定初始大小,这是菜鸟的体现。从内存、垃圾回收角度出发,这些容器和类在使用的时候一定要给定初始大小。

正是因为上面提到的容器、类可以动态扩展,所以通常我们都不会去考虑设置初始大小,反正不够了会自动扩容。

但扩容是有代价,是有很高的内存代价的。大部分的扩容都是进行copy的,将老的数据重新copy一份加到扩容后的新的结构中,对于不断扩容的情况,将会产生大量的无用的老的数组,这些数组占据内存,也会增加GC的压力。对于那些内存抖动比较厉害的,往往是发生了这种情况。

所以在使用上述这些容器、类的时候在构造函数中建议加上容量的预估值。


2.少用丑陋的引用置为null的方法

很多时候,我们会看到有人确实是面向GC的编程,在对象不用后将对象置为null,但这个真的有用吗?

List<MyObject> alist = new ArrayList<MyObject>();
....
...
alist = null;

MyBigObject myobj = new MyBigObject();
...
myobj = null;

答案是大部分情况这种操作是无用的,只会让你的代码变得更难看。

其实GC远比你想象的聪明,这种操作,益处微乎其微,GC会自行探知到无用的对象。

但是如果是在一个比较大的方法内,这个方法可能很长(展开后几百行的方法,有时也是常见的)、执行起来又比较耗时,这种情况提前将某些不用的大的对象置为null,这种操作某种情况下是有帮助的。


3.慎用对象池

在看一些代码优化的时候,发现很多人会使用对象池进行复用优化。这种优化出发点是好的,即通过减少对象的分配开销来提高性能。但带来的弊端就是由于对象池中的对象会长期存活,大部分对象会晋升到Old Generation,因此无法通过YoungGC回收。

对于对象池的使用,如果对象本身就很小,初始化开销并不大,那么对象池只会增加代码复杂度,这个就是典型的过度优化。如果恰巧对象本身就比较大,那么晋升到Old Generation后,对GC的压力就更大了。再从线程安全的角度去考虑,一般情况下,池都会被并发的访问,如果对象池不处理好并发问题,那么系统或应用就回面临相应问题。但处理线程同步同样也是有开销的,所以要平衡线程同步的开销和对象创建的开销来考虑。

使用对象池最合理的场景就是当创建对象的开销比较大的时候,例如:

网络链接
数据库链接
线程创建

4.手动GC真的有意义吗?

经常能看到这样两行代码

Thread.yield()
System.gc();

前者是主动让出CPU资源,后者是主动触发GC,但真的管用吗?

事实上JVM从保证这两件事

更要命的是如果在JVM启动参数中允许显式GC,则触发的是FullGC,是的你没有看错,就是FullGC,FullGC则意味着Stop the world!!!,这对于某些响应时间要求严格的应用来说无异于宕机。。。。

So,我们的结论是:

  1. 不要用Thread.yield()
  2. 不要用System.gc(),但Native Memory的GC除外

**注:**Native Memory只能通过FullGC或CMS GC回收,除非你清楚的知道什么时候要调用,且知道其后果,才可以调用System.gc()。当你使用了像

  1. 用DirectByteBuffer分配字节缓冲区
  2. NIO或者NIO框架(如Netty)
  3. 用MappedByteBuffer做内存映射

这些情况会使用到较多的Native Memory,手动触发切记要慎重。安全起见,最好在JVM的启动参数中加上-XX:+DisableExplicitGC来禁用显式GC,但又存在矛盾情况,即如果禁用了System.gc(),那么上面说的Native Memory就无法回收了,所以开发者在使用的时候要考虑周全,且行且珍惜。


5.关注对象、属性的作用域

从垃圾回收角度出发,尽量让对象的声明周期短一些,尽可能的缩短对象的作用域。请遵循如下建议:

如果可以在方法内声明的局部变量,就不要声明为实例变量。
除非你的对象是单例的或不变的,否则尽可能少地声明static变量。

局部变量在方法执行后就回被GC回收,所以在变量定义前优先考虑下变量的生命周期。

静态变量都会在常量池中,垃圾回收不会处理常量池,所以要尽可能少的定义常量。


6.缓存和引用

如果要自行实现缓存,最好不用HashMap或ConcurrentHashMap,争取用弱引用的WeakHashMap,最好是使用缓存框架,Guava的Cache相对较好,建议使用。


7. 总结

上面提到的6点意见都是实际工作中常用的经验性总结,在不同的场景中未必有多大的提升,但熟悉这些方法,从JVM内存和垃圾回收的角度去思考程序,对于写出卓越的代码非常有必要,Java路漫漫,且行且珍惜。