JVM学习之方法区

155 阅读6分钟

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

  • 三者的交互关系从一行简单的代码出发
Person p = new Person();

1、类信息Person存储在方法区中
2、引用p存储在栈中
3new Person()这个操作主要在堆上进行

二、方法区的理解

1、方法区在哪里?

  • 方法区被看作是一块独立于堆的内存空间

2、一些理解

  • 方法区与堆都是线程共享

  • 方法区在jvm启动的时候就被创建了,而且它的实际的物理内存空间与堆一样,可以不连续

  • 方法区的大小可以固定大小也可以动态扩展,这点和堆一样

  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,可能会使方法区溢出,出现OOM。所以方法区溢出的一个原因就是加载了过多的类。

  • 关闭jvm就是释放方法区的内存

三、设置方法区大小与OOM

1、不同jdk版本设置方法区大小

jdk7及以前版本

  • -XX:PermSize:设置用接待初始化空间,默认为20.75M
  • -XX:MaxPermSize:设置永久代最大可分配空间。32位机器为64M,64位机器为82M

jdk8及以后版本:

  • -XX:MetaspaceSize:设置元空间的初始大小
  • -XX:MaxMetaspaceSize:设置元空间的最大大小

2、解决OOM的方法论

  1. 先使用内存映像分析工具堆dump出来的堆转储快照进行分析。重点是确定是内存溢出还是内存泄露
  2. 如果是内存泄露,可进一步通过哦工具查看泄露对象到GC Roots的引用链。这样便可以找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器不发自动回收对象的。掌握了西楼对象的类型信息,以及GC Roots引用连的信息,就可以比较准确的定位出泄露代码的位置。
  3. 如果不存在内存泄露,换句话说就是内存中的对象确实都需要存活,那就应该检查虚拟机的堆参数,与机器物理内存对比是否可以调大;从代码检查是否存在某些对象生命周期过长、持有状态时间过长的情况。

四、方法区的内部结构

1、方法区存储什么?

  • 存储已经被虚拟机加载的类的类型信息、常量、静态变量、即时编译器编译后的代码缓存、域信息、方法信息。
  • 类型信息:对每个加载的类型(class,interface,enum,annotation)jvm必须在方法区中存储一下类型的信息:
    • 这个类型的完整的有效名称
    • 这个类型直接父类的完整有效名
    • 这个类型的修饰符
    • 这个类型有接口的一个有序列表
  • 域信息:保存属性的相关信息以及域的声明顺序。包含:域名称、域类型、修饰符
  • 方法信息:包含方法名称、返回类型、参数和类型、修饰符、方法的字节码、操作数栈、局部变量表
  • final修饰的值:final修饰的常量在编译的时候就已经分配了值;static修饰的值在链接的第二个阶段:准备阶段做初始化。

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

  • 一个有效的字节码文件中包含类的版本信息、字段、方法以及接口等描述信息外,还包含常量池,常量池中包含各种字面量和对类型、域和方法的符号引用

常量池

  • class文件的一部分

  • 为什么需要常量池呢?

    • 其实主要解释为什么存储的是符号引用?主要是不能将所有需要的类直接加载记录下来,所以通过符号引用去找对应的类
  • 常量池中存储的数据类型包含哪些?

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

运行时常量池

  • 方法区的一部分
  • 在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
  • 具有动态性

五、方法区的使用举例

六、方法区的细节演示

1、变迁历史

  • 这边讨论的内容都是在Hotspot虚拟机上讨论
jdk版本变化
jdk1.6及以前有永久代,静态变量存放在永久代上
jdk1.7有永久代,但已经在逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
jdk1.8无永久代;类型信息、字段、方法、常量保存在本地内存的元空间;字符串常量池、静态变量保存在堆中

2、为什么永久代会被元空间替换?

  • 主要是永久代的大小不能很好的被设置:在不断的加载类时,需要不断的申请永久代的大小,虽然前面说过可以设置永久代的初始值和最大值,但是这两个值都制约着系统性能。一方面设置太小容易引起OOM;另一方面设置太大容易浪费虚拟机资源。
  • 对永久代的调优比较困难:主要是类的回收的条件比较苛刻,这个后面会说。

3、StringTable(字符串常量池)为什么会被调整?

  • 永久代的GC的效率比较低,只有FullGC时才会触发回收字符串。我们在开发时,常常会用到i很多字符串,所以调整到堆中。堆中的GC效率相比永久代要高。

4、静态变量存放在哪里?

  • 在前面的变迁史中说到过静态变量存放在哪里;但是这边说的内容和上面有些不同。
class A{
    public static byte[] arr = new byte[1024*1024*100];
    main()方法
}
参数:-Xms200m -Xmx200m -XX:PermSize=300m  -XX:MaxPermSize=300m 
  • 上面的这个代码,运行后得出的结论是什么呢?
    • 通过new关键字得到的数组,不论jdk版本的变迁,都是保存在堆上
    • 引用arr会随着不同jdk版本保存在不同的地方,具体位置参考上面的变迁史的列表

七、方法区的垃圾回收

  • java虚拟机规范没有特别要求需要在方法区进行GC
  • 一般来说在方法区的回收效果比较差,特别是类型的卸载。
  • 方法区的GC主要收集下面两部分的内容:常量池的废弃常量和不在使用的类型

1、常量池的回收

  • 主要常量没有被任务地方引用就可以被回收

2、类的回收

  • 需要满足一些条件才能被回收
    • 该类的所有实例都已经被回收了,包含子类、父类
    • 该类的类加载器被卸载
    • 该类对应的java.lang.class对象没有被任何地方引用。
  • 满足以上条件后,仅仅是有可能被回收