栈、堆、方法区三者之间的关系如上图所示, 第一个Student代表类型,存放在方法区中, student 变量是在虚拟机栈中,最后 new 出来的Student对象放在堆中。本篇主要讲一下有关方法区中的知识。
文章目录
- 概念
- 方法区的内部结构
- 方法区的变化
概念
方法区是各个线程共享的运行时内存区域,它用来存储每一个类的结构信息,比如:运行时常量池、字段、方法数据、构造方法等。方法区是在虚拟机启动时就创建完成了,且在实际内存空间上不是连续的。虽然方法区在逻辑上属于堆的一部分(原因是二者在垃圾回收、内存分配上很相似),但实际上方法区是独立于java堆的内存空间,二者存放的内容是不一样的。需要注意的是:
1、方法区与堆一样,是各个线程共享的内存区域。
2、方法区在jvm启动的时候被创建,并且它实际的物理内存空间和虚拟机堆区一样都可以不是连续的。
3、方法区的大小和堆空间一样,可以选择固定大小或者扩展,它的大小可以决定系统保存多少个类,如果系统定义了太多的类,会导致方法区oom。(加载大量的第三方jar包,tomcat部署的工程过多,大量动态地生成反射类。)
方法区的内部结构
当java源代码编译成class文件,class文件被加载到运行时数据区后,class文件中的一部分信息会加载到方法区中,比如类class、接口interface、枚举enum以及运行时常量池等类型信息。
下面是方法区存放的一些内容:
一、类型信息
当加载一个class时,方法区需存放下面内容:
1、完整有效的类名,包括包名和类名。
2、直接父类的完整有效名。
3、访问修饰符(public、abstract)
4、直接接口的列表
二、域信息
域信息包括域名称、域类型、域修饰符。
三、方法信息
1、方法名称
2、方法的返回类型
3、方法参数的数量和类型
4、方法的访问修饰符
5、方法的字节码、操作数栈深度、局部变量表大小。
6、异常表
四、类变量和类常量
类变量是static修饰的成员变量,它可以看成是和类一个级别的,随着类加载而加载,类变量被类的所有实例共享。在jdk7之前类变量也是方法区的一部分,jdk7以及以后放到了堆空间。用final static修饰的成员变量叫静态常量,静态常量和静态变量的区别是静态常量在编译期就已经为其赋值。
五、常量池和运行时常量池
常量池(constant pool)是class 中的信息,当class 文件加载到内存中时,方法区会存放class文件的常量池信息,这时候常量池就变成了运行时常量池。
一个java应用程序中所有java类的常量池组装成了jvm中最大的运行时常量池。常量池中包含数据类型有:数值、字符串值、类引用、字段引用、方法引用。
常量池是class文件的一部分,用于存放编译期间生成的各种字面量和符号引用,这部分内容在类加载之后就放到了方法区中的运行时常量池内。
虚拟机加载类或接口之后,就会创建对应的运行时常量池(也就是说每一个类有一个常量池,加载后对应一个运行时常量池)。之所以叫做运行时常量池,是因为它除了包含编译期间的常量,还包含运行时解析后获得的方法或字段引用。最常见的就是String 的intern()方法。
方法区的变化
在jdk7及以前,习惯上把方法区称为永久代(因为里面的东西基本是不会变的,只有Full Gc的时候才会清除一下),在jdk8开始以及之后,方法区改名为元空间。永久代和元空间的本质类似,都是对jvm中方法区的实现,但是永久代和元空间最大的区别在于元空间不在虚拟机设置的内存中,而是在本地内存中。
jdk6以及之前:存在永久代,静态变量在永久代中
jdk7:存在永久代,但已经逐步去除永久代,字符串常量池、静态变量移除,保存在堆中
jdk8以及以后:无永久代,取而代之的是元空间,字符串常量池、静态变量仍然在堆中。
jdk6的方法区:
jdk7的方法区:
jdk7对字符串常量池的位置进行了调整,因为永久代的回收效率很低,只有在Full GC才会触发,导致字符串常量池回收效率不够高,我们创建的很多字符串并不需要永久保存,如果不回收的话,就会导致永久代内存不足,所以jdk7将字符串放到了堆中,内存就能及时回收利用。
移动静态变量的道理是一样的。
jdk8的方法区:
jdk8中的永久代改名为元空间,通过名字可以看出里面放的是一些元数据,就是有关类相关的信息。它最大的好处是占用的是本地内存,并不是堆,从而减少了堆内存的占用。