永久代
在自定义类加载器还不是很常见的时候,类大多是static的,很少被卸载或收集,因此被成为“永久的(Permanent)”。
同时,由于类class是JVM实现的一部分,并不是由应用创建的,所以又被认为是“非堆(Non-Heap)”内存。
- 在JDK8之前的HotSpot JVM,存放这些“永久的”区域叫做“永久代(permanent generation)”。
- 永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数
-XX:MaxPermSize
。-XX:MaxPermSize
:永久代的最大可分配内存空间。默认大小64M(64位JVM由于质真膨胀,默认是85M)。当JVM加载的类信息容量超过了参数设定的值时,应用将会报OOM的错误 - 永久代的垃圾收集和老年代(old generation)捆绑在一起,因此无论哪个满了,都会出发永久代和老年代的垃圾收集。
java.lang.OutOfMemoryError:PremGen space
这里的“PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是JVM的规范,而后者则是JVM规范的一种实现。并且只有HotSpot才有“PermGen space”,而对于其他类型的虚拟机,如JRockit(Oracle)、J9(IBM)并没有“PermGen space”。- 由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。并且JDK 1.8中参数PermSize和MaxPermSize已经失效
移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除
注意:永久代的移除并不代表自定义的类加载器泄露问题就解决了。因此,你还必须监控你的内存消耗情况,因为一旦发生泄漏,会占用你的大量本地内存,并且还可能导致交换区交换更加糟糕。
元空间(metaspace)
JDK 8的HotSpot JVM现在使用的是本地内存来表示类的元数据,这个区域就叫做元空间。
元空间这个东西,是在JDK8以后才存在的,JDK7及以前,只有永久代
元空间的存储位置是在计算机的内存当中,而永久的存储们置是在JVM的地堆中
每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间
只有class元数据才在元空间。
持久代的空间被彻底地删除了,它被一个叫元空间的区域所替代了。持久代删除了之后,很明显,JVM会忽略PermSize和MaxPermSize这两个参数,还有就是你再也看不到java.lang.OutOfMemoryError: PermGen error的异常了。原来类的静态变量和Interned Strings 都被转移到了java堆区,
元空间内存管理
元空间的内存管理由元空间虚拟机来完成。先前,对于类的元数据我们需要不同的垃圾回收器进行处理,现在只需要执行元空间虚拟机的C++代码即可完成。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。话句话说,只要类加载器存活,其加载的类的元数据也是存活的,因而不会被回收掉。
JVM用元空间代替永久代的原因:
1随着操作系统的发展,计算机支持的内存从32位的最大2^32字节,变为64位的最大
2^48字节。
2随着Java 在Web领域的发展,java程序变的得越来越大,需要加载的内容也越来越多,如果
使用永久代实现方法区,那么需要手动扩大堆的大小,而使用元空间之后,就可以直接存
储在内存当中,不用手动云修改堆的大小。
主要原因:
- 字符串存在永久代中,容易出现性能问题和内存溢出, 由于永久代内存经常不够用或发生内存泄露,爆出异常 java.lang.OutOfMemoryError: PermGen
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 可能会将HotSpot 与 JRockit 合二为一。
- 永久代空间大小很难确定,太小容易GC/OOM异常,太大占用内存(元空间并不在虚拟机中、而是使用本地内存,大小仅受本地内存限制)
- 永久代调优困难
- 垃圾回收频率低
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
元空间的特点:
- 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
- 每个加载器有专门的存储空间
- 只进行线性分配
- 不会单独回收某个类
- 省掉了GC扫描及压缩的时间
- 元空间里的对象的位置是固定的
- 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
元空间的内存分配模型
- 绝大多数的类元数据的空间都从本地内存中分配
- 用来描述类元数据的类也被删除了
- 分元数据分配了多个虚拟内存空间
- 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
- 归还内存块,释放内存块列表
- 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
- 减少碎片的策略
元空间的调优
使用-XX:MaxMetaspaceSize参数可以设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少。-XX:MetaspaceSize选项指定的是元空间的初始大小,如果没有指定的话,元空间会根据应用程序运行时的需要动态地调整大小。
MaxMetaspaceSize的调优
- -XX:MaxMetaspaceSize={unlimited}
- 元空间的大小受限于你机器的内存
- 限制类的元数据使用的内存大小,以免出现虚拟内存切换以及本地内存分配失败。如果怀疑有类加载器出现泄露,应当使用这个参数;32位机器上,如果地址空间可能会被耗尽,也应当设置这个参数。
- 元空间的初始大小是21M——这是GC的初始的高水位线,超过这个大小会进行Full GC来进行类的回收。
- 如果启动后GC过于频繁,请将该值设置得大一些
- 可以设置成和持久代一样的大小,以便推迟GC的执行时间
CompressedClassSpaceSize的调优
- 只有当-XX:+UseCompressedClassPointers开启了才有效
- -XX:CompressedClassSpaceSize=1G
- 由于这个大小在启动的时候就固定了的,因此最好设置得大点。
- 没有使用到的话不要进行设置
- JVM后续可能会让这个区可以动态的增长。不需要是连续的区域,只要从基地址可达就行;可能会将更多的类元信息放回到元空间中;未来会基于PredictedLoadedClassCount的值来自动的设置该空间的大小
元空间的一些工具
- jmap -permstat改成了jmap -clstats。它用来打印Java堆的类加载器的统计数据。对每一个类加载器,会输出它的名字,是否存活,地址,父类加载器,以及它已经加载的类的数量及大小。除此之外,驻留的字符串(intern)的数量及大小也会打印出来。
- jstat -gc,这个命令输出的是元空间的信息而非持久代的
- jcmd GC.class_stats提供类元数据大小的详细信息。使用这个功能启动程序时需要加上-XX:+UnlockDiagnosticVMOptions选项。
提高GC的性能
如果你理解了元空间的概念,很容易发现GC的性能得到了提升。
- Full GC中,元数据指向元数据的那些指针都不用再扫描了。很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了。
- 元空间只有少量的指针指向Java堆。这包括:类的元数据中指向java/lang/Class实例的指针;数组类的元数据中,指向java/lang/Class集合的指针。
- 没有元数据压缩的开销
- 减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)
- 减少了Full GC的时间
- G1回收器中,并发标记阶段完成后可以进行类的卸载
总结
- Hotspot中的元数据现在存储到了元空间里。mmap中的内存块的生命周期与类加载器的一致。
- 类指针压缩空间(Compressed class pointer space)目前仍然是固定大小的,但它的空间较大
- 可以进行参数的调优,不过这不是必需的。
- 未来可能会增加其它的优化及新特性。比如, 应用程序类数据共享;新生代GC优化,G1回收器进行类的回收;减少元数据的大小,以及JVM内部对象的内存占用量。