`
netcome
  • 浏览: 479585 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

关注性能: 调优垃圾收集

    博客分类:
  • JAVA
阅读更多

随着网志作为公共日记的流行,网志主机迅速地增长。所以对于 Blog-City 的人来说,非常清楚他们的站点需要发展和提高。为了满足其增长的需要,该公司最近刚刚推出了 Blog-City version 2.0。正像经常出现的情况那样,当新的应用程序转入运行阶段时,由于各种原因,其性能无法完全满足期望的要求,突然出现随机的长时间应用程序被挂起的现象还不是最坏的情况。

在其核心,Blog-City 依靠 Blue Dragon Servlet 引擎(CFML 引擎)和数据库。令人惊讶的是,所有这些软件都宿主在运行 Red Hat Linux 的相当老的 P3 机器上。这台机器具有单个硬盘和 512MB 内存,这对于过去的负载来说是足够强大的,但它正在承受不断增长的负载。Blog-City 的运作方式很成功,但其资源限制却成了其成功路上的绊脚石。尽管如此,这就是未来还要继续使用一段时间的所有硬件。

问题定义

整个过程的第一步是确定突然出现应用程序减慢的原因。首先我们怀疑的对象是垃圾收集。正如我们在 本专栏的上月文章 中所论述的那样,确定垃圾收集和内存利用问题是否对应用程序产生负面影响的最容易的方式是,设置 -verbose:gc JVM 选项,并检查日志输出。因此我们重新启动应用程序,打开冗长的垃圾收集日志选项,然后耐心地等待应用程序的性能降低。我们的耐心换来的是非常详细的垃圾收集日志文件。

从对日志文件的最初分析中看,在这一应用程序中垃圾收集的瓶颈是显而易见的。种种迹象包括垃圾收集的频率、持续时间和总体效率都已表明这一点。高于普通垃圾收集频率的常见原因是,堆的大小刚好足以适应所有当前正在使用的运行对象,无法适应新的正被创建的对象。虽然应用程序消耗大量堆可能有许多原因,但主要原因可能是没有足够内存而导致垃圾收集器运行,因为它设法满足当前需要。换句话说,应用程序试图分配新对象,但失败了,如果失败的话,将触发垃圾收集程序。如果垃圾收集失败而无法恢复足够内存,它将迫使另一个花费更大的垃圾收集程序发生。即使 GC 恢复了足够的空间来满足瞬间需求,可以肯定的是,在应用程序程序另一次分配失败,触发另一个 GC 之前,时间不会很长。因此,应该关注重复扫描空闲堆空间的无效任务,而不是服务于应用程序的 JVM。

应用程序逐步消耗所有可用的堆空间可能有许多原因,但如果有更多内存的话,临时解决方案就是配置更大的堆。假设应用程序没有内存泄漏(或者也就是我们常说的“无意识地保留对象”),它将找到一个“自然”级别的堆消耗,在这个级别中,GC 将能够很适应地得到维持(除非对象创建的速度过快,以至 GC 总是处于赛跑状态)。在这种情况下,以及无意识地保留对象的情况下,我们需要对应用程序做一些变动,以便获得某些改进。







如果仅仅是这样,那就太简单了

遗憾的是,我们必须面对严酷的现实因素——正在运行的机器只有 512 MB 内存。更糟的是,我们必须与数据库和其他运行在机器中的进程共享该空间。要完整理解这一点为什么至关重要,首先您必须明确理解垃圾收集的基本知识,以及它如何与底层操作系统进行交互。

虚拟内存不再是灵丹妙药

操作系统已经使用虚拟内存许多年了。正如您所知道的,虚拟内存使操作系统的内存看起来比实际的内存要多,这允许计算机运行那些所需内存比可用物理内存更大的程序,不使用内存的应用程序部分将保存在磁盘上。为了进一步简化,操作系统同时按页管理内存。页通常包含 512 字节到 8 KB,所有页的组合就组成了一个虚拟地址空间。操作系统维持一个页表,用于告诉操作系统如何映射虚拟地址到物理地址。当应用程序要求某个内存位置的内容时,操作系统(或硬件)将识别包含虚拟地址的页面。然后确定该页面是否在内存中,如果不在,将会报告 页面错误。但是有许多种方式来处理页面错误,最终的结果是,页面必须从磁盘载入到内存中。这样应用程序就可以访问到有效虚拟地址的内容。

如果相关对象总是在内存的同一页面上聚合,那么 GC 的连续工作很可能出现困难。但是现实世界中,相关对象很少(如果有的话)出现聚合现象。实际结果是,依靠虚拟内存的系统将导致操作系统将页从内存中换入和换出,因为它标记然后废弃堆空间,而当聚合现象发生时,GC 将很多时间花在等待页面从磁盘换入而不是实际恢复内存上。因此,应用程序正在等待 GC,而 GC 正在等待磁盘,其间未完成任何真正的工作。由于本系统只有一个磁盘,并且它还需要支持数据库,因此我们在解决问题时处于两难境地。一方面,我们需要增加内存数量,这样我们可以减少 GC 的频率,但另一方面,我们还需要确保数据库的完好运行,而数据库也是内存的消耗大户。因此,我们需要了解应用程序所需的最小内存数量。

正如我们在上月看到的,在冗长的 GC 日志中这一信息可以很容易得到,无需为这一信息而扫描整个日志,我们使用免费的 JTune 工具(请参阅 参考资料)来解释冗长的 GC 日志。图 1 显示了经过垃圾收集之后的内存利用情况,其中我们将 -Xmx 设置为 256 MB。


图 1. 垃圾收集之后的内存利用情况
图 1. 垃圾收集之后的内存利用情况 




回页首


分析 verbose:gc 输出

在图 1 中,蓝色部分表示部分 GC。橙色区域表示完整的 GC,而粉色矩形表示两个完整 GC 在它们之间少于一毫秒之内已经发生的堆利用情况。从结果中我们看到,平均每 0.257 秒有 12,823 次清除。总共有 345 次完整的垃圾收集和 44 次紧挨着的垃圾收集。完整垃圾收集的平均持续时间是 7.303 秒,结果表明有 9.36% 的运行时间花费在垃圾收集程序上。虽然这个值偏高,它仍然保持在 10% 的正常水平之内。因此,在本例中,GC 是系统的繁重负担但还没有达到严重的地步。真正的问题是存在内存泄漏,这一点可以从总体上堆利用率不断增长的趋势看出来。

即使内存泄漏消耗了 50 MB 内存,它也应该是经过很长一段时间后才发生,这使得内存泄漏在较短的测试中很少会引人注意。内存泄漏的实际结果是,它把 JVM 的内存消耗推动到某个点,在该点它强迫 JVM (从而强迫操作系统)消耗内存,它强迫启动分页。图 2 就证明了这一点。注意正好在 55,000 秒标记之后,每一 GC 周期的持续时间中内存消耗突然地持续增加。


图 2. GC 持续时间
图 2. GC 持续时间 

如您所想,由于垃圾收集的阻塞将导致系统只有更少的时间来分配给用户线程,因此用户响应开始增加。在日志的过去 10,000 秒中,我们看到每次完全收集(总共 15 次)花费时间超过了 30 秒,平均持续时间大约 70 秒 —— 这导致超过 10% 的处理时间分配给完全 GC。部分收集(这里刚好超过了 1000 次)无法正常工作,平均每次请求耗时 1.24 秒,远高于以前 11,800 次清除中的平均 0.25 秒。

分代收集

本文不涉及太深的细节(请参阅 参考资料,获取分代 GC 的详细描述),分代堆空间产生了“年轻”和“年老”对象,它们位于分开的堆空间中。在本配置中,年轻和年老分代空间可以通过不同的 GC 算法和策略来维持,以提高 GC 的整体性能。

一种这样的策略是,进一步将年轻分代划分为创建空间,称为 Eden,以及残存(survivor)空间,用于幸存一个或者多个收集的年轻对象。如果在 Eden 中有足够的内存来适应新对象创建的话,这一般能工作正常。如果不是这种情况,那么对象可以在年老对象空间中创建。同样,如果残存空间足够的话,那么对象将移入年老分代空间。我们将使用这些事实来帮助调优遇到的问题。







减少完全收集的次数

Blog-City 所碰到的难题是在某一随机点出现长的暂停时间。一旦应用程序启动出现问题,不重新启动机器的话,就无法返回跟踪。由于长时间暂停的现象直接与长的 GC 相关,我们考虑如果将对象保持在年轻分代来减少完全 GC 的次数。由于完全 GC 的代价如此之大,在年轻分代收集更多对象能够得到更短的暂停时间。要完成这一任务,我们调整了一些垃圾收集参数,包括 残存比率(survivor ratio)和 期限阈值(tenuring threshold)

残存比率用于设置与年轻分代空间总体大小相关的残存空间的大小。如果残存比率设置为 8(Intel 的默认值),那么每一残存空间将是 Eden 空间的 1/8 大小。另一种考察它的方式是,年轻分代将该 Eden 空间划分为 10 个相同大小的值,该 Eden 将分配其中的 8 个,每一个残存空间的大小为 1。

我们的假设是,通过减少残存比率,我们可以减少由于残存空间中空间的缺乏,对象过早地被提升为年老分代的几率。另一种方法是增加期限阈值,这样的话,对象在提升之前将需要保留更多的 GC 事件。本着这个想法,Blog-City 将设置更改为 -XX:SurvivorRatio=4 ,然后重新启动。

选择低暂停时间的垃圾收集算法

由于这次技术调优的目标之一是减少暂停时间,我们决定抛弃默认的单线程、标记清扫的垃圾收集程序。我们选择通过标志 XX:+UseParallelGC 来采用并行拷贝收集程序。同样,有关实际算法的细节可以在 Resources中找到,这里需要提一下的是,这一标志调用了一个多线程收集程序。线程的数量设置为 CPU 的数量。基于这一事实,了解为什么单线程并行拷贝垃圾收集程序要比传统的标记清扫算法工作更好是很困难的,但是从实际观察中可以体会到提供了某些性能上的优势。

图 3 和图 4 的输出展示了使用标志 -XX:SurvivorRatio=4 +XX:+UseParallelGC -server -Xmx256M 运行时的结果。


图 3. 新配置下的内存使用情况
图 3. 新配置下的内存使用情况 

结果图表显示了明显的不同。虽然仍然有一个内存漏洞。内存消耗的总数相比前一个图已经是大大降低了。GC 持续时间的快速比较揭示了年轻分代和年老分代的总体 GC 持续时间的明显减少。


图 4. 新配置下的 GC 持续时间
图 4. 新配置下的 GC 持续时间 

有意的、无意的对象保持

由于应用程序是依靠内存的,跟踪内存泄漏并消除它们已经变得越来越重要。在本例中,用于支持缓存策略的组件决定了主要漏洞的来源。从最后的内存分析情况来看(图 3),虽然消除了主要内存泄漏,我们可以看到仍有另一个“低级别的”的漏洞,但这个漏洞比较小,因此它在下一版本发布之前可以忽略。







结束语

本文提出了许多挑战。首先,我们正在调优一个现实中的应用程序,这意味着更改会受到很多限制。第二个挑战是,这项任务是使用 IRC 聊天室远程操控的。聊天室不提供任何级别或质量的相互通信,而通信在这种类型的任务中往往是必需的。在本例中,团队已经习惯了聊天室的真实性,并能通过这种真实性毫无任何阻碍地工作着。

最后也是最困难的挑战是我们受硬件的限制。由于多种原因,我们不可能为系统添加新硬件。其中最大的问题是系统中物理内存的数量,而 JVM 和 MySQL 需要大量的内存。但是,通过系统地逐一应用许多更改,并度量它们对系统产生的影响,我们可以逐步地改进总体系统性能。



参考资料

分享到:
评论

相关推荐

    JVM-性能调优垃圾收集算法虚拟机组成

    对于任何Java开发者来说,理解和掌握JVM的性能调优、垃圾收集算法以及虚拟机的组成是提高应用程序效率的关键。 首先,我们要了解JVM的组成部分。JVM主要包括以下几个部分: 1. **类加载器(ClassLoader)**:负责...

    JVM性能调优垃圾收集算法虚拟机组成.zip

    在JVM中,性能调优是一个关键环节,特别是垃圾收集(Garbage Collection, GC)算法的选择和配置,对系统的响应时间、吞吐量以及内存使用效率有着直接影响。本文将深入探讨JVM的组成、垃圾收集的基本概念及其常见算法...

    JVM下篇:性能监控与调优篇.7z

    - **G1(Garbage-First)GC**:新一代垃圾收集器,目标是达到可预测的暂停时间模型。 3. **性能监控工具**: - **JVisualVM**:集成了多种JVM监控功能,如内存、线程、类加载、CPU使用率等。 - **JConsole**:...

    JVM性能调优总结.docx

    垃圾收集器参数设置对JVM性能调优也非常重要。以下是一些常用的参数设置: * -XX:NewRatio=4:设置年轻代与年老代的比值。 * -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。 * -XX:MaxPermSize=...

    个人总结之—JVM性能调优实战

    ### 个人总结之—JVM性能调优实战 #### 概述 本文档是一篇关于JVM(Java虚拟机)性能调优的经典实战总结。在实际应用开发与维护过程中,JVM性能调优是一个非常重要的话题,它直接关系到应用程序运行效率、资源利用...

    Java性能调优大全

    Java性能调优是一个复杂而重要的领域,涉及到程序的运行效率、资源消耗以及系统的稳定性。这份“Java性能调优大全”提供了全面的指南,包括VisualVM的使用、JVM的性能优化、OMM(可能是指Oracle Management Monitor...

    阿里巴巴Java性能调优实战(2021华山版).rar

    《阿里巴巴Java性能调优实战(2021华山版)》是一本专注于Java应用程序性能优化的专业书籍,由阿里巴巴的技术专家团队倾力打造。这本书基于阿里巴巴的实际业务场景,结合丰富的实践经验,为Java开发者提供了深入、...

    JAVA虚拟机性能参数调优指导书.doc

    随着Java技术的发展,JVM的性能调优显得尤为重要,因为这直接影响到程序的响应速度和资源利用率。通过理解JVM的运行时分析,我们可以更好地控制应用程序的运行状态。 2. **JAVA虚拟机运行机制概览** - **运行时...

    大数据各类性能调优

    - Spark Streaming的性能调优主要关注点在于: - 调整Batch Duration。 - 优化Checkpoint配置。 - 使用Window函数等。 ##### 12.9.4 Spark CBO调优 - Spark CBO的调优主要涉及: - 优化统计信息收集。 - 合理...

    WebLogic Server性能调优

    - 调整Java虚拟机的堆大小、新生代和老年代的比例,设置垃圾收集策略,以及启用或调整JVM的性能监控工具,如JConsole和VisualVM,以监控和优化内存使用。 3. **WebLogic Server调整** - 配置WebLogic Server的...

    Java 性能调优 Java 性能调优 Java 性能调优

    1. **垃圾收集器的选择**:不同的垃圾收集器适用于不同的场景,如CMS、G1等。 2. **内存分配**:合理配置年轻代和老年代的比例,可以减少垃圾回收的频率。 3. **线程堆栈大小**:根据程序的特点调整线程堆栈大小...

    Java性能调优指南.pptx

    【Java性能调优指南】 在Java开发中,性能调优是一项关键任务,它涉及到程序运行效率、资源利用和系统稳定性。本指南主要关注Java虚拟机(JVM)调优、编码最佳实践以及微基准测试的重要性。 **基本规则** 1. **...

    J2EE应用性能调优.docx

    【性能调优--0:前言】 性能调优是一个针对软件系统进行的持续改进过程,旨在提高系统的效率、响应速度和资源利用率。在J2EE应用中,性能问题可能导致用户体验下降,甚至影响整个系统的稳定性和可扩展性。性能调优...

    Java虚拟机性能参数调优指导书

    在Java开发领域,Java虚拟机(JVM)的性能调优是提升应用程序效率的关键环节。《Java虚拟机性能参数调优指导书》由姚建华编著,为开发者提供了深入理解和优化JVM的宝贵资源。本书详细阐述了JVM的运行机制、参数分类...

    Java性能调优命令

    在Java应用程序的性能调优过程中,掌握一系列命令行工具是至关重要的。这些工具可以帮助开发者监控应用程序的运行状态,诊断性能瓶颈,并对JVM进行调优。以下是一些常用的Java性能调优命令及其用法和相关知识点。 1...

    Java虚拟机性能参数调优.docx

    常见的性能参数包括堆的初始大小和最大大小、垃圾收集的频率和策略等。正确地设置这些参数可以提高Java虚拟机的性能,使其更加高效地运行。 在调优Java虚拟机性能参数时,我们需要了解Java虚拟机的运行机制和内部...

    weblogic性能调优

    例如,设置合适的堆大小(如新生代、老年代),调整并发垃圾收集器参数,以及优化类加载行为,以减少停顿时间并提高整体性能。 3. **WebLogic Server调整**: - 包括WebLogic Server的启动参数配置、线程池大小、...

    Java ee性能调优.rar

    性能调优是确保这些应用程序高效运行的关键环节。下面将详细介绍Java EE性能调优的相关知识点。 1. **JVM参数调优**: - **内存配置**:调整初始堆大小(`-Xms`)和最大堆大小(`-Xmx`),以适应应用的内存需求,...

Global site tag (gtag.js) - Google Analytics