方法区的基本概念
-
方法区和堆一样是多个线程共享的,
-
《java虚拟机规范》中声明,所有的方法区在逻辑上可以看作是堆的一部分,但是一些简单的实现可能不会选择区进行垃圾收集或者压缩,
-
而对于hotSpot而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开,所以,方法区可以看作是一块独立与堆的内存空间,而且堆的主要目的是存放创建出来的各种对象,方法区里则会有类的信息,从内容来说,也不相同
-
方法区在jvm启动时被创建,它的实际物理内存空间和java堆区一样都是可以不连续的
-
方法区的大小和堆空间一样,可以固定大小也可以扩展
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法去溢出,虚拟机同样会抛出oom:PermGen space 或者oom : Metspace (1.7之前叫做永久代,1.8叫元空间,报的错误也不一样)
-
关闭jvm就会释放这个区域的内存
1.7之前习惯性的把方法区叫做永久代,1.8使用元空间取代了永久代,(可以把方法区看作是接口,永久代和元空间都是对应的实现)
-
方法区和永久代并不等价,只是在hotSpot中是等价的,《java虚拟机规范》中并没有规定具体的实现,不同的虚拟机是不一样的,
-
永久代使用的是虚拟机的内存,内存方面限制较大,容易溢出
-
元空间采用的是本地内存来实现的,空间更大
设置方法区的大小
-
jdk1.7的永久代
-XX:PermSize 初始分配空间 ,默认:20.75M
-XX:MaxPermSize 最大空间 ,32位物理机默认64M,64位默认82M
-
1.8 元空间:因为用的是本地内存,所以和平台有关系
-XX:MetaspaceSize 初始分配空间 ,windows默认21M
-XX:MaxMetaspaceSize 最大空间 ,默认是-1,也就是没有限制
- 对于初始的元空间大小,会被认为是高水位线,一旦触及这个水位线,就会执行Full GC,卸载没用的类,然后重置高水位线,新的高水位线的取值取决于GC回收了多少元空间,如果释放的空间不足,在不超过最大空间时,会适当提高水位线,如果释放空间过多,会降低水位线
- 所以如果初始化水位线设置过低,高水位线会频繁变动,所以建议初始水位线设置一个较高的值
方法区的内部结构
文件被加载到方法区后,会保留是被哪一个类加载器加载的该文件,也算是类的一部分
方法区用于存储已被虚拟机加载的类型信息(类、接口、枚举、注解)、常量、静态变量、即时编译器编译后的代码缓存等
-
类型信息:对每个加载的类型信息,jvm必须在方法区中存储以下类型信息:
- 这个类型完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(接口或者object是没有父类的)
- 这个类型的修饰符(public、abstract、final)
- 这个类型直接接口的一个有序列表(可能会实现了多个接口,是一个有序的列表)
-
域(Feild,俗称属性)信息
- jvm必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括:域名称、域类型、域修饰符
-
方法信息:
- 方法名称
- 方法返回类型
- 方法参数的数量、类型(是有序的)
- 方法修饰符
- 方法字节码、操作数栈、局部变量表及其大小
- 异常表(异常开始位置、结束位置等)
non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,成为类数据在逻辑上的一部分
- 类变量被所有实例共享
全局常量(final 的 static),在编译的时候就会被分配了
运行时常量池
方法区内部包含了运行时常量池。
字节码文件,包含常量池,把常量池的信息加载到内存后,就叫做运行时常量池
常量池包含了各种字面量和对类型、域和方法的符号引用,也就是一些基本信息
常量池可以看作是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等信息
-
常量池是字节码文件的一部分,用于存放编译时生成的各种字面量与符号引用,这部分内容在类加载后会存放到方法区的运行时常量池
-
运行时常量池是方法区的一部分
-
每个类或接口都会有一个常量池,当中的数据就像数组一样,可以用索引来访问
-
运行时常量池中,存储的就不是符号引用了,是真实的地址
-
运行时常量池相较于常量池来说,具备动态性,有可能常量池中并没有相关信息,比如虚方法等
而字符串常量池再1.6的时候是在永久代的,而1.7就转移到了堆中,1.8虽然去除了永久代,改为元空间,但是字符串常量池依然保留在了堆中,而不是方法区
方法区的演变
只有hotspot才有永久代
在jdk1.6之前,有永久代,而且静态变量存放在永久代上(new的对象的实体始终在堆中,引用在永久代)
1.7,依然有永久代,但是已经在逐步去除永久代了,字符串常量池、静态变量等从永久代中移除,转移到堆中
1.8及以后,就没有永久代了,类型信息、字段、方法、常量保存至本地内存的元空间,但是字符串常量池、静态变量依然放在堆中
为什么要用元空间代替永久代
官方给的消息是,jdk8 ,oracle将hotSport 和 JRockit 进行了整合,而 JRockit 是没有永久代的,所以1.8也没有永久代
也有以下原因:
- 永久代的空间大小难以确定,如果动态加载的类太多,会频繁出现错误,而元空间采用的是本地内存,不受虚拟机内存的限制
- 对永久代的调优很难,想要回收类,条件很苛刻(full GC 代价很大)
字符串常量池的变化
jdk 7 将StringTable放到了堆空间,因为永久代的回收效率很低,只有在full gc 是才会触发,而full GC 是老年代、永久代不足时才会触发,这就使得StingTable回收效率不高,而一般开发中通常会有大量的字符串被创建,如果回收效率低,容易导致永久代内存不足,放在堆里。可以及时的回收内存
方法区的垃圾回收
《java虚拟机规范》对方法区的约束是比较宽松的,方法区的垃圾回收,在不同的虚拟机实现是不同的,是可以不进行垃圾回收的
一般来说,方法区的垃圾回收效果是比较困难的,尤其是类型的卸载,条件十分苛刻,但是方法区的回收有时又是非常由必要的,否则有可能会出现内存泄漏
方法区的垃圾回收主要涉及两部分:
- 常量池中废弃的常量:字面量和符号引用
- 字面量:文本字符串、final的常量值 (比较接近java语言层次)
- 符号引用:类、接口的全限定名,字段的名称和描述符,方法的名称和描述符 (属于编译原理的概念)
- 对于常量池的回收策略很明确:只要常量池中的常量没有被任何地方引用,就可以回收
- 不在使用的类型:想要判定类型是否是不在被使用的条件是很苛刻的,需要满足以下全部条件
- 所有的实例都被回收了,且不存在任何子类实例
- 加载该类的类加载器已经被回收了
- 该类的 .class 对象没有被任何地方引用
- 只有全部满足上面的三个条件,才允许被回收,而不是一定会回收