《JVM故障诊断指南》之2 —— 调整合适的Java堆大小的技巧

原文链接 原文作者:Byron Kiourtzoglou 翻译:梅小西(904516706)

在生产系统上决定合适的Java堆大小不是一个容易的操作。许多性能问题的发生都是由于不恰当的Java堆容量的错误调整。这部分将从介绍一些技巧作为开头,它能帮助你在当前的或者新的生产系统上决定最佳的Java堆大小。其中一些技巧对预防OutOfMemoryError问题和内存泄露方面也同样有用。

请注意这些技巧是倾向于“帮助你”决定合适的Java堆大小。因为每一个IT环境都不相同,实际上你是处于最好的时机来精确决定你客户环境所需要的Java堆参数。


1 – JVM: 你总是担心你不理解的部分

你如何期望去配置,调整和故障诊断那些你不懂的部分?你也许从没有机会去调整和改善Java虚拟机参数,但是为了提高你的知识以及故障诊断能力,你仍然可以免费的学习它们的基础。 也许你不同意,但是从我的观点来说,那种Java程序员不需要了解内部JVM内存管理的想法是错误的。

Java堆调整和故障处理对Java和JavaEE初学者来说尤其是一个挑战,下面是一个典型的情形 :
• 你的客户生产环境经常面对OutOfMemoryError 问题,并导致影响了许多业务。你的支持团队对于解决这些问题有很大压力。
• 通过快速Google搜索,你能找到类似问题的例子并认为(以为)你面对的是同样的问题。
• 此时你从另一个同样发生了OutOfMemoryError 情况的JVM里拿到-Xms和-Xmx的值,并希望能快速解决客户的问题。
• 然后你对你的环境进行了处理并实现了同样的调整。2天过后,你发现问题依然存在(甚至更糟糕或者情况好一点),你只能继续斗争下去…

问题出在哪?

• 你没有首先找到问题的根源并对它有合适的了解。
• 同样你也没有对你生产环境上的更深层次的情况(比如参数规范,负载情况等)有合适的了解。网络搜索是一个很好的可以学到并分享知识的方法,但是你还是需要去做严格的调查以及问题根源的分析。
• 你缺少一些JVM及其内存管理方式的基础知识,这阻碍了你从更多维度获得信息并整合。

我给你的 #1 技巧和建议是去学习和理解JVM不同内存空间的基础规则,这种知识很关键,它能使你给客户做出有用的建议,并能适度的了解后期调整策略可能出现的影响和危险。

再次提醒,Java虚拟机内存被分为3个部分:
• Java 堆: 应用于所有JVM提供商,通常被分为年轻代 (nursery)和年老代(tenured)。
• 持久代: 仅应用于Sun HotSpot虚拟机 (持久代会在之后的Java更新中被移除)
• 本地堆(C堆): 应用于所有JVM提供商。

如你所见,Java虚拟机内存管理远远复杂于通过-Xmx设置最大的可能值。你应该从各个角度来看,包括你的本地区和持久代的内存需求,它需要基于主机的物理可用内存(以及cpu核心数)。

对于32位JVM而言比较棘手,因为Java堆和本地堆在内存上是竞争关系。你的Java堆越大,本地堆就越小。尝试去为32位虚拟机设置一个大内存比如2.5G以上,根据你应用的内存占用和线程的数量,它会增大本地堆发生OutOfMemoryError 的危险性。64位虚拟机解决了这些问题,但它仍然限制于物理可用资源以及垃圾收集开销(major gc成本随着空间变大而增加)。下面这图说明内存更大并不总是好事,所以你不要以为你可以在一个16G机器 64位JVM进程上运行20个Java EE应用。

这里写图片描述

2 – 数据和应用为主:重新检查你的静态数据占用需求

你的应用以及相关的数据会决定Java堆的占用需求。通过静态内存,也就是说“可预计”的内存需求要遵循下面每条:
• 确定你要部署多少个应用到单独的JVM进程中,比如多个EAR文件,WAR文件,jar文件等。你在单个JVM里部署的越多,本地堆的内存需求越高。
• 确定有多少潜在的Java类文件在运行时会被加载,包括第三方API。类在运行时加载越多,Hotspot虚拟机的持久代内存需求越高,内部JIT相关的优化对象也越多。
• 确定数据缓存占用,举个例子,由应用程序(和第三方API)加载作为内部缓存的数据结构,如从数据库拿到的作为缓存的数据,从文件读取的数据等。你用的缓存数据越多,Java堆老年代空间的内存需求也越高。
• 确定你的中间件允许创建的线程数。这点很重要,因为Java线程需要足够的本地内存,否则会发生OutOfMemoryError 。

举个例子,如果你需要部署10个不同的EAR应用到单独的JVM进程中,和2个或3个应用相比,你需要很多本地内存和持久代内存。数据缓存没有被序列化到磁盘或者数据库将需要占用额外的老年代内存。

尽量计算出静态内存占用需要的合理值。这对于在你真正开始测量操作之前设置一些起始的JVM容量数字非常有用。对于32位JVM,我通常不建议Java堆大小超过2Gb(-Xms2048m, -Xmx2048m),因为你需要为你的Java EE应用和线程在持久代和本地堆分配足够的内存。

这种分配对于在32位JVM进程中部署太多应用并且很容易导致本地堆内存消耗过大而言尤其有用。特别是在多线程环境中。
对于64位而言,我通常建议在每个JVM进程中Java堆大小的起始值设置在3 GB 或者 4 GB。

3 – 业务流量设置规则:重新检查你的动态占用内存需求

业务流量通常会决定你动态内存占用。并发的用户和请求会产生JVM“心跳”是因为有非常频繁的对象被创建以及对于短生命周期,长生命周期对象的频繁垃圾收集,你可以用多种监控工具来观察。从上面的JVM图表可以看出,典型的年轻代和老年代的比例是1:3或者33%。

对于典型的32位虚拟机,Java堆设置到2Gb(使用分代&并发收集器)通常会分配500M给年轻代,1.5G给老年代。
减少major gc的频率是性能优化的关键,所以,了解和估算在峰值期间需要多少内存是很重要的。

同样的,你的应用和数据的类型会决定你需要的内存大小。购物车类型的应用(长时间存活对象)涉及大的非序列化的会话数据,通常需要很大的Java堆和很大的老年代空间。无状态和XML处理为主的应用(很多短时间存活对象) 为了减少major收集的频率需要合适的年轻代空间。

比如:
• 你有5个EAR应用(超过2000个类)部署(也包括中间件代码)。
• 你的本地堆需求被预估在1Gb(需要足够大去处理线程创建)。
• 你的持久代空间预估在512 MB。
• 你的内部静态数据缓存预估在500MB.
• 你在高峰期预估的流量每小时总共有5000个并发用户。
• 每个用户会话数据占用预估在500 K。
• 整个会话数据的占用需求在高峰期有2.5 GB。

如你所见,像这种需求是不可能将所有的流量都放在单个32位JVM进程上。一个典型的处理方法是分离这些流量到几个JVM进程上或者物理机上(假定你有足够的硬件和CPU核数可用)。

然而,对于这个例子,给了这么多内存的要求还要保证一个可扩展的环境能长时间运行,我会建议用64位虚拟机,并用小一点的堆作为初始值比如3Gb来减小GC成本。如果你确定要给老年代额外的缓存空间,我通常会建议内存占用达到50%的时候再交给major收集,这是为了维持低的Full GC频率和足够的缓存应对容错事件。

大多数时间,你的业务流量会用掉你大部分的内存,除非你需要用大批的数据缓存来达到合适的性能,这通常用于大型门户(媒体)应用。太多的数据缓存也会产生很多的警告,你也许在之后需要重新审视和设计。

4 – 不要猜测,要测量

在这里你应该:
• 了解JVM规则和内存空间的基础知识。
• 对所有应用以及他们的特点(大小,类型,动态流量,无状态vs有状态对象,内部缓存等等)有很深的认识和理解。
• 对你的每一个应用的业务流量(比如并发用户等)有很好的了解和前瞻预估。
• 对是否需要64位的虚拟机有自己的看法,并且知道初始参数设置。
• 对是否需要更多JVM(中间件)进程有自己的看法。

但是等一下,你的工作还没完。尽管上面的信息对于你想出“最好的推测”Java堆设置是很关键和有用的,但最好的建议还是在你的真实应用上模拟操作并且通过合适的分析,负载和性能测试去验证Java堆内存的需求。

你可以学习和利用一些工具如JProfiler。我自己的观点是,学习如何使用分析工具是最好的方法,它能使你恰当的了解应用中的内存占用的。另一种方法我经常用在生产线上的是堆转储分析,可以用Eclipse MAT工具。堆转储分析是非常有用的,能让你对整个堆内存占用有一个全局认识和理解,包括类加载相关的数据,它在任何内存占用分析中都是必做的操作,尤其是内存泄露。
这里写图片描述

Java分析器和堆转储分析工具可以使你了解和确认你应用程序的内存占用,包括内存泄露的检测和解决。负载和性能测试也是必做的,通过模拟前期并发用户它可以帮你验证之前的预测。同时也能暴露你应用程序的瓶颈以及使你更好的调整JVM参数设置。你可以用类似Apache JMeter工具,它简单易学,当然也可以使用一些其他的商业化的产品。

最后,我经常看到Java EE环境能运行得非常好,直到有一天基础设施开始出现问题比如硬件问题。很快运行环境中各种指标数量持续的下降 (JVM进程减少),然后整个环境开始崩溃。到底发生了什么?

有许多情形会导致连环影响,但是缺少JVM调优和缺乏处理容错(短期额外负载)的能力是最常见的。如果你的JVM进程运行在老年代空间里对象占比80%+的状态下并且频繁的垃圾收集,你如何期望它能处理任何容错情况?

你可以在前期进行模拟这种情形下的负载性能测试,这样就可以调整合适的设置,你的Java堆要有足够的缓存来处理短期内额外的负载(多余的对象)。这对于动态内存占用是最主要也是最适用的方式,因为容错意味着将某个百分比的并发用户转发到其他可用的JVM进程(如中间件)上去。

5 – 划分与战胜

到这里,你已经完成了几十次的负载迭代测试。你知道你的JVM没有泄露内存。你的应用程序占用也不会继续降低了。你试过好几种调整策略比如使用64位Java堆并设置10G以上内存,以及多种GC策略,但是仍然没有找到合适的可接受的性能指标?

在我的经验中,我发现,基于当前的JVM参数规格,在每个物理主机上创建几个JVM进程,并跨越多个主机,这种适当的横向和纵向伸缩性可以带给你需要的吞吐量和容量。如果在几个逻辑点关掉一些应用(有自己的JVM进程,线程和调优参数),你的IT环境会有更好的容错性。

这种“划分与战胜”策略将你的应用流量分到多个JVM进程中,它能带给你:
• 每个JVM进程(包括静态和动态占用)减少了Java堆大小
• 降低了JVM调优的复杂性
• 降低了每个JVM进程消耗和暂停的时间
• 增强了冗余和容错能力
• 能和最新的云端IT虚拟化策略保持一致

总结下,当你发现你花了太多时间在64位JVM上调优,是时候重新审视你的中间件和JVM部署策略,并充分利用横向纵向伸缩性的优点。完成这些策略虽然很费劲,但是却能给你长时间运行带来好结果。

译者介绍:
梅小西
Java工程师,关注JVM,并发编程,喜欢研究Python,Scala,Golang等。

译者相关译文:
JVM内部原理
《JVM故障诊断指南》之1 ——JVM概览与介绍
《JVM故障诊断指南》之2 ——调整合适的Java堆大小的技巧
《JVM故障诊断指南》之3 ——Java 线程: JVM持有内存的分析
《JVM故障诊断指南》之4 ——Java 8:从持久代到metaspace

FavoriteLoading添加本文到我的收藏
  • Trackback are closed
  • Comments (0)
  1. No comments yet.

You must be logged in to post a comment.

return top