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

 

图1

 

 

2 方法区的具体理解

1)方法区可以看作是一块独立于java堆的内存空间。

2)方法区于java堆一样,是各个线程共享的内存区域。

3)方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展长度。

4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。

5)关闭JVM会释放这个区域的内存。

3 Hotspot中方法区的演进

jdk7以前,习惯上把方法区称为永久代,jdk8以后,使用元空间取代了永久代。本质上方法区并不等价于永久代,JRockit和J9都没有方法区的概念。

相比之下,使用永久代因为还是使用 的虚拟机本身的内存,所以更加容易导致oom。

jdk8以后,元空间取代了方法区,因为JRockit被收购,二者整合时方法区改为使用本地内存。最大的区别就是元空间不在虚拟机设置的内存中,而是使用的本地内存。

4 方法区的内部结构

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。(经典但不绝对)

类型信息:

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

1)这个类型的完整有效名称(全名=包名.类名)

2)这个类型直接父类的完整有效名。

3)这个类型的修饰符(public,abstract,final的某个子集)

4)这个类型直接接口的一个有序列表。

域(Filed)信息:

1)JVM必须在方法区中保存类型的所有域对象的相关信息以及域声明顺序。

2)域相关的信息包括:域名称、域类型、域修饰符。

方法信息:

1)方法名称

2)方法的返回类型

3)方法参数的数量和类型(按顺序)

4)方法的修饰符

5)方法的字节码、操作数栈、局部变量表的大小(abstract和native除外)

6)异常表(abstract和native除外)

non-final的类变量:

1)静态变量和类关联在一起,随着类的加载而加载,它们成为数据在逻辑上的一部分。

2)类变量被类的所有实例共享,即使没有类实例时你也可以访问它。

补充说明:被声明为final的类的处理方法不同,在编译的时候就会被分配空间以及初始化值。如图二所示。

 

图2

 

 

运行时常量池和常量池

常量池:

一个java文件中的类、接口,编译后产生一个字节码文件。而java中的字节码需要 数据支持,通常这些数据支持会很大以至于不能直接存在字节码里,换另外一种方式,存在常量池,这个字节码包含了指向常量池的引用。里面包括的类型有数量值、字符串值、类引用、字段引用、方法引用。

小结:可以看作是一张表,虚拟机指令根据这张表找到要执行了类名、方法名、参数类型、字面量等类型。

运行时常量池:

1)运行时常量池时方法区的一部分,而常量池表时class文件第一部分。

2)运行时常量池加载到虚拟机机后,就会创建相应的运行时常量池。

3)JVM为每个已加载的类型都维护一个常量池。池中的数据项就像数组项一样,是通过索引访问的。

4)运行时常量池相较于class文件常量池,其具有动态性

5)如果创建类或者接口的运行时常量池时,如果所需的空间超过了方法区所能提供的最大值,就会oom。

5 方法区的演进细节

1)首先明确一点:只有HotSpot才有永久代

2)Hotspot中方法区的变化:

jdk1.6之前:有永久代,静态变量存放在永久代上。

jdk1.7:有永久代,但是已经开始去永久代化,静态变量和字符串常量池移到堆中。

jdk1.8:无永久代,类型信息、字段、方法

常量保存在本地内存的元空间,但字符串常量池和静态变量依旧存在堆上。

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

1)为永久代设置空间大小是很难确定的。

2)对永久代进行调优是很困难的。

String Table为什么要移动位置:

因为永久代的回收效率很低,在full gc的时候才会被触发,导致stirng table的回收效率很低,但是我们开发中会创建很多的Sting类型的对象,所以将其移动到堆区,能及时回收内存。

6 方法区的垃圾回收

1)方法区的约束时很宽松的,可以实现垃圾回收也可以不实现。

2)方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

3)判断一个类型是否不再使用需要满足三个条件:该类的所有实例都已经被回收,也就是java堆中不存在该类和其任何派生子类的实例;加载该类的类加载器已经回收;该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类方法。当然也仅仅是允许被回收,并不一定。