7. 手工处理的对象池对很大范围内的应用都是合适的
对Stop-The-World停顿的坏感觉引起一个常见的应对,即在java堆的范围内,为应用程序组发明它们自己的内存管理技术。经常这会归结为实现一个对象池(或甚至是全面引用计数)的方法,并且需要让任何使用了领域对象的代码参与进来。
这种技术几乎总是被误导。它通常具有自身久远以前的根源,那时对象定位代价昂贵,突然的变化被认为是不重要的。但现在的世界已经非常不同。
现代的硬件具有难以想象的定位效率;近来桌面或服务器硬件的内存容量至少达到了2到3GB。这是一个很大的数字;除了专业的使用情形,让实际的应用充满那么大的容量不是很容易。
对象池一般很难正确的实现(特别是有多个线程在工作的时候),并且有几个消极的要求使得把它作为一般场景使用成为一个艰难选择:
所有接触到代码的开发者都必须清楚对象池并正确的处理它;
“对池清醒”代码与“对池不清醒”代码之间的界限必须要通晓并明文规定;
所有这些附加的复杂性必须保持最新,并定期评估;
如果这里任何地方失败了,无声损坏的风险(类似C中的指针重用)将被再次引入。
总之,只有在GC停顿不能被接受,而且在调试与重构过程中聪明的尝试也不能缩减停顿到可接受水平的时候,对象池才可以使用。
8. 在GC中CMS总是比Parallel Old更好
Oracle JDK默认使用一个并行的,全部停止(stop-the-world STW)垃圾收集器来收集老年代的垃圾。
另外一个选择是并发标记清除(CMS)收集器。这个收集器允许程序线程在大部分的GC周期中仍然继续工作,但它需要付出一些代价和带来一些警告。
允许程序线程和GC线程一起运行不可避免地导致对象表的变异同时又影响到对象的活跃性。这不得不在发生后进行清楚,所以CMS实际上有两个STW阶段(通常非常短)。
这会带来一些后果:
所有程序线程不得不放进一个安全点并且在每次完全收集时停止两次;
在收集并发运行地同时,程序吞吐量会减少(通常是50%)
在JVM从事通过CMS来收集垃圾的总体数据上(包括CPU周期)比并行收集更加高的。
依据程序的情况这些成本或者是值得的或者又不是。但并没有免费的午餐。CMS收集器是一个卓越的工程品,但它不是功能较多药。
所以在介绍前,CMS是你正确的GC策略,你得首先考虑Parallel Old的STW是不可接收的和不能调和的。最后,(我不能足够地强调),确定所有的指标都从相当的生产系统上得到。
9. 增加堆内存会解决你内存溢出的问题
当一个应用程序崩溃,GC中止运行时,许多应用组会通过增加堆内存来解决问题。在许多情况下,这可以很快解决问题,并争取时间来考虑出一个更深的解决方案。然而,在没有真正理解性能产生的根源时,这种解决策略实际上会使情况更糟糕。
试想一下,一个编码很烂的应用构造了非常多的领域对象(生命周期大概维持2,3秒)。如果内存分配率足够高,垃圾回收就会很快地执行,并把这些领域对象放到年老代。一旦进入了老年代,对象就会立即死去,但直到下一次完全回收才会被垃圾回收器回收。
如果这个应用增加其堆内存,那么我们能做的是增加空间,为了存放那些相对短期存在,然后消逝的领域对象。这会使得 Stop-The-World 的时间更长,对应用毫无益处。
在改变堆内存和或其他参数之前,理解一下对象的动态分配和生命周期是很有必要的。没做调查就行动,只会使事情更糟。在这里,垃圾回收器的老年分布信息是非常重要的。
总结
当说道Java性能调优时直觉通常会误导人。我们需要经验数据和工具来帮助我们具象化和了解平台的特性。
垃圾收集也许提供了这方面最好的例子。GC子系统对于调优和生产数据指导调整有惊人的潜力,但对于生产程序它是很难去不借助工具来让产生的数据有意义。
运行任何Java进程,默认都应该最少有这些标记:
-verbose:gc(打印GC日志)
-Xloggc:(更全面的GC日志)
-XX:+PringGCDetail(更详细的输出)
-XX:+PrintTenuringDistribution(显示由JVM设定的保有阈值)
然后使用工具来分析日志——手写脚本和一些生成图,或一个可视化工具如(开源的)GCViewer或JClarity Censum。