Java虚拟机JVM性能优化(三):垃圾收集详解(3)
并行收集的主要代价(特别是对于生产环境)是应用程序线程在垃圾收集期间无法正常工作,就像复制收集器一样。因此那些对于响应时间敏感的应用程序使用并行收集器会有很大的影响。特别是在堆空间中有很多复杂的活动对象结构时,有很多的对象引用需要跟踪。(还记得吗标记-清除收集器回收内存的时间取决于跟踪活动对象集合的时间加上遍历整个堆的时间)对于并行方法来说,整个垃圾收集时间应用程序都会暂停。
并发收集器
并发垃圾收集器更适合那些对响应时间敏感的应用程序。并发意味着垃圾收集线程和应用程序线程并发执行。垃圾收集线程并不独占所有资源,因此需要决定何时开始一次垃圾收集,需要有足够的时间跟踪活动对象集合并在应用程序内存溢出前回收内存。如果垃圾收集没有及时完成,应用程序就会抛出内存溢出错误,另一方面又不希望垃圾收集执行时间太长因为那样会消耗应用程序的资源进而影响吞吐量。保持这种平衡是需要技巧的,因此在确定开始垃圾收集的时机以及选择垃圾收集优化的时机时都使用了启发式算法。
另一个难点在于确定何时可以安全执行一些操作(需要完整准确的堆快照的操作),例如:需要知道何时标记阶段完成,这样就可以进入清理阶段。对于stop-the-world的并行收集器来说这不成问题,因为世界已经暂停了(译者注:应用程序线程暂停,垃圾收集线程独占资源)。但对于并发收集器而言,从标记阶段立刻切换到清理阶段可能不安全。如果应用程序线程修改了一段内存,而这段内存已经被垃圾收集器跟踪并标注过了,这就可能产生了新的没有标注的引用。在一些并发收集实现中,这会使应用程序陷入长时间重复标注的循环,当应用程序需要这段内存时也无法获得空闲内存。
通过到目前为止的讨论我们知道有很多的垃圾收集器和垃圾收集算法,分别适合特定的应用程序类型和不同的负载。不仅是不同的算法,还有不同的算法实现。所以在指定垃圾收集器钱最好了解应用程序的需求以及自身特点。接下来我们将介绍Java平台内存模型的一些陷阱,这里陷阱的意思是,在动态变化的生产环境中Java程序员容易做出的一些使得应用程序性能变得更差的假设。
为什么调优无法代替垃圾收集
多数的Java程序员都知道如果要优化Java程序可以有很多选择。若干个可选的JVM、垃圾收集器和性能调优参数让开发者花费大量的时间在无休无尽的性能调优方面。这使有些人因此得出结论:垃圾收集是糟糕的,通过调优使垃圾收集较少发生或者持续时间较短是一个很好的变通办法,不过这样做是有风险的。
考虑一下针对具体应用程序的调优,多数的调优参数(例如内存分配率、对象大小、响应时间)都是基于当前测试的数据量对应用程序的内存分配率(译者注:或者其他参数)调整。最终可能造成以下两个结果:
1.在测试中通过的用例在生产环境中失败。
2.数据量的变化或者应用程序的变化要求重新调优。
调优是需要反复的,特别是并发垃圾收集器可能需要很多调优(尤其在生产环境中)。需要启发式方法来满足应用程序的需要。为了要满足最坏的情况,调优的结果可能是一个非常死板的配置,这也导致了大量的资源浪费。这种调优方法是一种堂吉诃德式的探索。事实上,你越是优化垃圾收集器来匹配特定的负载,越是远离了Java运行时的动态特性。毕竟有多少应用程序的负载是稳定的呢,你所预期的负载的可靠性又有多高呢?
那么如果你不将注意力放在调优上,能够做些什么来防止内存溢出错误和提高响应时间呢?首要的事情就是找到影响Java应用程序性能的主要因素。
碎片化
影响Java应用程序性能的因素不是垃圾收集器,而是碎片化以及垃圾收集器如何处理碎片化。所谓碎片化是这样一种状态:堆空间中有空闲可用的空间,但并没有足够大的连续内存空间,以至于无法为新对象分配内存。正如在第一篇中提到的,内存碎片要么是堆中残留的一段空间TLAB,要么是在长期存活对象中间被释放的小对象所占用的空间。
随着时间的推移和应用程序的运行,这些碎片就会遍布在堆中。在某些情况下,使用了静态化调优的参数可能会更糟,因为这些参数无法满足应用程序的动态需要。应用程序无法有效利用这些碎片化的空间。如果不做任何事情,那么将导致接连不断的垃圾收集,垃圾收集器尝试释放内存分配给新对象。在最坏的情况下,即使是接连不断的垃圾收集也无法释放更多的内存(碎片太多),然后JVM不得不抛出内存溢出的错误。你可以通过重启应用程序来解决碎片化,这样Java堆就有连续的内存空间可以分配给新对象。重启程序导致宕机,而且一段时间后Java堆将再次充满碎片,不得不再次重启。
- 上一篇:浅析java创建文件和目录
- 下一篇:Java虚拟机JVM性能优化(二):编译器