首页 > 技术文章 > 方法区

tang321 2021-04-24 21:54 原文

方法区

方法区的别名叫做Non-Heap(非堆),目的就是要和堆分开。
方法区是线程共享的内存区域
方法区在JVM启动的时候被创建,并且它的实际物理内存空间和Java堆区一样都可以是不连续的
方法区的大小可以是固定的或可扩展的
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space 或者java.lang.OutOfMemoryError: Metaspace
关闭JVM就会释放方法区的内存

 

Hotspot中方法区的演进

在jdk7及以前,习惯上把方法区称为永久代;jdk8开始,使用元空间取代了永久代
本质上,方法区和永久代并不等价。《Java虚拟机规范》对于如何实现方法区,不做统一要求(即永久代只是方法区的一个实现)
现在看来, 永久代已经被抛弃了,因为永久代会导致Java程序更容易OOM(超过 -XX:MaxPermSize 上限)
元空间和永久代的最大区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存

 

设置方法区内存的大小

jdk7 及以前:
-XX:PermSize 设置永久代初始分配空间,默认值为20.75M
-XX:MaxPermSize 设置永久代最大可分配空间,32位机器默认是64M;64位机器默认是82M

jdk8 及以后:
-XX:MetaspaceSize 设置元空间初始分配空间
-XX:MaxMetaspaceSize 设置元空间最大可分配空间
windows下,默认 -XX:MetaspaceSize 值为21MB。这就是初始的高水位线,一旦触及水位线,将触发FGC并卸载没用的类(这些类对应的类加载器不再存活),然后这个高水位线将被重置。如果释放的空间不足,适当提高该值;如果释放空间过多,则适当降低该值

 

如何解决OOM?

要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是先分清到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。这样就可以比较准确地定位出泄漏代码的位置
如果不存在内存泄漏,也就是说内存中的对象都必须是活着的(都是必须存在的),那就应当检查虚拟机的堆参数,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

内存泄漏堆积会导致内存溢出,所以判断内存溢出第一步是查看内存是否泄漏
内存泄漏:病人康复了不出院
内存溢出:病人太多了床位不够

 

方法区的内部结构

Hotspot中方法区的变化:
jdk6及之前:有永久代,静态变量存放在永久代上
jdk7:有永久代,但已经逐步“去永久代”,字符串常量池、静态变量保存在堆中
jdk8及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量保存在堆

 

全局常量(被static final 修饰):每个全局常量在类编译成字节码文件的时候就会被赋予真实值

方法区存储内容:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等

 

类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation)。JVM必须在方法区中存储以下类型信息:
这个类型的全类名(包名+类名)
这个类型直接父类的全类名(对于interface或java.lang.Object都没有父类)
这个类型的修饰符
这个类型直接接口的一个有序列表

 

域(Field)信息

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
域的相关信息包括:域名称、域类型、域修饰符

 

方法(Method)信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
方法名称
方法的返回类型
方法参数的数量和类型(按顺序)
方法的修饰符
方法的字节码、操作数栈和局部变量表的大小(abstract和native方法除外)
异常表(abstract和native方法除外):每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

 

常量池和运行时常量池

方法区内部包含了运行时常量池(Runtime Constant Pool)
字节码文件内部包含了常量池(Constant Pool)

常量池

字节码为什么要需要常量池?
如下代码虽然内容不多,但里面却使用了String、System、PrintStream及Object等结构,如果将这些结构都直接存到当前类的字节码文件中,这是不可想象的。这时就用到了符号引用、常量池来解决这个问题
class文件是编译的时候生成的,编译时并不知道这些引用对象在真实内存中的地址,因此暂时使用符号引用来表示引用对象的地址。在被类加载器加载的时候,才能把符号引用转成直接引用

 

常量池存储的数据类型:

  • 数量值
  • 字符串值
  • 类引用
  • 字段引用
  • 方法引用

运行时常量池

常量池存放的编译期生成的各种字面量和符号引用将在类加载后存放到方法区的运行时常量池中
在加载类和接口到虚拟机后,就会创建对应的运行时常量池
JVM为每个已加载的类型(类或接口)都维护一个运行时常量池,池中的数据项像数组项一样,是通过索引访问的
运行时常量池中包含编译期就已经确定的数值字面量以及符号引用转换过来的直接引用
运行时常量池相对于常量池的重要特征是:具备动态性
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所提供最大值,则JVM抛OOM异常

 

静态变量存放位置

public class StaticObjectTest {
	static class Test {
		static ObjectHolder staticObj = new ObjectHolder();
		ObjectHolder instanceObj = new ObjectHolder();
		
		void foo() {
			ObjectHolder localObj = new ObjectHolder();
		}
	}
		private static class ObjectHolder {
		}
		
		public static void main(String[] args) {
			Test test = new StaticObjectTest.Test();
			test.foo();
		}
}

  

 

方法区的垃圾回收

一般来说方法区的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

 

方法区内常量池中主要存放的两大类常量:字面量(字符串、被声明为final的常量值)和符号引用
只要常量池中的常量没有被如何地方引用,就可以被回收

判断一个常量是否“废弃”相对简单,判断一个类型是否属于“不再被使用的类”的条件就比较苛刻。需要同时满足以下三个条件:
该类所有的实例都已经被回收,也就是Java堆中不存在该类及任何派生子类的实例
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收

在大量使用反射、动态代理、GCLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力

 

StringTable为什么要放到堆里?
永久代回收效率很低,只有FGC的时候StringTable才会被回收。开发中会有大量的字符串被创建,回收效率低,导致永久代内存空间不足。放到堆里,能及时回收内存

 

推荐阅读