网站建网站建设网站站网站,亚马逊德国做deals 网站,wordpress信息发布付费,工商注册核名当Java程序性能达不到既定目标#xff0c;且其他优化手段都已经穷尽时#xff0c;通常需要调整垃圾回收器来进一步提高性能#xff0c;称为GC优化。但GC算法复杂#xff0c;影响GC性能的参数众多#xff0c;且参数调整又依赖于应用各自的特点#xff0c;这些因素很大程度… 当Java程序性能达不到既定目标且其他优化手段都已经穷尽时通常需要调整垃圾回收器来进一步提高性能称为GC优化。但GC算法复杂影响GC性能的参数众多且参数调整又依赖于应用各自的特点这些因素很大程度上增加了GC优化的难度。即便如此GC调优也不是无章可循仍然有一些通用的思考方法。本篇会介绍这些通用的GC优化策略和相关实践案例主要包括如下内容 优化前准备: 简单回顾JVM相关知识、介绍GC优化的一些通用策略。 优化方法: 介绍调优的一般流程明确优化目标→优化→跟踪优化结果。 优化案例: 简述笔者所在团队遇到的GC问题以及优化方案。 一、优化前的准备 GC优化需知 为了更好地理解本篇所介绍的内容你需要了解如下内容。 1. GC相关基础知识包括但不限于 a) GC工作原理。 b) 理解新生代、老年代、晋升等术语含义。 c) 可以看懂GC日志。 GC优化不能解决一切性能问题它是最后的调优手段。如果对第一点中提及的知识点不是很熟悉可以先阅读小结-JVM基础回顾如果已经很熟悉可以跳过该节直接往下阅读。 JVM基础回顾 JVM内存结构 简单介绍一下JVM内存结构和常见的垃圾回收器。 当代主流虚拟机Hotspot VM的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实对象的生命周期不同所以针对不同生命周期的对象可以采取不同的回收方式以便提高回收效率。 Hotspot VM将内存划分为不同的物理区就是“分代”思想的体现。如图所示JVM内存主要由新生代、老年代、永久代构成。 ① 新生代Young Generation大多数对象在新生代中被创建其中很多对象的生命周期很短。每次新生代的垃圾回收又称Minor GC后只有少量对象存活所以选用复制算法只需要少量的复制成本就可以完成回收。 新生代内又分三个区一个Eden区两个Survivor区一般而言大部分对象在Eden区中生成。当Eden区满时还存活的对象将被复制到两个Survivor区中的一个。当这个Survivor区满时此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC年龄加1达到“晋升年龄阈值”后被放到老年代这个过程也称为“晋升”。显然“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间在Serial和ParNew GC两种回收器中“晋升年龄阈值”通过参数MaxTenuringThreshold设定默认值为15。 ② 老年代Old Generation在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到年老代该区域中对象存活率高。老年代的垃圾回收又称Major GC通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GCHotSpot VM里除了CMS之外其它能收集老年代的GC都会同时收集整个GC堆包括新生代。 ③ 永久代Perm Generation主要存放元数据例如Class、Method的元信息与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说该区域的划分对垃圾回收影响比较小。 常见垃圾回收器 不同的垃圾回收器适用于不同的场景。常用的垃圾回收器 串行Serial回收器是单线程的一个回收器简单、易实现、效率高。并行ParNew回收器是Serial的多线程版可以充分的利用CPU资源减少回收的时间。吞吐量优先Parallel Scavenge回收器侧重于吞吐量的控制。并发标记清除CMSConcurrent Mark Sweep回收器是一种以获取最短回收停顿时间为目标的回收器该回收器是基于“标记-清除”算法实现的。GC日志 每一种回收器的日志格式都是由其自身的实现决定的换而言之每种回收器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读将各个回收器的日志都维持一定的共性。JavaGC日志 中简单介绍了这些共性。 参数基本策略 各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小分析活跃数据的大小是很好的切入点。 活跃数据的大小是指应用程序稳定运行时长期存活对象在堆中占用的空间大小也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出比较准确的方法是在程序稳定后多次获取GC数据通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下见参考文献1 空间倍数总大小3-4 倍活跃数据的大小新生代1-1.5 活跃数据的大小老年代2-3 倍活跃数据的大小永久代1.2-1.5 倍Full GC后的永久代空间占用例如根据GC日志获得老年代的活跃数据大小为300M那么各分区大小可以设为 总堆1200MB 300MB × 4* 新生代450MB 300MB × 1.5* 老年代 750MB 1200MB - 450MB* 这部分设置仅仅是堆大小的初始值后面的优化中可能会调整这些值具体情况取决于应用程序的特性和需求。 二、优化步骤 GC优化一般步骤可以概括为确定目标、优化参数、验收结果。 确定目标 明确应用程序的系统需求是性能优化的基础系统的需求是指应用程序运行时某方面的要求譬如 - 高可用可用性达到几个9。 - 低延迟请求必须多少毫秒内完成响应。 - 高吞吐每秒完成多少次事务。 明确系统需求之所以重要是因为上述性能指标间可能冲突。比如通常情况下缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。 由于笔者所在团队主要关注高可用和低延迟两项指标所以接下来分析如何量化GC时间和频率对于响应时间和可用性的影响。通过这个量化指标可以计算出当前GC情况对服务的影响也能评估出GC优化后对响应时间的收益这两点对于低延迟服务很重要。 举例假设单位时间T内发生一次持续25ms的GC接口平均响应时间为50ms且请求均匀到达根据下图所示 那么有(50ms25ms)/T比例的请求会受GC影响其中GC前的50ms内到达的请求都会增加25msGC期间的25ms内到达的请求会增加0-25ms不等如果时间T内发生N次GC受GC影响请求占比(接口响应时间GC时间)×N/T 。可见无论降低单次GC时间还是降低GC次数N都可以有效减少GC对响应时间的影响。 优化 通过收集GC信息结合系统需求确定优化方案例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。 进行调整后将不同的优化方案分别应用到多台机器上然后比较这些机器上GC的性能差异有针对性的做出选择再通过不断的试验和观察找到最合适的参数。 验收优化结果 将修改应用到所有服务器判断优化结果是否符合预期总结相关经验。 接下来我们通过三个案例来实践以上的优化流程和基本原则本文中三个案例使用的垃圾回收器均为ParNewCMSCMS失败时Serial Old替补)。 三、GC优化案例 案例一 Major GC和Minor GC频繁 确定目标 服务情况Minor GC每分钟100次 Major GC每4分钟一次单次Minor GC耗时25ms单次Major GC耗时200ms接口响应时间50ms。 由于这个服务要求低延时高可用结合上文中提到的GC对服务响应时间的影响计算可知由于Minor GC的发生12.5%的请求响应时间会增加其中8.3%的请求响应时间会增加25ms可见当前GC情况对响应时间影响较大。 50ms25ms× 100次/60000ms 12.5%50ms × 100次/60000ms 8.3% 。 优化目标降低TP99、TP90时间。 优化 首先优化Minor GC频繁问题。通常情况下由于新生代空间较小Eden区很快被填满就会导致频繁Minor GC因此可以通过增大新生代空间来降低Minor GC的频率。例如在相同的内存分配率的前提下新生代中的Eden区增加一倍Minor GC的次数就会减少一半。 这时很多人有这样的疑问扩容Eden区虽然可以减少Minor GC的次数但会增加单次Minor GC时间么根据上面公式如果单次Minor GC时间也增加很难保证最后的优化效果。我们结合下面情况来分析单次Minor GC时间主要受哪些因素影响是否和新生代大小存在线性关系 首先单次Minor GC时间由以下两部分组成T1扫描新生代和 T2复制存活对象到Survivor区如下图。注这里为了简化问题我们认为T1只扫描新生代判断对象是否存活的时间其实该阶段还需要扫描部分老年代后面案例中有详细描述。 扩容前新生代容量为R 假设对象A的存活时间为750msMinor GC间隔500ms那么本次Minor GC时间 T1扫描新生代RT2复制对象A到S。扩容后新生代容量为2R 对象A的生命周期为750ms那么Minor GC间隔增加为1000ms此时Minor GC对象A已不再存活不需要把它复制到Survivor区那么本次GC时间 2 × T1扫描新生代R没有T2复制时间。可见扩容后Minor GC时增加了T1扫描时间但省去T2复制对象的时间更重要的是对于虚拟机来说复制对象的成本要远高于扫描成本所以单次Minor GC时间更多取决于GC后存活对象的数量而非Eden区的大小。因此如果堆中短期对象很多那么扩容新生代单次Minor GC时间不会显著增加。下面需要确认下服务中对象的生命周期分布情况 通过上图GC日志中两处红色框标记内容可知 1. new threshold 2动态年龄判断对象的晋升年龄阈值为2对象仅经历2次Minor GC后就晋升到老年代这样老年代会迅速被填满直接导致了频繁的Major GC。 2. Major GC后老年代使用空间为300M意味着此时绝大多数(86% 2G/2.3G)的对象已经不再存活也就是说生命周期长的对象占比很小。 由此可见服务中存在大量短期临时对象扩容新生代空间后Minor GC频率降低对象在新生代得到充分回收只有生命周期长的对象才进入老年代。这样老年代增速变慢Major GC频率自然也会降低。 优化结果 通过扩容新生代为为原来的三倍单次Minor GC时间增加小于5ms频率下降了60%服务响应时间TP90TP99都下降了10ms服务可用性得到提升。 调整前 调整后 小结 如何选择各分区大小应该依赖应用程序中对象生命周期的分布情况如果应用存在大量的短期对象应该选择较大的年轻代如果存在相对较多的持久对象老年代应该适当增大。 更多思考 关于上文中提到晋升年龄阈值为2很多同学有疑问为什么设置了MaxTenuringThreshold15对象仍然仅经历2次Minor GC就晋升到老年代这里涉及到“动态年龄计算”的概念。 动态年龄计算Hotspot遍历所有对象时按照年龄从小到大对其所占用的大小进行累积当累积的某个年龄大小超过了survivor区的一半时取这个年龄和MaxTenuringThreshold中更小的一个值作为新的晋升年龄阈值。在本案例中调优前Survivor区 64Mdesired survivor 32M此时Survivor区中age2的对象累计大小为41M41M大于32M所以晋升年龄阈值被设置为2下次Minor GC时将年龄超过2的对象被晋升到老年代。 JVM引入动态年龄计算主要基于如下两点考虑 如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件 aMaxTenuringThreshold设置的过大原本应该晋升的对象一直停留在Survivor区直到Survivor区溢出一旦溢出发生EdenSvuvivor中对象将不再依据年龄全部提升到老年代这样对象老化的机制就失效了。 bMaxTenuringThreshold设置的过小“过早晋升”即对象不能在新生代充分被回收大量短期对象被晋升到老年代老年代空间迅速增长引起频繁的Major GC。分代回收失去了意义严重影响GC性能。相同应用在不同时间的表现不同特殊任务的执行或者流量成分的变化都会导致对象的生命周期分布发生波动那么固定的阈值设定因为无法动态适应变化会造成和上面相同的问题。总结来说为了更好的适应不同程序的内存情况虚拟机并不总是要求对象年龄必须达到Maxtenuringthreshhold再晋级老年代。 案例二 请求高峰期发生GC导致服务可用性下降 确定目标 GC日志显示高峰期CMS在重标记Remark阶段耗时1.39s。Remark阶段是Stop-The-World以下简称为STW的即在执行垃圾回收时Java应用程序中除了垃圾回收器线程之外其他所有线程都被挂起意味着在此期间用户正常工作的线程全部被暂停下来这是低延时服务不能接受的。本次优化目标是降低Remark时间。 优化 解决问题前先回顾一下CMS的四个主要阶段以及各个阶段的工作内容。下图展示了CMS各个阶段可以标记的对象用不同颜色区分。 1. Init-mark初始标记(STW) 该阶段进行可达性分析标记GC ROOT能直接关联到的对象所以很快。 2. Concurrent-mark并发标记由前阶段标记过的绿色对象出发所有可到达的对象都在本阶段中标记。 3. Remark重标记(STW) 暂停所有用户线程重新扫描堆中的对象进行可达性分析标记活着的对象。因为并发标记阶段是和用户线程并发执行的过程所以该过程中可能有用户线程修改某些活跃对象的字段指向了一个未标记过的对象如下图中红色对象在并发标记开始时不可达但是并行期间引用发生变化变为对象可达这个阶段需要重新标记出此类对象防止在下一阶段被清理掉这个过程也是需要STW的。特别需要注意一点这个阶段是以新生代中对象为根来判断对象是否存活的。 4. 并发清理进行并发的垃圾清理。 可见Remark阶段主要是通过扫描堆来判断对象是否存活。那么准确判断对象是否存活需要扫描哪些对象CMS对老年代做回收Remark阶段仅扫描老年代是否可行结论是不可行原因如下 如果仅扫描老年代中对象即以老年代中对象为根判断对象是否存在引用上图中对象A因为引用存在新生代中它在Remark阶段就不会被修正标记为可达GC时会被错误回收。 新生代对象持有老年代中对象的引用这种情况称为“跨代引用”。因它的存在Remark阶段必须扫描整个堆来判断对象是否存活包括图中灰色的不可达对象。 灰色对象已经不可达但仍然需要扫描的原因新生代GC和老年代的GC是各自分开独立进行的只有Minor GC时才会使用根搜索算法标记新生代对象是否可达也就是说虽然一些对象已经不可达但在Minor GC发生前不会被标记为不可达CMS也无法辨认哪些对象存活只能全堆扫描新生代老年代。由此可见堆中对象的数目影响了Remark阶段耗时。 分析GC日志可以得出同样的规律Remark耗时500ms时新生代使用率都在75%以上。这样降低Remark阶段耗时问题转换成如何减少新生代对象数量。 新生代中对象的特点是“朝生夕灭”这样如果Remark前执行一次Minor GC大部分对象就会被回收。CMS就采用了这样的方式在Remark前增加了一个可中断的并发预清理CMS-concurrent-abortable-preclean该阶段主要工作仍然是并发标记对象是否存活只是这个过程可被中断。此阶段在Eden区使用超过2M时启动当然2M是默认的阈值可以通过参数修改。如果此阶段执行时等到了Minor GC那么上述灰色对象将被回收Reamark阶段需要扫描的对象就少了。 除此之外CMS为了避免这个阶段没有等到Minor GC而陷入无限等待提供了参数CMSMaxAbortablePrecleanTime 默认为5s含义是如果可中断的预清理执行超过5s不管发没发生Minor GC都会中止此阶段进入Remark。 根据GC日志红色标记2处显示可中断的并发预清理执行了5.35s超过了设置的5s被中断期间没有等到Minor GC 所以Remark时新生代中仍然有很多对象。 对于这种情况CMS提供CMSScavengeBeforeRemark参数用来保证Remark前强制进行一次Minor GC。 优化结果 经过增加CMSScavengeBeforeRemark参数单次执行时间200ms的GC停顿消失从监控上观察GCtime和业务波动保持一致不再有明显的毛刺。 小结 通过案例分析了解到由于跨代引用的存在CMS在Remark阶段必须扫描整个堆同时为了避免扫描时新生代有很多对象增加了可中断的预清理阶段用来等待Minor GC的发生。只是该阶段有时间限制如果超时等不到Minor GCRemark时新生代仍然有很多对象我们的调优策略是通过参数强制Remark前进行一次Minor GC从而降低Remark阶段的时间。 更多思考 案例中只涉及老年代GC其实新生代GC存在同样的问题即老年代可能持有新生代对象引用所以Minor GC时也必须扫描老年代。 JVM是如何避免Minor GC时扫描全堆的 经过统计信息显示老年代持有新生代对象引用的情况不足1%根据这一特性JVM引入了卡表card table来实现这一目的。如下图所示 卡表的具体策略是将老年代的空间分成大小为512B的若干张卡card。卡表本身是单字节数组数组中的每个元素对应着一张卡当发生老年代引用新生代时虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示卡表3被标记为脏卡表还有另外的作用标识并发标记阶段哪些块被修改过之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式避免了全堆扫描。 总结来说CMS的设计聚焦在获取最短的时延为此它“不遗余力”地做了很多工作包括尽量让应用程序和GC线程并发、增加可中断的并发预清理阶段、引入卡表等虽然这些操作牺牲了一定吞吐量但获得了更短的回收停顿时间。 案例三 发生Stop-The-World的GC 确定目标 GC日志如下图在GC日志中Full GC是用来说明这次垃圾回收的停顿类型代表STW类型的GC并不特指老年代GC根据GC日志可知本次Full GC耗时1.23s。这个在线服务同样要求低时延高可用。本次优化目标是降低单次STW回收停顿时间提高可用性。 优化 首先什么时候可能会触发STW的Full GC呢 1. Perm空间不足 2. CMS GC时出现promotion failed和concurrent mode failureconcurrent mode failure发生的原因一般是CMS正在进行但是由于老年代空间不足需要尽快回收老年代里面的不再被使用的对象这时停止所有的线程同时终止CMS直接进行Serial Old GC 3. 统计得到的Young GC晋升到老年代的平均大小大于老年代的剩余空间 4. 主动触发Full GC执行jmap -histo:live [pid]来避免碎片问题。 然后我们来逐一分析一下 - 排除原因2如果是原因2中两种情况日志中会有特殊标识目前没有。 - 排除原因3根据GC日志当时老年代使用量仅为20%也不存在大于2G的大对象产生。 - 排除原因4因为当时没有相关命令执行。 - 锁定原因1根据日志发现Full GC后Perm区变大了推断是由于永久代空间不足容量扩展导致的。 找到原因后解决方法有两种 1. 通过把-XX:PermSize参数和-XX:MaxPermSize设置成一样强制虚拟机在启动的时候就把永久代的容量固定下来避免运行时自动扩容。 2. CMS默认情况下不会回收Perm区通过参数CMSPermGenSweepingEnabled、CMSClassUnloadingEnabled 可以让CMS在Perm区容量不足时对其回收。 由于该服务没有生成大量动态类回收Perm区收益不大所以我们采用方案1启动时将Perm区大小固定避免进行动态扩容。 优化结果 调整参数后服务不再有Perm区扩容导致的STW GC发生。 小结 对于性能要求很高的服务建议将MaxPermSize和MinPermSize设置成一致JDK8开始Perm区完全消失转而使用元空间。而元空间是直接存在内存中不在JVM中Xms和Xmx也设置为相同这样可以减少内存自动扩容和收缩带来的性能损失。虚拟机启动的时候就会把参数中所设定的内存全部化为私有即使扩容前有一部分内存不会被用户代码用到这部分内存在虚拟机中被标识为虚拟内存也不会交给其他进程使用。 四、总结 结合上述GC优化案例做个总结 1. 首先再次声明在进行GC优化之前需要确认项目的架构和代码等已经没有优化空间。我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用通过GC优化令其性能达到一个质的飞跃。 2. 其次通过上述分析可以看出虚拟机内部已有很多优化来保证应用的稳定运行所以不要为了调优而调优不当的调优可能适得其反。 3. 最后GC优化是一个系统而复杂的工作没有万能的调优策略可以满足所有的性能指标。GC优化必须建立在我们深入理解各种垃圾回收器的基础上才能有事半功倍的效果。 本文中案例均来北京业务安全中心也称风控对接服务的实践经验。同时感谢风控的小伙伴们是他们专业负责的审阅才让这篇文章更加完善。对于本文中涉及到的内容欢迎大家指正和补充。 作者简介 录录2016年加入美团点评主要负责北京业务安全中心对接服务的后台研发工作。招聘 美团点评北京业务安全中心致力于建设公司平台级业务安全基础设施、保障业务安全运行工作涵盖交易秩序、帐号安全、爬虫防控等风控方向基于千万级订单、千万级日活跃用户、亿级存量用户进行数据挖掘实时处理每日百亿级流量热诚期待各位开发、算法、策略产品经理人才加入。联系邮箱tangyizhe#meituan.com。 参考文献 Scott O. Java Performance:The Definitive Guide. O’Reilly, 2014.周志明深入理解Java虚拟机[M]机械工业出版社2013.CMS垃圾回收机制.