JVM-方法区(元空间)

一、栈、堆、方法区的交互关系

从线程共享与否的角度来看:

 下面一个简单的图示,说明了栈、堆、方法区之间关系:

  • person 对象的引用存放在栈中
  • new Person() 对象本身存放在堆中
  • Person 生成的结构,class文件存放在方法区

二、方法区的理解

方法区是线程共享的,存放的java文件编译后的指令(class)。

尽管所有方法区在逻辑上都属于堆的一部分,但一些简单的实现可能不会选择去进行GC或者进行压缩。但是HtoSpotJVM,把方法区叫做Non-Heap,目的就是要与堆分开。所以方法区看做是独立于java堆的内存空间。

  • jvm启动的时候创建,物理上可以不连续
  • 大小固定或者扩展
  • 大小决定了保存多少个类,可能OOM:Metaspace/PermGen Space
  • jvm关闭后,方法区释放

Hotspot中方法的演进:

  • jdk7-永久代、jdk8-元空间
  • 永久代和方法区并不等价,但是在Hotspot JVM中,是等价的,永久代还是使用java内存(-XX:MaxPermSize),更容易出现OOM
  • jdk8之后,使用元空间代替了永久代,也使用了本地内存,相对更不容易出现OOM了,同时内部实现也改变了

三、设置防区大小与OOM

大小可以设置成固定或者可变的。

jdk7及以前:

  • -XX:PermSize,默认值是20.75M
  • -XX:MaxPermSize,32位默认64M,64位默认82M

jdk8及以后:

  • -XX:MetaspaceSize,win平台 21M
  • -XX:MaxMetaspaceSize,win平台无限制
  • MetaspaceSize满了之后,会触发Full GC,所以初始值甚至的大一些

动态反射,会一直创建新的类,一直到OOM。

遇到OOM怎么解决:

  • 导出dump,分析文件,是内存泄露(Memory Leak)还是内存溢出(Memoru Overflow)
  • 如果是内存泄露,查看GC Roots引用链。
  • 如果不是内存泄露,那么就调整堆大小,或者调整对象的生命周期

四、方法区的内部结构

主要存放类信息和运行时常量池(字符串常量)。

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

 1、类型信息(class、interface、enum、annotation),必须存储如下信息

  • 这个类型的完整有效名称(全名=包名.类名)
  • 这个类型直接父类的完整有效名称(interface、Object 都没有父类)
  • 这个类型的修饰符(public,abstract,final的某个子类)
  • 这个类型直接接口的一个有序列表

2、域信息(属性\成员变量)

  • jvm在方法区必须保存类型的所有域信息以及域的声明顺序
  • 域的类型包括:域名称、域类型、域修饰符

3、方法信息

  • 方法名称
  • 方法返回值
  • 方法参数的梳理和类型(按顺序)
  • 方法的修饰符
  • 方法的字节码、操作数栈、局部变量表及大小
  • 异常表,每个异常的开始位置、结束位置、代码处理在程序计数器中的便宜地址、被捕获的异常类的常量池索引

类加载到方法区之后,会记录是哪个ClassLoader加载的自己,ClassLoader也会记录了加载了哪些类。

non-final变量:

  • 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分
  • 类变量被类的所有实例共享,即使没有类实例是,也可以访问它

全局常量(static final):被声明为final的类变量的处理方法不同,每个全局常量在编译的时候就被分配了,就是在编译的时候,值就确定了。static是在初始化的时候才会赋值。

运行时常量池VS常量池

常量池就是字节码的一部分(Constant Pool Table),包含了字面量和对类型、域、方法的符号引用。

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

一段程序运行成功,需要的关联的各种类会很多,所以这些用到的信息,作为一个符号出现也就是符号引用,当真正使用的时候,才运行真正的程序。其实就是为了减小程序。

常量池可以看做是一张表,JVM质量根据这张表,找到需要执行的类名、方法名、参数类型、字面量等类型。

字节码文件的常量池,经过ClassLoader加载到方法区之后,就成为运行时常量池(Runtime Constant Pool),但是相对而言具备了动态性。

五、方法区的使用举例

 

六、方法区的演进细节

Hotspot的方法区的变化:

jdk6及之前 有永久代(permanent generation),静态变量放在永久带上
jdk7                 有永久代,但是已经逐渐开始“去永久代”,字符串常量池、静态变量移除,保存在堆中
jdk8及之后 无永久代,类型信息、字段、方法、常量保存在本地内存元空间,但是字符串常量池、静态变量仍然在堆中

 

 

为什么替换:

  • 永久代设置空间大小,很难确定,如果加载类多,那么容易出现OOM,空间小也容易出现Full GC
  • 对永久代调优很困难,判断类或者常量不在使用也是耗费时间,所以STW时间很长

StringTable、静态变量为什么调整位置:

1、StringTable:因为永久代垃圾回收效率很低,在Full GC的时候才会触发,而Full GC是老年代的空间不足、永久代空间不足才会触发。这就是导致StringTable回收效率低。而在开发中,会大量创建字符串,回收效率低,到时永久代内存不足。放到堆里,才能及时回收。

2、静态变量存放在哪里:静态引用对应的对象都放在堆空间中,静态变量的引用,从jdk7开始存放在堆中了

七、方法区的垃圾回收

因为类的卸载条件很苛刻,所以方法区的垃圾回收效果很难令人满意,但是和部分的GC有时确实需要。

主要回收的内容:常量池中废弃的常量和不在使用的类型

判断类不在使用的三个条件:

  • 所有对象都被回收,也就是堆中不存在该类及其任何派生子类的实例
  • 加载该类的ClassLoader已经回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用

八、总结

猜你喜欢

转载自blog.csdn.net/liming0025/article/details/121480643