GC分代的基本假设
GC分代的基本假设是:
绝大部分对象的生命周期都非常短暂,存活时间短。
基于这个前提,在编码过程中,我们应该尽可能地缩短对象的生命周期。
结论:
- 分配小对象的开销非常小,不要吝啬去创建
- GC最喜欢这种小而短命的对象
- 让对象的生命周期尽可能短,例如在方法体内创建,使其能尽快地在
YoungGC
中被回收,不会晋升到年老代(Old Generation
)
对象分配的优化
基于大部分对象都是小而短命,并且不存在多线程的数据竞争。这些小对象的分配,会优先在线程私有的TLAB
中分配,TLAB
中创建的对象,不存在锁甚至是CAS
的开销。
TLAB
占用的空间在Eden Generation
当对象比较大,TLAB
的空间不足以放下,而JVM
又认为当前线程占用的TLAB
剩余空间还足够时,就会直接在Eden Generation
上分配,此时是存在并发竞争的,所以会有CAS
的开销,但是也还好。
当对象大到Eden Generation
放不下时,JVM
只能尝试去Old Generation
分配,这种情况需要尽可能避免,因为一旦在Old Generation
上分配,这个对象只能被Old Generation
的GC
或者是Full GC
回收了。
不可变对象的好处
GC
算法在扫描存活对象时通常需要从ROOT
节点开始,扫描所有存活对象的引用,构建出对象图。
不可变对象对GC
的优化,主要体现在Old Generation
中。
如果存在Old Generation
的对象引用了Young Generation
的对象,那么在每次YoungGC
的过程中,就必须考虑到这种情况。
卡表(Card Table)
当Old Generation
中的对象发生对Young Generation
中的对象产生新的引用关系或释放引用时,都会在卡表中相应的标记上标为脏(dirty
);而当YoungGC
时,只需要扫描这些dirty
的项就可以了。
可变对象对其他对象的引用关系可能会频繁变化,并且有可能在运行过程中持有越来越多的引用,特别是容器。这些都会导致对应的卡表项被频繁标记为dirty
。
而不可变对象的引用关系非常稳定,在扫描卡表时就不会扫到它们对应的项了。
这里的不可变对象,不是指仅仅自身引用不可变的
final
对象,而是真正的Immutable Objects
。
指定容器初始化大小
如果采用默认无参构造函数,构建一个ArrayList,不断增加元素知道OOM,那么在此过程中会导致:
- 多次数组扩容,重新分配更大空间的数组
- 多次数组拷贝
- 内存碎片
对象池
为了减少对象分配开销,提高性能,可能有人会采用对象池的方式来缓存对象集合,作为复用的手段。
但是对象池中的对象由于在运行期长期存活,大部分会晋升到Old Generation,因此无法通过YoungGC回收。
可以借助成熟的开源框架,例如Apache Commons Pool
对象作用域
尽可能缩小对象的作用域,即生命周期
- 如果可以在方法内声明的局部变量,就不要声明为实例变量
- 除非你的对象是单例的或者不变的,否则尽可能少地声明static变量