JDK8移除永久代

"welcome to ARTAvrilLavigne Blog"

Posted by ARTAvrilLavigne on May 25, 2020

一、移除永久代的原因

  首先理解方法区与永久代的区别。在Java虚拟机规范中,方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择不在方法区实现垃圾回收与压缩。这个版本的虚拟机规范也不限定实现方法区的内存位置和编译代码的管理策略。所以不同的JVM厂商,针对自己的JVM可能有不同的方法区实现方式。在HotSpot中,设计者将方法区纳入GC分代收集。HotSpot虚拟机堆内存被分为新生代和老年代,对堆内存进行分代管理,所以HotSpot虚拟机使用者更愿意将方法区称为老年代。方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。因此方法区与永久代其实是一个意思哈~~~
  进入正题:HotSpot团队选择移除永久代,移到本地内存(native memory)的元空间(Metaspace)。如下图所示

object

  其中有内因和外因两部分原因:
  一、从外因来说,如下所示JEP 122的Motivation(动机)部分。意思为移除永久代也是为了和JRockit进行融合而做的努力,JRockit用户并不需要配置永久代(因为JRockit就没有永久代)。

This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to 
configure the permanent generation (since JRockit does not have a permanent generation) and 
are accustomed to not configuring the permanent generation.

  二、从内因来说,永久代大小受到-XX:PermSize和-XX:MaxPermSize两个参数的限制,而这两个参数又受到JVM设定的内存大小限制,这就导致在使用中可能会出现永久代内存溢出的问题,因此在Java 8及之后的版本中彻底移除了永久代而使用Metaspace来进行替代。为什么会内存溢出:永久代这一部分用于存放Class和Meta的信息,Class在被加载的时候被放入永久代,它和和存放Instance的堆区域不同,所以如果应用程序会加载很多CLASS的话,就很可能出现OutOfMemoryError:PermGen space错误。这种错误常见在web服务器对JSP进行pre compile的时候。

  元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机JVM内存中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。因此Metaspace具体大小理论上取决于32位/64位系统可用内存的大小,可见也不是无限制的,需要配置参数。在永久代移除后,常量池中的字符串常量池也不再放在永久代了,但是也没有放到元空间里,而是留在了堆中。而运行时常量池和class文件常量池则跟着移到了元空间中。

  java8的内存模型总结如下图所示:

JVM

二、JVM堆与本地内存区别

  操作系统会创建一个进程来执行java程序,而每个进程都有自己的虚拟地址空间,JVM用到的内存(包括堆、栈和方法区)就是从进程的虚拟地址空间上分配的。注意JVM内存只是进程空间的一部分,除此之外进程空间内还有代码段、数据段、内存映射区、内核空间等。JVM的角度看,JVM内存之外的部分叫作本地内存,C语言程序代码在运行过程中用到的内存就是本地内存中分配的。下面我们通过一张图来理解一下:

object

三、元空间的组成

  metaspace其实由两大部分组成:Klass MetaspaceNoKlass 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是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。
  klass相关介绍:blog address

3.1、metaspace的相关参数

  如果要改变metaspace的一些行为,一般会对其相关的一些参数做调整,因为metaspace的参数本身不是很多,所以将涉及到的所有参数都做一个介绍。
1. UseLargePagesInMetaspace
  默认false,这个参数是说是否在metaspace里使用LargePage,一般情况下使用4KB的page size,这个参数依赖于UseLargePages这个参数开启,不过这个参数一般不开。

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

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

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

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

6. 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将会是要分配的内存大小基础上的一个增量。

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

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

8. 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。

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

四、元空间内存管理

  元空间的内存管理由Metaspace VM(元空间虚拟机)来完成。Metaspace VM使用一个块分配器(chunking allocator)来管理Metaspace空间的内存分配。块的大小依赖于类加载器的类型,并且Metaspace VM中有一个全局的可使用的块列表(a global free list of chunks)。
  JDK8之前对于类的元数据我们需要不同的垃圾回收器进行处理,现在只需要执行元空间虚拟机的C++代码即可完成。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。换句话说,只要类加载器存活,其加载的类的元数据也是存活的,因而不会被回收掉。准确的来说,每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。在元空间的回收过程中没有重定位和压缩等操作,但是元空间内的元数据会进行扫描来确定java引用。
  具体管理:元空间虚拟机负责元空间的分配,其采用的形式为组块分配。组块的大小因类加载器的类型而异,在元空间虚拟机中存在一个全局的空闲组块列表。
  1. 当一个类加载器需要组块时,它就会从这个全局的组块列表中获取并维持一个自己的组块列表。
  2. 当一个类加载器不再存活时,那么其持有的组块将会被释放,并返回给全局组块列表。
  3. 类加载器持有的组块chunk又会被分成多个块blocks,每一个块block存储一个单元的元信息(a unit of metadata),而组块中的块是线性分配(指针碰撞分配形式)。组块分配自内存映射区域,这些全局的虚拟内存映射区域以链表形式连接,一旦某个虚拟内存映射区域清空,这部分内存就会返回给操作系统。
  当前存在的改进点:元空间虚拟机采用了组块分配的形式,同时区块的大小由类加载器类型决定。类信息并不是固定大小,因此有可能分配的空闲区块和类需要的区块大小不同,这种情况下可能导致碎片存在。元空间虚拟机目前并不支持压缩操作,所以碎片化是目前最大的问题。

==============================================================================
英文部分原文:
  The Metaspace VM now employs memory management techniques to manage Metaspace. Thus moving the work from the different garbage collectors to just the one Metaspace VM that performs all of its work in C++ in the Metaspace. A theme behind the Metaspace is simply that the lifetime of classes and their metadata matches the lifetime of the class loaders’. That is, as long as the classloader is alive, the metadata remains alive in the Metaspace and can’t be freed.

  We have been using the term “Metaspace” loosely. More formally, per classloader storage area is called “a metaspace”. And these metaspaces are collectively called “the Metaspace”. The reclamation of metaspace per classloader can happen only after its classloader is no longer alive and is reported dead by the garbage collector. There is no relocation or compaction in these metaspaces. But metadata is scanned for Java references.

  The Metaspace VM manages the Metaspace allocation by employing a chunking allocator. The chunking size depends on the type of the classloader. There is a global free list of chunks. Whenever a classloader needs a chunk, it gets it out of this global list and maintains its own chunk list. When any classloader dies, its chunks are freed, and returned back to the global free list. The chunks are further divided into blocks and each block holds a unit of metadata. The allocation of blocks from chunks is linear (pointer bump). The chunks are allocated out of memory mapped (mmapped) spaces. There is a linked list of such global virtual mmapped spaces and whenever any virtual space is emptied, its returned back to the operating system.

  As mentioned earlier, the Metaspace VM employs a chunking allocator. There are multiple chunk sizes depending on the type of classloader. Also, the class items themselves are not of a fixed size, thus there are chances that free chunks may not be of the same size as the chunk needed for a class item. All this could lead to fragmentation. The Metaspace VM doesn’t (yet) employ compaction hence fragmentation is a major concern at this moment.
==============================================================================

五、参考

[1]https://www.jianshu.com/p/66e4e64ff278
[2]https://www.cnblogs.com/duanxz/p/3520829.html
[3]https://blog.csdn.net/pzxwhc/article/details/46722411
[4]https://blog.csdn.net/u010515202/article/details/106056592/
[5]https://www.cnblogs.com/xrq730/p/8688203.html
[6]https://blog.csdn.net/q5706503/article/details/84621210
[7]Monica Beckwith作者英文博客:https://www.infoq.com/articles/Java-PERMGEN-Removed/,中译:https://www.infoq.cn/article/Java-PERMGEN-Removed
[8]蚂蚁金服JVM团队:http://lovestblog.cn/blog/2016/10/29/metaspace/


みなさんのごおうえんをおねがいします~~