前面 我们学习了 Java7 的内存模型,重点了解了它的 Runtime Data Area,今天我们要来学习一下 Java8 HotSpot (TM) VM 内存模型,看看它与 Java7 VM 存在哪些差异。

与 Java7 相比较,Java8 VM 的一个重大更新:完全移除永久代(PermGen),取而代之的为元空间(Metaspace)

![Java8 VM MetaSpace](https://img.i7years.com/blog/Java8 VM MetaSpace.png)

为什么要移除 PermGen

来源:http://openjdk.java.net/jeps/122

Jon Masamitsu 对此的解释如下:

A goal for removing perm gen was so that users do not have to think about correctly sizing it.

Set MetaspaceSize to a value larger than the default, if you know that your applications needs more space for class data. Setting it to a larger size will avoid some number of GC’s at startup. It is not necessary and I do not particularly recommend it unless you want to avoid as many GC’s as possible.

Set MaxMetaspaceSize if you want to limit the space for class data. You might want to do this if you suspect you are leaking classloaders and want the application to stop before it uses up too much native memory. Another case might be where you have multiple applications running on a server and you want to limit how much space each uses for class data.

PermGen 空间的大小在 JVM 启动时就已经分配好了,但是随着动态类加载的情况越来越多,如果设置的太小,则容易出现 OOM 的异常,设置的太大,则容易造成空间浪费。

而 Metaspace 空间的容量则会根据应用程序在运行时的需求动态调整大小,省得为 PermGen 的大小分配操碎了心。

有哪些变化

来源:http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-September/006679.html

PermGen 情况
  • PermGen 被完全移除,对应的控制参数 PermSizeMaxPermSize 也变得无效。
Metaspace 内存分配模型
  • 类的元数据信息(metadata)不再是存储在连续的堆空间上,而是移动到叫做 “Metaspace” 的本地内存(Native memory)中。PermGen 中剩下的一些杂项数据已移至 Java Heap 中。

  • 用于描述类元数据的类 (klasses) 已被删除(klassKlass 及其派生类)。

Metaspace 空间容量
  • 默认情况下,类元数据分配受到可用的本机内存总容量的限制(容量依然取决于你使用 32 位 JVM 还是 64 位操作系统的虚拟内存的可用性)。
  • 可以使用一个新的参数 (MaxMetaspaceSize) 来限制用于类元数据的本地内存。如果没有特别指定,元空间将会根据应用程序在运行时的需求动态调整大小。
Metaspace 垃圾回收
  • 当 class metadata 的使用的内存达到 MetaspaceSize(32 位 clientVM 默认 12Mbytes,32 位 ServerVM 默认是 16Mbytes) 时就会对死亡的类加载器和类进行垃圾收集。 设置 MetaspaceSize 为一个较高的值可以推迟垃圾收集的发生。
  • 为了限制垃圾回收的频率和延迟,适当的监控和调优元空间是非常有必要的。元空间过多的垃圾收集可能表示类加载器内存泄漏或对你的应用程序来说空间太小了。
Java Heap 影响
  • 一些杂乱的数据从 PermGen 移到了 Java Heap,这意味着升级到 JDK8 之后,Java Heap 的大小会有明显的增加。

Metaspace 的组成

来源:http://lovestblog.cn/blog/2016/10/29/metaspace/

组成

  • Klass Metaspace
  • NoKlass Metaspace

组成说明

  • Klass Metaspace 就是用来存 klass 的,klass 是我们熟知的 class 文件在 jvm 里的运行时数据结构,不过有点要提的是我们看到的类似 A.class 其实是存在 heap 里的,是 java.lang.Class 的一个对象实例。这块内存是紧接着 Heap 的,和我们之前的 perm 一样,这块内存大小可通过 -XX:CompressedClassSpaceSize 参数来控制,这个参数默认是 1G,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这种情况下 klass 都会存在 NoKlass Metaspace 里,另外如果我们把 - Xmx 设置大于 32G 的话,其实也是没有这块内存的,因为会这么大内存会关闭压缩指针开关。还有就是这块内存最多只会存在一块。
  • NoKlass Metaspace 专门来存 klass 相关的其他的内容,比如 method,constantPool 等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。这块内存是必须的,虽然叫做 NoKlass Metaspace,但是也其实可以存 klass 的内容,上面已经提到了对应场景。
  • Klass Metaspace 和 NoKlass Mestaspace 都是所有 classloader 共享的,所以类加载器要分配内存,但是每个类加载器都有一个 SpaceManager,来管理属于这个类加载的内存小块。如果 Klass Metaspace 用完了,那就会 OOM 了,不过一般情况下不会,NoKlass Mestaspace 是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。

Metaspace 的几个参数

如果我们要改变 metaspace 的一些行为,我们一般会对其相关的一些参数做调整,因为 metaspace 的参数本身不是很多,所以我这里将涉及到的所有参数都做一个介绍,也许好些参数大家都是有误解的

  • UseLargePagesInMetaspace
  • InitialBootClassLoaderMetaspaceSize
  • MetaspaceSize
  • MaxMetaspaceSize
  • CompressedClassSpaceSize
  • MinMetaspaceExpansion
  • MaxMetaspaceExpansion
  • MinMetaspaceFreeRatio
  • MaxMetaspaceFreeRatio

UseLargePagesInMetaspace

默认 false,这个参数是说是否在 metaspace 里使用 LargePage,一般情况下我们使用 4KB 的 page size,这个参数依赖于 UseLargePages 这个参数开启,不过这个参数我们一般不开。

InitialBootClassLoaderMetaspaceSize

64 位下默认 4M,32 位下默认 2200K,metasapce 前面已经提到主要分了两大块,Klass Metaspace 以及 NoKlass Metaspace,而 NoKlass Metaspace 是由一块块内存组合起来的,这个参数决定了 NoKlass Metaspace 的第一个内存 Block 的大小,即 2 * InitialBootClassLoaderMetaspaceSize,同时为 bootstrapClassLoader 的第一块内存 chunk 分配了 InitialBootClassLoaderMetaspaceSize 的大小

MetaspaceSize

默认 20.8M 左右 (x86 下开启 c2 模式),主要是控制 metaspaceGC 发生的初始阈值,也是最小阈值,但是触发 metaspaceGC 的阈值是不断变化的,与之对比的主要是指 Klass Metaspace 与 NoKlass Metaspace 两块 committed 的内存和。

MaxMetaspaceSize

默认基本是无穷大,但是我还是建议大家设置这个参数,因为很可能会因为没有限制而导致 metaspace 被无止境使用 (一般是内存泄漏) 而被 OS Kill。这个参数会限制 metaspace (包括了 Klass Metaspace 以及 NoKlass Metaspace) 被 committed 的内存大小,会保证 committed 的内存不会超过这个值,一旦超过就会触发 GC,这里要注意和 MaxPermSize 的区别,MaxMetaspaceSize 并不会在 jvm 启动的时候分配一块这么大的内存出来,而 MaxPermSize 是会分配一块这么大的内存的。

CompressedClassSpaceSize

默认 1G,这个参数主要是设置 Klass Metaspace 的大小,不过这个参数设置了也不一定起作用,前提是能开启压缩指针,假如 - Xmx 超过了 32G,压缩指针是开启不来的。如果有 Klass Metaspace,那这块内存是和 Heap 连着的。

MinMetaspaceExpansion

MinMetaspaceExpansion 和 MaxMetaspaceExpansion 这两个参数或许和大家认识的并不一样,也许很多人会认为这两个参数不就是内存不够的时候,然后扩容的最小大小吗?其实不然

这两个参数和扩容其实并没有直接的关系,也就是并不是为了增大 committed 的内存,而是为了增大触发 metaspace GC 的阈值

这两个参数主要是在比较特殊的场景下救急使用,比如 gcLocker 或者 should_concurrent_collect 的一些场景,因为这些场景下接下来会做一次 GC,相信在接下来的 GC 中可能会释放一些 metaspace 的内存,于是先临时扩大下 metaspace 触发 GC 的阈值,而有些内存分配失败其实正好是因为这个阈值触顶导致的,于是可以通过增大阈值暂时绕过去

默认 332.8K,增大触发 metaspace GC 阈值的最小要求。假如我们要救急分配的内存很小,没有达到 MinMetaspaceExpansion,但是我们会将这次触发 metaspace GC 的阈值提升 MinMetaspaceExpansion,之所以要大于这次要分配的内存大小主要是为了防止别的线程也有类似的请求而频繁触发相关的操作,不过如果要分配的内存超过了 MaxMetaspaceExpansion,那 MinMetaspaceExpansion 将会是要分配的内存大小基础上的一个增量

MaxMetaspaceExpansion

默认 5.2M,增大触发 metaspace GC 阈值的最大要求。假如说我们要分配的内存超过了 MinMetaspaceExpansion 但是低于 MaxMetaspaceExpansion,那增量是 MaxMetaspaceExpansion,如果超过了 MaxMetaspaceExpansion,那增量是 MinMetaspaceExpansion 加上要分配的内存大小

注:每次分配只会给对应的线程一次扩展触发 metaspace GC 阈值的机会,如果扩展了,但是还不能分配,那就只能等着做 GC 了

MinMetaspaceFreeRatio

MinMetaspaceFreeRatio 和下面的 MaxMetaspaceFreeRatio,主要是影响触发 metaspaceGC 的阈值

默认 40,表示每次 GC 完之后,假设我们允许接下来 metaspace 可以继续被 commit 的内存占到了被 commit 之后总共 committed 的内存量的 MinMetaspaceFreeRatio%,如果这个总共被 committed 的量比当前触发 metaspaceGC 的阈值要大,那么将尝试做扩容,也就是增大触发 metaspaceGC 的阈值,不过这个增量至少是 MinMetaspaceExpansion 才会做,不然不会增加这个阈值

这个参数主要是为了避免触发 metaspaceGC 的阈值和 gc 之后 committed 的内存的量比较接近,于是将这个阈值进行扩大

一般情况下在 gc 完之后,如果被 committed 的量还是比较大的时候,换个说法就是离触发 metaspaceGC 的阈值比较接近的时候,这个调整会比较明显

注:这里不用 gc 之后 used 的量来算,主要是担心可能出现 committed 的量超过了触发 metaspaceGC 的阈值,这种情况一旦发生会很危险,会不断做 gc,这应该是 jdk8 在某个版本之后才修复的 bug

MaxMetaspaceFreeRatio

默认 70,这个参数和上面的参数基本是相反的,是为了避免触发 metaspaceGC 的阈值过大,而想对这个值进行缩小。这个参数在 gc 之后 committed 的内存比较小的时候并且离触发 metaspaceGC 的阈值比较远的时候,调整会比较明显

MetaSpace 与 PermGen 运行时比较

前面提到过,MetaSpace 与 PermGen 最主要的区别就在于,内存空间的是否能够自动扩容上,下面我们来分别演示一下 MetaSpace 与 PermGen 的 GC 的表现形式之间的区别。示例代码 下载

JDK 1.7 @64-bit – PermGen depletion

演示异常:ERROR: java.lang.OutOfMemoryError: PermGen space

VM 参数:

1
-verbose:gc -Xms1024M -Xmx1024M -XX:MaxPermSize=128m -XX:PermSize=128m -XX:+PrintGCDetails -Xloggc:gc.log

JVisualVM 监控显示:

Java8_MetaSpace_1

GC 日志:

gc_1

JDK 1.8 @64-bit – Metaspace dynamic re-size

演示 metaspace 空间不断动态调整的过程

VM 参数

1
-verbose:gc -Xms1024M -Xmx1024M -XX:+PrintGCDetails -Xloggc:gc.log

JVisualVM 监控显示:

JVisualVM_2

GC 日志:

gc_2

JDK 1.8 @64-bit – Metaspace depletion

演示异常:ERROR: java.lang.OutOfMemoryError: Metadata space

VM 参数:

1
-verbose:gc -Xms1024M -Xmx1024M -XX:MaxMetaspaceSize=128m -XX:+PrintGCDetails -Xloggc:gc.log

JVisualVM 监控显示:

JVisualVM_3

GC 日志:

gc_3

参考资料