JVM演变,元空间学习
文章目录
1. 演变历史
基于 HotSpot 虚拟机,从 JDK1.6、JDK1.7 和 JDK1.8 三个版本进行学习,这三个版本的更新可以说是从PermGen到Metaspace 的演变。
1.1 JDK 1.6
在1.6及之前版本的JVM中,方法区是通过永久代实现的,字符串常量池、运行时常量池、都在该区域。
1.2 JDK 1.7
在1.7版本中,将全局字符串常量池从方法区(永久代)中移动到堆内存中,这个时候还保留着永久代。
1.3 JDK 1.8
在1.8版本,永久代被完全移除,所以永久代的参数-XX:PermSize
和-XX:MaxPermSize
也被移除了。类的元信息存储在元空间中,静态变量和字符串常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
参考:JEP 122: Remove the Permanent Generation:http://openjdk.java.net/jeps/122 ,部分截图如下
永久代被移除之后,方法区还在吗?方法区当然还在,方法区是JVM的规范,在1.8之前,方法区是由永久代实现,在1.8之后,则通过元空间实现。
一个问题,java8中我也不知道运行时常量池到底位于哪一个区域中,网上也是众说纷纭,个人认为在元空间中,符号引用(Symbols)转移到了元空间;字面量转移到了java heap;类的静态变量转移到了java heap
1.4 为什么移除永久代(了解)
在原来的永久代划分中,每当一个类初次被加载的时候,它的元数据都会放到永久代中。但是永久代的内存空间也是有大小限制的,如果加载的类太多,很有可能导致永久代内存溢出;同时,永久代大小也不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,但是PermSize指定太小又很容易造成永久代内存溢出;同时,HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。永久代会为GC带来不必要的复杂度,并且回收效率偏低。将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。
其实总结下来大致分为3点
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值,如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量。但是,永久代的移除并不代表自定义的类加载器泄露问题就解决了。
2. 元空间 Metaspace
2.1 什么是元空间
元空间是方法区的实现,其的本质与永久代类似。元空间与永久代之间区别在于:元空间使用本地内存,不在JVM虚拟机中 ,因此元空间的大小仅受本地内存限制。
2.2 配置
- UseLargePagesInMetaspace
- InitialBootClassLoaderMetaspaceSize
- MetaspaceSize:初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数。
- MaxMetaspaceSize:限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。
- CompressedClassSpaceSize:默认1G,这个参数主要是设置Klass Metaspace的大小,不过这个参数设置了也不一定起作用,前提是能开启压缩指针,假如-Xmx超过了32G,压缩指针是开启不来的。如果有Klass Metaspace,那这块内存是和Heap连着的
2.3 组成
有Klass Metaspace
和NoKlass Metaspace
组成
- Klass Metaspace就是用来存klass的,klass就是class文件在jvm里的运行时数据结构。这块内存是紧接着Heap的,这块内存大小可通过
-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是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作
2.4 内存分配模型
- 大部分类元数据并不在虚拟机,而是在本地内存分配。(这里的本地内存应该就是NativeMemory,供JVM进程使用)
- 用于描述类元数据的“klasses“已经被移除
- 分元数据分配了多个虚拟内存空间
- 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
- 归还内存块,释放内存块列表
- 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
- 减少碎片的策略
- Metaspace可以动态增长
2.5 容量
新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。
2.6 垃圾回收
- 对于僵死的类及类加载器的垃圾回收将在元数据使用达到
MaxMetaspaceSize
参数的设定值时进行。 - 适时地监控和调整元空间对于减小垃圾回收频率和减少延时是很有必要的。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。
2.7 其他特点
- 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
- 每个加载器有专门的存储空间
- 只进行线性分配
- 不会单独回收某个类
- 省掉了GC扫描及压缩的时间
- 元空间里的对象的位置是固定的
- 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
Reference :
https://github.com/igithu/java-summary/wiki/JDK中的变化:-PermGen--VS-MetaSpace-总结