简介
在《深入理解java虚拟机》这一书中,堆方法区的定位为-方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与java堆分开来。
jdk7以前把方法区称为永久代。jdk8开始,称之为元空间。
- 到了jdk8后,完全废弃了永久代的概念,采用元空间的概念来替代
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:
元空间不在虚拟机设置的内存中,而是使用**本地内存。**
- 永久代、元空间二者并不只是名字变了,内部结构也调整了。
- 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将
抛出OOM异常。
内存位置
《Java虚拟机规范》中明确说明:"尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看作是一块独立于Java堆的内存空间。
说明
- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:
java.lang.outofMemoryError:PermGen space
或者java.lang.outOfMemoryError: Metaspace
;- 加载大量的第三方应用
- tomcat部署的工程过多 30-50个
- 大量的动态生成反射类
- 关闭JVM就会释放这个区域的内存。
功能设置
- jdk7以前-忽略
- 方法区的大小不必是固定的,jvm可以根据应用需要进行动态调整
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspacesize的值是-1,即没有限制。
问题:什么是内存泄露?什么是内存溢出
内部结构
前文提到,在方法区主要存放的信息为:被虚拟机加载的类信息、常量、静态变量、类加载器、即时编译器编译后的代码等数据
类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation), JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
- 这个类型的修饰符(public, abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
方法信息
JVM必须保存所有方法的以卞信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public, private, protected,static, final,synchronized, native, abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外)
- 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
域field信息(属性信息、成员变量信息)
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
- 域的相关信息包括:域名称、域类型、域修饰符(public, private,protected,static,final, volatile,transient的某个子集)
non-final非常量信息
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。
- 类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
运行时常量池
这里发现字节码文件和方法区都有常量池这一区域,分别是运行时常量池与常量池
- 方法区包含了运行时常量池
- 字节码文件中,内部包含的为常量池
- 在加载对应的常量池后,就会创建对应的运行时常量池。即运行时常量池就是常量池运行时的一种体现
- jvm会为每一个已经加载的类维护一个常量池(从编译后的class文件中体现出来的常量池)
- 运行时常量池和常量池的区别:运行时常量池具备动态性
- 比如说
String.intern()
,如果在常量池中没有,将在运行时常量池中加载进去
- 比如说
- 这里创建类或者接口时的运行时常量池,如果构造运行时常量池所需的内存空间大小超过了方法区所能提供的最大值,jvm将抛出
OutOfMemoryError
异常
方法区中的运行时常量池是由字节码文件中的常量池通过类的加载器加载到方法区中形成的。
几种在常量池内存储的数据类型包括:
- 数量值
- 字符串值
- 类引用
- 字段引用
- 方法引用
常量池可以看作一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名、参数类型、字面量类型等类型
问题:静态变量存在哪里?
静态对象本身都是存放在堆空间里的,不论是jdk6、7、8都是放在堆空间的。
public static byte[] arr = new byte[1024 * 1024 * 100]; //100MB
运行这上面的代码,再使用jsp或者其他工具查看程序内存状况,会发现一直都是存放在永久代(jdk6、7)或者老年代 (jdk8)里面的
只要是对象实例都必然会在java堆中分配
垃圾回收
在jvm虚拟机规范中,没有明确要求方法区一定要进行垃圾回收。
有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
- 先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
- 回收废弃常量与回收ava堆中的对象非常类似。
评论区