文章目录

  • 元空间(Meta Space)
  • 1、元空间的基本介绍
  • 2、Meta Space的演进
  • 永久代为什么要被元空间替换
  • 3、设置方法区大小和OOM
  • JDK 8及以后
  • 4、元空间( Meta Space)存储什么?
  • 类型信息
  • 域(Field)信息
  • 方法(Method)信息
  • 静态变量(non-fianal的类变量)
  • 运行时常量池(Runtime Constant Pool)
  • 5、运行时常量池VS常量池


元空间(Meta Space)

1、元空间的基本介绍

在Java 8 中称为元空间(Meta Space),在Java 7 及以前称为方法区。

  • 和Java堆一样,是各个线程共享的内存区域
  • JVM启动时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的
  • 元空间的大小,跟堆空间一样,可以选择固定大小或者可拓展
  • 元空间的大小决定了系统可以保存多少类,如果系统定义了太多的类导致元空间内存溢出,虚拟机同样会抛出内存溢出错误java.lang.OufOfMemoryError:PermGen Space或者java.lang.OutOfMemoryError:Metaspce
  • 加载大量的Jar包
  • 关闭JVM就会释放这个区域的内存

2、Meta Space的演进

  • JDK 8之前,习惯上称方法区为永久代JDK 8开始,使用元空间替代了永久代
  • 本质上:方法区永久代不相等,只是对Hotspot来说的,《Java虚拟机规范》对如何实现方法区,不做统一的要求
  • 从现在的角度来看,之前使用永久代,不是很好的实现方式,导致Java程序更容易OOM
  • JDK 8完全废弃了永久代的概念,改用和JRockit J9一样在本地内要中实现元空间(Metaspce)来代理。
  • 元空间本质上和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代最大的区别在于
  • 元空间不在虚拟机设置的中,而是使用本地内存。
  • 永久代、元空间二者不只是名字的改变,内部结果也调整了
  • 根据《Java虚拟机规范》的的规定,如果元空间无法满足新内存分配需求时,将抛出OOM异常

永久代为什么要被元空间替换

随着Java 8的到来,Hotspot VM中再也见不到永久代了,但是并不意味着类的元数据信息消失了。这些数据被移动到了一个于堆不相连的本地内存区域 ,这个区域就时元空间(Metaspace)

  • 改动的原因
  1. 因为永久代设置空间大小是很难确定的再某些场下,如果动态加载的类过多容易产生Permanent区的OOM。比如实际Web工程中,因为功能点较多,运行过程中,要不断动态加载很多类,经常出现致命错误
  2. 同时对永久代的调优是很困难的
  • 而元空间和永久代最大的区别是:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存限制

3、设置方法区大小和OOM

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整

JDK 8及以后

  • 元空间大小可以使用参数 -XX:MetaspaceSize-XX:MaxMetapaceSize指定。
  • 默认值依赖于平台,windows下-XX:MetaspceSize是21M-XXMaxMetaspaceSize的值时 -1即没有限制
  • 和永久代不同,如果不指定大小,默认情况下,虚拟机会好景所有可用的系统资源内存,如果元空间发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
  • -XX:MetaspceSize:设置元空间大小,对于一个64位的服务器端JVM来说,默认的-XX:MetaspceSize值位21MB。这就时初始化的高水位先,一旦触及着水位线,Full GC就会被触发开始卸载没有用的类

4、元空间( Meta Space)存储什么?

用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码缓存等

  • 元空间的内部结构

类型信息

每个加载的类型(类Class、接口interface)枚举enum、注解annotation,JVM必须在方法区中存储一下类型信息

  • 这个类型完整有效的名称(全名=包名.类名)
  • 这个类型的直接父类的完成有效名(对于Interface或者时java.lang.Object,都没有分类)
  • 这个类型的修饰符(pulbic abstrac、final的某个子集)
  • 这给类型直接接口的一个有序列表

域(Field)信息

  • JVM必须在方法区中保存类型的所有域的相关信息以及声明类型的顺序
  • 域的相关信息:域名称、域类型、域修饰符(public、private、protecte、static、final、volatile、transient的某个子集)

方法(Method)信息

  • JVM必须保存所有方法的以下信息
  • 方法名称
  • 方法的返回值类型(或void)
  • 方法参数的数量和类型(按顺序)
  • 方法的修饰符(public,private,protected,static,fianl,synchronized,native,abstract的一个子集)
  • 方法的字节码(Bytecodes),操作数栈,局部变量表及大小(abstract和native方法除外)
  • 异常表(abstract和native方法除外)
  • 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被复活的异常类的常量池索引。

静态变量(non-fianal的类变量)

  • 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分
  • 类变量被类的所有实实例贡献,即时没有实例时,你也可以通过类直接访问它。 - 补充说明
- 全局变量:static final
  - 被声明位final的类变量的处理方法时比较特殊的,每个全局变量在编译的时候就确定了。

  - 声明为static final的在类字节码中使用具体值替换
  - 编译的时候就进行了替换

运行时常量池(Runtime Constant Pool)

是元空间(方法区)的一部分

  • 在加载类和接口到虚拟机后,就会创建对应的运行时常量池
  • JVM为每个已加载的类型(类或接口)都维护了一个常量池,池中的数据项就像数组项一样,通过索引访问
  • 运行时常量池中包含多种不同的常量,包含编译期就已经明确的数值字面量,包括到运行器解析后才能够获得的方法或者字段引用,此时不再是常量池中的符号地址了,这里就会换成了真实地址

运行常量池,相对于Class文件常量池的另一重要的特征是:具备动态性。

  • 创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛出OutOfMemoryError异常
  • 演进
- jdk 6及之前

  - 有永久代(Permanent Generation),静态变量存放再永久代上。

- jdk 7

  - 有永久代,但已经逐步"去永久代",字符串常量池、静态变量移除、保存再堆中

- jdk 8及以后

  - 无永久代,类型信息、字段、方法、常量保存再本地内存的元空间,但是字符串常量池、静态变量仍在堆中。

5、运行时常量池VS常量池

各自所在位置

  • 元空间(方法区),内部包含了运行时常量池
  • 字节码文件内包含常量池

要弄清方法区,需要理解ClassFile,因为类加载器的信息都在方法区,要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池:

  • 常量池在class文件中的位置
  • 一个有效的字节码文件中除了包含类的版本信息字段方法以及接口等描述信息为,还包含一项信息那就是常量池表(Constant Pool Table),包含各种字面量和对类型方法符号引用
  • 常量池Class文件一部分,用于存放编译器生成的各种字面量符号引用,这部分将在类加载后存放到方法区的运行时常量池中

为什么需要常量池

  • 个java源文件的类、接口、编译后会产生一个字节码文件,而Java中的字节码需要数据支持
  • 通常这种数据会很大以至于不能直接存到字节码里换另一种形式,可以存到常量池中,这个字节码包含了指向常量池的引用,在动态链接的时候,会用到运行时常量池

常量池中有什么?

Java中在常量池中的数据类型包括

  • 数量值
  • 字符串值
  • 类引用
  • 方法引用

常量池可以看作一张表,虚拟机指令根据这张表找到需要执行的类、方法名、参数类型、字面量等类型