0%

方法区

方法区

方法区在逻辑上虽然属于堆的一部分,但是简单的实现上可能不会选择去进行垃圾回收或者进行压缩,在Hotspot中为了区分方法区和堆,方法区又被称为非堆,所以方法区可以看做是独立于堆的一块内存空间

方法区的生命周期与JVM实例也是一致的,在JVM启动的时候就会被创建,也是各个线程共享的区域,其存储的是已被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据,所以方法区的大小决定了系统可以保存多少个类,如果系统定义的类过多,方法区就会溢出,从而出现OOM

jdk7及之前报的是java.lang.OutOfMemoryError:PermGen space,jdk8报的是java.lang.OutOfMemoryError:Metaspace

在jdk7中方法区被称为永久代(Permanent Space),在jdk8中方法区被称为元空间(MetaSpace)

永久代和元空间可以看做是方法区的实现

元空间

元空间是jdk8中对于方法区的实现,元空间使用的不是JVM的内存,而是使用的本地内存,也就是使用的直接内存,直接内存是在java堆外的、直接向系统申请的内存空间

在jdk7的时候将字符串常量池和静态变量从永久代移到了堆中,jdk8虽然永久代去除,改为元空间,但是字符串常量池和静态变量还是在堆中

方法区存储的内容

1
2
3
4
// User 存储在方法区
// user 存储在栈
// new User() 存储在堆
User user = new User();

方法区存储的主要是被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等

其实就是类加载器加载之后的字节码class文件中的内容以及加载器的信息

不同的jdk版本所存放的内容是不一样的

jdk6中静态变量存储在永久代中,此时永久代中包含了类型信息、字段、方法、JIT代码缓存、静态变量(对象实例数据存储在堆空间,变量引用存储在永久代)、运行时常量池以及字符串常量池

jdk7中将字符串常量池、静态变量(对象实例数据和变量引用存储在堆空间)保存在堆中,其他信息存储在永久代

jdk8由于将永久代移除,所有类型信息、字段、方法、JIT代码缓存、运行时常量池都保存在本地内存的元空间中,但是字符串常量池、静态变量(对象实例数据和变量引用存储在堆空间)仍然存放在堆中。元空间是可以动态扩展的,默认-XX:MetaspaceSize值为21M的高水位线。一旦触及则FullGC将被触发并卸载没有用的类,然后高水位线将会重置。新的高水位线的值取决于GC后释放的元空间。如果释放的空间少,这个高水位线则上升。如果释放的空间多,则高水位线下降。

类信息

对于每个加载的类型(类class、接口interface、枚举enum、注解annotation)方法区中存储的类型信息有哪些呢

  • 这个类型的的完整有效名称
  • 这个类型直接父类的完整有效名
  • 这个类型的修饰符
  • 这个类型直接接口的一个有序列表

域信息(Filed属性信息)

方法区中存储的域信息有哪些呢?

  • 保存类型的所有域的相关信息以及域的声明顺序,域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient)

方法信息

方法区中存储的方法信息有哪些呢?

  • 方法名称
  • 方法返回类型
  • 方法参数的数量和类型,按顺序存储
  • 方法的修饰符
  • 方法的字节码、操作数栈、局部变量表以及其大小(abstract和native方法除外)
  • 异常表(abstract和native方法除外),每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

静态变量

静态变量是属于类的,不需要对象实例,属于类数据的一部分

来看一下静态变量和静态常量的区别

1
2
3
4
public class Test {
private static int count = 10;
private static final int number = 20;
}
1
2
3
4
5
6
7
8
9
private static int count;
descriptor: I
flags: ACC_PRIVATE, ACC_STATIC

private static final int number;
descriptor: I
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: int 20

可以看到静态常量在编译成字节码的时候就将值存储下来了

jdk7静态变量移至堆中

运行时常量池

什么是运行时常量池呢?在字节码文件中有一个常量池,JVM为每个已加载的类型都维护一个常量池,用于存放编译期间生成的各种字面量和符号引用,通过索引访问。这部分内容是在类加载之后存放到运行时常量池中的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 包含有数量值、字符串值、类引用、字段引用、方法引用
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/zhanghe/study/jvm/Test.count:I
#3 = Class #22 // com/zhanghe/study/jvm/Test
#4 = Class #23 // java/lang/Object
#5 = Utf8 count
#6 = Utf8 I
#7 = Utf8 number
#8 = Utf8 ConstantValue
#9 = Integer 20
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/zhanghe/study/jvm/Test;
#17 = Utf8 <clinit>
#18 = Utf8 SourceFile
#19 = Utf8 Test.java
#20 = NameAndType #10:#11 // "<init>":()V

而将字节码中的常量池加载到方法区之后,就称为运行时常量池,运行时常量池中包含有多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能获得的方法和字段引用,将常量池中的符号地址换为真实地址,运行时常量池相对于Class文件常量池的重要特征是具备动态性

运行期间可以通过String.intern()方法将新的常量放入池中

jdk7运行时常量池移至堆中

方法区的垃圾回收

方法区出现垃圾回收的情况很少,主要包含有两部分内容:常量池中废弃的常量和不再使用的类型

方法区的常量池存储的主要是字面量和符号引用,只要常量池中的常量没有任何地方引用,就可以被回收了

方法区的参数设置

设置方法区内存大小

-XX:MetaspaceSize 元空间初始分配空间,默认21M

-XX:MaxMetaspaceSize 元空间最大分配空间,默认是-1,没有限制

jdk7及以前的配置与jdk8不同

-XX:PermSize 永久代初始分配空间,默认20.75M

-XX:MaxPermSize 永久代最大分配空间,默认是82M

欢迎关注我的其它发布渠道