JVM内存分配

大家好! 我们希望将今天的资料翻译与Java Developer课程中新线程的发布同时进行,该课程将于明天开始。 好吧,让我们开始吧。

JVM可能是一个复杂的野兽。 幸运的是,这种复杂性大部分都隐藏在内部,我们作为应用程序开发人员和负责部署的人,通常不必为此担心很多。 尽管由于用于在容器中部署应用程序的技术的日益普及,但是值得关注JVM中的内存分配。



两种记忆

JVM将内存分为两大类:堆和非堆。 堆是开发人员最熟悉的JVM内存。 应用程序创建的对象存储在此处。 它们会一直保留在那里,直到被垃圾收集器移除为止。 通常,应用程序使用的堆大小取决于当前负载。

堆外内存分为几个区域。 在HotSpot中,您可以使用本机内存跟踪(NMT)机制来探索此内存的区域。 请注意,尽管NMT不会跟踪所有本机内存的使用( 例如,它不会跟踪第三方代码对本机内存的分配 ),但其功能对于大多数典型的Spring应用程序来说已经足够。 要使用NMT,请使用-XX:NativeMemoryTracking=summary运行该应用程序-XX:NativeMemoryTracking=summary并使用 jcmd VM.native_memory摘要请参阅有关已用内存的信息。

让我们以老朋友Petclinic的NMT为例 。 下图显示了以最大堆大小为48 MB( -Xmx48M )启动Petclinic时,根据NMT数据(减去其自身的NMT开销)使用的JVM内存:



如您所见,堆之外的内存占了使用的大多数JVM内存,并且堆内存仅占总数的六分之一。 在这种情况下,大约为44 MB(其中33 MB在垃圾回收后立即使用)。 堆外内存使用总计223 MB。

本机内存区域

压缩的类空间 :用于存储有关已加载类的信息。 限于MaxMetaspaceSize参数。 一个已加载的类数的函数。

译者注

由于某种原因,作者写的是压缩类空间,而不是整个类区域。 压缩的类空间区域是“类”区域的一部分,并且MaxMetaspaceSize参数限制整个“类”区域的大小,而不仅限于压缩的类空间。 为了限制“压缩类空间”,使用了CompressedClassSpaceSize参数。

从这里
如果打开UseCompressedClassesPointers并使用UseCompressedClassesPointers ,则将本机内存的两个逻辑上不同的区域用于类元数据...
为这些压缩的类指针分配了一个区域(32位偏移量)。 可以使用CompressedClassSpaceSize设置区域的大小,默认情况下为1 GB。
MaxMetaspaceSize适用于已提交的压缩类空间和其他类元数据的空间的总和

如果UseCompressedOops参数并使用UseCompressedOops ,则将本机内存的两个逻辑上不同的区域用于类元数据...

对于压缩的指针,分配了一个内存区域(32位偏移量)。 可以通过CompressedClassSpaceSize设置此区域的大小,默认情况下为1 GB ...
MaxMetaspaceSize参数引用压缩的指针区域与其他类元数据的区域之和。


  • 线程:JVM中的线程使用的内存。 正在运行的线程数的函数。
  • 代码缓存:JIT用于运行它的内存。 一个已加载的类数的函数。 限于ReservedCodeCacheSize 。 您可以减少JIT的设置,例如,通过禁用分层编译。
  • GC(垃圾收集器):存储垃圾​​收集器使用的数据。 取决于使用的垃圾收集器。
  • 符号:存储诸如字段名称,方法签名和插入字符串之类的字符。 过度使用字符存储器可能表示行太固定了。
  • 内部:存储其他区域中未包含的其他内部数据。

差异性

与堆相比,堆外内存在负载下的变化较小。 一旦应用程序加载了将要使用的所有类,并且JIT被完全预热,一切将进入稳定状态。 要减少对Compressed类空间的使用,必须由垃圾收集器删除加载类的类加载器。 在过去,当将应用程序部署在servlet容器或应用程序服务器中时(在从应用程序服务器中删除应用程序时,垃圾收集器会删除应用程序类加载器),这是很常见的,但是在现代的应用程序部署方法中很少发生这种情况。

配置JVM

配置JVM以有效使用可用的RAM并不容易。 如果使用-Xmx16M参数运行JVM,并且希望使用的内存不超过16 MB,那么您将得到一个不愉快的惊喜。

JVM内存的一个有趣区域是JIT代码缓存。 默认情况下,HotSpot JVM最多使用240 MB。 如果代码缓存太小,则JIT可能没有足够的空间来存储其数据,结果将降低性能。 如果缓存太大,则可能会浪费内存。 确定高速缓存的大小时,重要的是要考虑其对内存使用率和性能的影响。

现在 ,在Docker容器中运行时,最新版本的Java 知道容器的内存限制,并正在尝试相应地调整JVM内存的大小。 不幸的是,经常在堆外部分配大量内存,而在堆中分配的内存不足。 假设您有一个运行在带有2个处理器和512 MB可用内存的容器中的应用程序。 您想要处理更多的工作量,并将处理器数量增加到4,内存增加到1 GB。 正如我们上面所讨论的,堆大小通常随负载而变化,并且堆外的内存变化少得多。 因此,我们希望将额外的512 MB中的大多数分配给堆以处理增加的负载。 不幸的是,默认情况下,JVM不会执行此操作,而是会在堆上和堆外的内存之间或多或少地平均分配其他内存。

幸运的是,CloudFoundry团队对JVM中的内存分配有广泛的了解。 如果您将应用程序下载到CloudFoundry,则构建包将自动将这些知识应用于您。 如果您不使用CloudFoudry或想了解有关如何配置JVM的更多信息,建议阅读Java buildpack的第三版内存计算器的描述

这对Spring意味着什么

Spring团队花费大量时间来考虑性能和内存使用,同时考虑在堆上和堆外使用内存的可能性。 限制堆外内存使用的一种方法是使框架的各个部分尽可能通用。 这样的一个示例是使用Reflection创建依赖关系并将其注入到应用程序的Bean中。 通过使用反射,无论您的应用程序中有多少个Bean,您使用的框架代码的数量都保持不变。 为了优化启动时间,我们使用堆上的缓存,在启动完成后清除该缓存。 垃圾回收器可以轻松清理堆内存,以为应用程序提供更多可用内存。

传统上,我们等待您对材料的评论。

Source: https://habr.com/ru/post/zh-CN445312/


All Articles