Java 内存分区之 堆外内存 Metaspace 元空间 取方法区PermGen 而代之

时间:2024-03-24 14:35:37

在稍微了解Java内存分区的时候,大多数文章都是出自深入理解jvm这本书,上来就是给你分了 程序计数器,Java虚拟机栈,本地方法栈,堆,方法区,还有个直接内存,还说方法区里面有个常量池。在写这本书的时候,jdk还在1.6,但是现在2020年jdk都已经jdk14了,虽然还没普及jdk14,但是估计以后都会使用的吧,就像现在基本最低都要使用jdk1.8一样。1.7是在2011年发布的,1.8是14年发布的,时至今日,内存分区已经有了些许变化。虽然理论还是大差不差的。

永久代主要存放以下数据:

  • JVM internal representation of classes and their metadata //类及其元数据的JVM内部表示
  • Class statics //类的静态
  • Interned strings //实际字符串,说的就是常量池吧

从 JDK7 开始,JDK 开发者们就有消灭永久代的打算了。有部分数据移到永久代之外了:

  • Symbols => native memory // 符号引用 >本机内存
  • Interned strings => Java Heap // Interned string => Java堆
  • Class statics => Java Heap //类statics => Java堆

到了 JDK8,这个工作终于完成了,彻底废弃了 PermGen,Metaspace 取而代之。

方法区都存了些什么

  • JVM中类的元数据在Java堆中的存储区域。
  • Java类对应的HotSpot虚拟机中的内部表示也存储在这里。
  • 类的层级信息,字段,名字。
  • 方法的编译信息及字节码。
  • 变量
  • 常量池和符号解析

持久代的大小

  • 它的上限是MaxPermSize,默认是64M
  • Java堆中的连续区域 : 如果存储在非连续的堆空间中的话,要定位出持久代到新对象的引用非常复杂并且耗时。卡表(card table),是一种记忆集(Remembered Set),它用来记录某个内存代中普通对象指针(oops)的修改。
  • 持久代用完后,会抛出OutOfMemoryError "PermGen space"异常。解决方案:应用程序清理引用来触发类卸载;增加MaxPermSize的大小。
  • 需要多大的持久代空间取决于类的数量,方法的大小,以及常量池的大小。

为什么移除持久代

  • 它的大小是在启动时固定好的——很难进行调优。-XX:MaxPermSize,设置成多少好呢?
  • HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)。
  • 简化Full GC:每一个回收器有专门的元数据迭代器。
  • 可以在GC不进行暂停的情况下并发地释放类数据。
  • 使得原来受限于持久代的一些改进未来有可能实现

根据上面的各种原因,永久代最终被移除,方法区移至Metaspace,字符串常量移至Java Heap(待测试确认)

什么是 Metaspace

Metaspace 区域位于堆外,所以它的最大内存大小取决于系统内存,而不是堆大小,我们可以指定 MaxMetaspaceSize 参数来限定它的最大内存。
Metaspace 是用来存放 class metadata 的,class metadata 用于记录一个 Java 类在 JVM 中的信息,包括但不限于 JVM class file format 的运行时数据: 
1、Klass 结构,这个非常重要,把它理解为一个 Java 类在虚拟机内部的表示吧;
2、method metadata,包括方法的字节码、局部变量表、异常表、参数信息等;
3、常量池;
4、注解;
5、方法计数器,记录方法被执行的次数,用来辅助 JIT 决策
6、 其他
虽然每个 Java 类都关联了一个 java.lang.Class 的实例,而且它是一个贮存在堆中的 Java 对象。但是类的 class metadata 不是一个 Java 对象,它不在堆中,而是在 Metaspace 中。

什么时候分配 Metaspace 空间

当一个类被加载时,它的类加载器会负责在 Metaspace 中分配空间用于存放这个类的元数据

什么时候回收 Metaspace 空间

分配给一个类的空间,是归属于这个类的类加载器的,只有当这个类加载器卸载的时候,这个空间才会被释放。
所以,只有当这个类加载器加载的所有类都没有存活的对象,并且没有到达这些类和类加载器的引用时,相应的 Metaspace 空间才会被 GC 释放。

配置 Metaspace 空间

  • -XX:MaxMetaspaceSize:Metaspace 总空间的最大允许使用内存,默认是不限制。

  • -XX:CompressedClassSpaceSize:Metaspace 中的 Compressed Class Space 的最大允许内存,默认值是 1G,这部分会在 JVM 启动的时候向操作系统申请 1G 的虚拟地址映射,但不是真的就用了操作系统的 1G 内存。

Metaspace 和 GC

Metaspace 只在 GC 运行并且卸载类加载器的时候才会释放空间。当然,在某些时候,需要主动触发 GC 来回收一些没用的 class metadata,即使这个时候对于堆空间来说,还达不到 GC 的条件。
Metaspace 可能在两种情况下触发 GC:
1、分配空间时:虚拟机维护了一个阈值,如果 Metaspace 的空间大小超过了这个阈值,那么在新的空间分配申请时,虚拟机首先会通过收集可以卸载的类加载器来达到复用空间的目的,而不是扩大 Metaspace 的空间,这个时候会触发 GC。这个阈值会上下调整,和 Metaspace 已经占用的操作系统内存保持一个距离。
2、碰到 Metaspace OOM:Metaspace 的总使用空间达到了 MaxMetaspaceSize 设置的阈值,或者 Compressed Class Space 被使用光了,如果这次 GC 真的通过卸载类加载器腾出了很多的空间,这很好,否则的话,我们会进入一个糟糕的 GC 周期,即使我们有足够的堆内存。

各个jdk的发行时间

Java 内存分区之 堆外内存 Metaspace 元空间 取方法区PermGen 而代之Java 内存分区之 堆外内存 Metaspace 元空间 取方法区PermGen 而代之

从这个表中我们可以看出一个非常有意思的现象,就是JDK的每一个版本号都使用一个开发代号表示(就是表中的中文名)。而且从JDK1.2.2 开始,主要版本(如1.3,1.4,5.0)都是以鸟类或哺乳动物来命名的. 而它们的bug修正版本(如1.2.2,1.3.1,1.4.2)都是以昆虫命名的。

时间-事件轴

1995年5月23日,Java语言诞生
1996年1月,第一个JDK-JDK1.0诞生
1996年4月,10个最主要的操作系统供应商申明将在其产品中嵌入JAVA技术
1996年9月,约8.3万个网页应用了JAVA技术来制作
1997年2月18日,JDK1.1发布
1997年4月2日,JavaOne会议召开,参与者逾一万人,创当时全球同类会议规模之纪录
1997年9月,JavaDeveloperConnection社区成员超过十万
1998年2月,JDK1.1被下载超过2,000,000次
1998年12月8日,JAVA2企业平台J2EE发布
1999年6月,SUN公司发布Java的三个版本:标准版、企业版和微型版(J2SE、J2EE、J2ME)
2000年5月8日,JDK1.3发布
2000年5月29日,JDK1.4发布
2001年6月5日,NOKIA宣布,到2003年将出售1亿部支持Java的手机
2001年9月24日,J2EE1.3发布
2002年2月13日,J2SE1.4发布,自此Java的计算能力有了大幅提升。
2004年9月30日18:00PM,J2SE1.5发布,是Java语言的发展史上的又一里程碑事件。为了表示这个版本的重要性,J2SE1.5更名为J2SE5.0
2005年6月,JavaOne大会召开,SUN公司公开Java SE 6。此时,Java的各种版本已经更名以取消其中的数字“2”:J2EE更名为Java EE, J2SE更名为Java SE,J2ME更名为Java ME。
2006年11月13日,SUN公司宣布Java全线采纳GNU General Public License Version 2,从而公开了Java的源代码。

附2张自己造的图

jdk1.7的时候,一张jvm内存的gc图

Java 内存分区之 堆外内存 Metaspace 元空间 取方法区PermGen 而代之

这个时候PermGen还是存在的,验证了文章开头的理论。

jdk1.8的时候,一张jvm内存的gc图

Java 内存分区之 堆外内存 Metaspace 元空间 取方法区PermGen 而代之

配置的jvm的参数;

-XX:+PrintGCDetails -Xms200M -Xmx200M -Xmn10M -XX:SurvivorRatio=8 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:CompressedClassSpaceSize=5M

看到方法区真的就不在了,被Metaspace区替代了。也验证文章开头的理论。

 

参考:

深入理解堆外内存 Metaspace

Metaspace 之一:Metaspace整体介绍(永久代被替换原因、元空间特点、元空间内存查看分析方法)