领先一步
VMware提供培训和认证,以加速您的进步。
了解更多JVM可能是一个复杂的系统。幸运的是,大部分复杂性都隐藏在底层,作为应用程序开发人员和部署人员,我们通常不必过多担心。随着基于容器的部署策略的兴起,一个需要关注的复杂性领域是JVM的内存占用。
JVM将其内存分为两大类:堆内存和非堆内存。堆内存是人们通常最熟悉的部分。它存储应用程序创建的对象。它们保留在那里,直到不再被引用并被垃圾收集。通常,应用程序使用的堆大小会随着当前负载而波动。
JVM的非堆内存被划分为几个不同的区域。我们可以使用HotSpot VM的本地内存跟踪 (NMT)来检查其在这些区域的内存使用情况。请注意,虽然NMT不会跟踪所有本地内存使用情况(例如,它不会跟踪第三方本地代码的内存分配),但对于大多数典型的Spring应用程序来说已经足够了。可以使用-XX:NativeMemoryTracking=summary
启动应用程序,然后使用jcmd <pid> VM.native_memory summary
显示内存使用情况摘要。
让我们通过查看一个应用程序(在本例中是我们的老朋友Petclinic)来说明NMT的使用。下面的饼图显示了NMT报告的JVM内存使用情况(减去其自身的开销),当使用48MB最大堆(-Xmx48M
)启动Petclinic时。
正如您所看到的,非堆内存占JVM内存使用的大部分,堆内存仅占总内存的六分之一。在本例中,它大约是44MB(其中33MB是在垃圾收集后立即使用的)。非堆内存总使用量为223MB。
MaxMetaspaceSize
限制。已加载类的数量的函数。ReservedCodeCacheSize
限制。可以通过调整JIT来减少,例如,禁用分层编译。与堆内存相比,非堆内存在负载下的变化较小。一旦应用程序加载了它将使用的所有类并且JIT完全预热后,事情就会稳定下来。要减少压缩类空间的使用,需要垃圾收集加载类的类加载器。这在过去更常见,当时应用程序部署到servlet容器或应用程序服务器时——应用程序的类加载器会在应用程序卸载时被垃圾收集——但在现代应用程序部署方法中很少发生。
配置JVM以有效利用给定数量的可用RAM并非易事。如果您使用-Xmx16M
启动JVM并期望它最多使用16MB的RAM,那么您将面临一个令人不快的意外。
JVM大小调整的一个有趣领域是JIT的代码缓存。默认情况下,HotSpot JVM将使用最多240MB。如果代码缓存太小,JIT将耗尽空间来存储其输出,从而导致性能下降。如果缓存太大,则可能会浪费内存。调整代码缓存大小时,务必查看其对应用程序内存使用量和性能的影响。
在Docker容器中运行时,最新版本的Java现在能够感知容器的内存限制并尝试相应地调整JVM大小。不幸的是,这种大小调整通常会过度分配非堆内存并低估堆内存。假设您有一个在具有2个CPU和512MB可用内存的容器中运行的应用程序。您希望它能够处理更多负载,因此您将CPU加倍到4个,内存加倍到1GB。正如我们上面讨论的那样,堆使用量通常取决于负载,而非堆使用量则少得多。因此,我们希望将大部分额外的512MB内存分配给堆以应对增加的负载。不幸的是,JVM默认情况下不会这样做,它会将其额外内存更平均地分配到堆和非堆区域。
值得庆幸的是,CloudFoundry团队拥有丰富的关于JVM内存占用的知识。如果您正在将应用程序推送到CloudFoundry,则构建包将自动为您应用此知识。如果您没有使用CloudFoudry,或者您想了解更多关于如何调整JVM大小的信息,则设计文档(关于第三版Java构建包内存计算器)提供了一些强烈推荐的进一步阅读材料。
Spring团队花费大量时间思考性能和内存利用率,同时考虑堆和非堆内存使用情况。限制非堆内存使用的一种方法是使框架的某些部分尽可能通用。一个例子是使用反射来创建和注入应用程序bean的依赖项。由于使用了反射,无论应用程序包含多少bean,使用的框架代码量都保持不变。我们使用基于堆的缓存来优化启动时间,并在启动完成后清除此缓存。然后,垃圾收集器可以轻松地回收堆内存,从而使应用程序在处理其工作负载时尽可能多地使用内存。