这是我参与更文挑战的第 5 天,活动详情查看: 更文挑战
方法区
1.概述
- 方法区随着虚拟机的创建而创建,内存物理上不一定连续,但是逻辑上连续。
- 虽然方法区逻辑上属于堆的一部分,但是一些简单的实现不会选择垃圾回收或者压缩,方法区看作是一块独立于Java堆的空间。
- 和堆一样,属于线程共享。
- 方法区可以设置大小,决定系统可以包含多少类,如果定义的类太多,会报OOM。
2.方法区内存大小设置
- 方法区内存大小可以不设置固定值,JVM可以根据应用的需求动态调整。
- JDK7及之前使用
-XX:PermSzie来设置初始值,默认20.75m。使用-XX:MaxPermSize来设置最大的空间,32位默认64m,64位默认82m。 - JDK8及以后只能使用
-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定初始和最大内存。 -XX:MetaspaceSize默认21m,也就是初始的高水位线,一旦超过这个水位线后,Full GC就会触发,然后根据GC释放了多少空间来重置高水位线,如果释放的空间不足,在不超过-XX:MaxMetaspaceSize的情况下适当提高该值,如果释放的空间多,适当减低该值,因此,在设置-XX:MetaspaceSize尽可能设置一个较高的值,避免频繁GC。
3.方法区内部结构
方法区主要存储的内容有:类型信息、运行时常量池、静态变量、即时编译器编译后的代码缓存等。
类型信息(类、接口、枚举、注解):
- 完整有效的名称,包名加类名
- 直接父类的有效名
- 类型的修饰符
- 类的直接接口的一个有序列表
- 域信息(成员变量),域名称、类型、修饰符
- 方法信息,方法名称、返回类型、参数的类型和数量、修饰符、字节码、异常表
常量池/运行时常量池:
常量池(字节码的一部分):
在Java的字节码文件中,不会直接存储数据支持,通常使用一个常量池保持这些数据,字节码文件中使用一个引用指向常量池的数据。常量池可以看作一张表,虚拟机可以根据这张常量表找到找到要执行类名、方法名、参数类型、字面量等类型。
运行时常量池(方法区的一部分):
在类和接口加载到虚拟机后,就会创建对应的运行时常量池。运行时常量池中包含多种不同的常量,包括编译器确定的数值字面量,也包括运行期解析后才能获得的方法和字段引用。此时不再是常量池中的符号地址,而是真实的地址。
4.方法区的演进
变化过程:
-
JDK6及以前:有永久代,静态变量存放在永久代上。
-
JDK7:有永久代,但已经逐步去除永久代,字符串常量池、静态变量存放在堆。
-
JDK8:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池和静态变量仍然在堆空间。
为什么取消了永久代使用元空间替代?
-
内存分配方面:在永久代的版本中,所有的内存都是由JVM分配的,而永久代设这大小是很难控制的,如果分配的内存太小而动态加载类过多,就会出现频繁的GC和OOM,如果分配的太大就会浪费内存资源。而元空间与永久代最大的区别就是内存的分配不同,元空间的内存并在虚拟机中,而是使用本地内存,所以,元空间的大小仅受本地内存限制。
-
调优方面:在永久代进行调优非常困难。进行FullGC的时候判断类是否还被使用的过程花费时间很久。
字符串常量池(StringTable)为什么要调整?
- 在永久代中的数据回收率很低,只能伴随Full GC的触发才会进行回收,从而导致StringTable回收率不高,而开发中有会创建大量的字符串,导致永久代内存不足。放到堆中可以及时被回收。