深入理解Java虚拟机之【方法区】
一、概述
-
方法区看作是一块独立于Java堆的内存空间,它和Java堆一样,是各个线程共享的内存区域
-
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样,都是可以不连续的
-
方法区的大小和堆空间一样,可以选择固定大小或者可扩展
-
关闭JVM就会释放这个区域的内存
-
方法区可以选择固定大小或者可扩展,它的大小决定了系统可以保存多少个类,如果定义太多类,加载大量的第三方的Jar包,Tomcat部署过多工程,导致方法区溢出,虚拟机同样会抛出OOM
栈、堆、方法区关系
二、方法区的内部结构
方法区用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存
1.类型信息
对于每个加载的类型(类Class,接口Interface,枚举Enum,注解annotation),JVM必须在方法区中存储以下类型信息
这个类的完整有效名称(全名=包名.类名)
这个类型直接父类的完整有效名,对于interface或Object没有父类
这个类型的修饰符,public,abstract,final
这个类型直接接口的一个有序列表
2.域信息
JVM必须在方法区中保存类型的所有域的相关信息,以及域的声明顺序
域名称
域类型
域修饰符
3.方法信息
JVM必须保存所有方法的以下信息,一样包括声明顺序
方法名称
方法的返回类型
方法参数的数量和类型
方法的修饰符
方法的字节码bytecodes,操作数栈,局部变量表及大小
异常表(abstract和native方法除外)
4.non-final的类变量
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例共享,即使没有类实例时,你也可以访问他
5.全局常量 static final
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了
6.常量池
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
常量池有:数量值、字符串值、类引用、字段引用、方法引用
一个java源文件中的类、接口、编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大,以至于不能直接存到字节码里。
换一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接时会用到运行时常量池。
一个有效的字节码文件中除了包含的类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
字节码文件,内部包含了常量池,运行时将常量池加载到方法区,就是运行时常量池
7.运行时常量池
JVM为每个已加载的类型都维护一个常量池,池中的数据像数组项一样,通过索引访问
运行时常量池包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后,才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里转换为真实地址。
运行时常量池,相对于class文件常量池的另一个重要特征是:具备动态性
当创建类或接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值。则JVM会抛出OOM异常
常量池数量为N,则索引为1到N-1,0属于保留索引(特殊情况使用)
三、方法区的演进细节(HotSpot)
只有HotSpot才有永久代
HotSpot中方法区的变化
jdk1.6及之前,有永久代,静态变量存放在永久代上
jdk1.7, 有永久代,但已经逐步去永久代,字符串常量池,静态变量移除,保存在堆中
jdk1.8及之后,无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆
永久代为什么被元空间替换?
随着JAVA8的到来,HotSpotVM中再也见不到永久代了,但是并不意味着类的元数据信息也消失了,这些数据被转移到了一个与堆不相连的本地内存区域,这个区域叫做元空间MetaSpace
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统的可用内存空间
为永久代设置空间大小很难确定,在某些场景下,如果动态加载类过多,就容易产生OOM,而元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制
四、方法区的垃圾回收
Java虚拟机规范对方法区的约束非常宽松,提到过可以不要求虚拟机在方法区实现垃圾收集。
方法区的垃圾收集主要回收两部分内容
1. 常量池中废弃的常量
HotSpot对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收
2. 不再使用的类型
需要同时满足三个条件
该类所有的实例已经被回收,java堆中不存在该类及其任何派生子类的实例
加载该类的类加载器已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法
满足以上三个条件后,并不是和对象一样立即被回收,仅仅是允许。
方法区内常量池中主要存放的两大类常量
字面量
如文本字符串,被声明为final的常量值等
符号引用
类和接口的全限定名
字段的方法和描述符
方法的名称和描述符
五、设置方法区大小与OOM
方法区大小不是固定的,jvm可以根据应用动态调整
JDK7及以前
-XX:PermSize 来设置永久代初始分配空间,默认值是20.75M
-XX:MaxPermSize来设定永久代最大可分配空间。
如果JVM加载的类信息容量超过了这个值,会报OOM:PermGenspace
JDK8及以后
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
默认值依赖平台,windows下初始为21M,最大是-1即没有限制
如果不指定大小,虚拟机耗用所有可用系统内存,元数据区发生溢出,一样OOM:Metaspace
对于一个64位服务端JVM来说,默认的初始元数据区空间为21M,这就是初始的高水位线。
一旦触及这个水位线,FULLGC会触发并卸载没有用的类,然后高水位线会被重置。新的高水位线的值取决于GC后释放了多少元空间。
如果释放空间不足,在不超过最大设定值时,适当提高该值。如果释放空间过多,则适当降低该值。
如何解决OOM
要解决OOM或heap space异常,一般的手段是通过内存映像分析工具,对dump出来的堆转存储快照进行分析,重点确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露,还是内存溢出
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到内存泄露对象时通过怎样的路径与GC Roots相关联,导致垃圾收集器无法自动回收他们。根据引用链信息,可以较准确的定位出泄露代码的位置
如果不存在内存泄露,或者说内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与物理机器内存对比是否还可以调大,从代码检查是否某些对象生命周期过长,持有状态时间过长,尝试减少程序运行时的内存耗用
OVER(∩_∩)O~