【目录】 【上一篇:JVM 堆】 【下一篇:对象的实例化内存布局与访问定位】
五、方法区
1、栈、堆、方法区的交互关系
User u = new User();
↑ ↑ ↑
对象类型存储在方法区 变量存储在栈中 new出来的具体实例存储在堆中
2、方法区的理解
- 方法区(Method Area)与 Java 堆一样,是线程共享的区域;
- 方法区在 JVM 启动的时候就被创建,并且它的实际物理内存空间和 Java 堆区一样,都可以是不连续的,在 JVM 关闭的时候消失;
- 方法区的大小可以设置为固定的,也可以设置为动态拓展的;
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机会抛出内存溢出的错误:java.lang.OutOfMemoryError: PermGen space (JDK1.7 及之前)或java.lang.OutOfMemoryError: Metaspace(JDK1.8及之后);
- 在 jdk7 及以前,习惯上把方法区称之为永久代;在 jdk8 开始,使用元空间取代了永久代。而元空间与永久代的最大区别在于:元空间不在虚拟机设置内存中,而是使用本地内存。
3、设置方法区大小与OOM
方法区大小的设置可分为两种:固定、自动拓展
jdk7 及以前:
-XX:PermSize 来设置永久代初始分配空间,其默认值是 20.75M
-XX:MaxPermSize 来设置永久代最大可分配空间,32位机器默认是 64M,64位机器默认是 82M
当 JVM 加载的类容量大小超过了最大可分配内存空间,则会报 OutOfMemoryError:PermGen space 异常
💡 可以通过 【 jinfo -flay PermSize 进程号 】命令来查询方法区空间
jdk8 及之后:
使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来指定元空间的初始分配空间与最大可分配空间。在 windows 平台下,初始元空间的默认可分配空间是 21M ,最大可分配空间参数值为 -1(-1表示不限制)。
如果不指定元空间可分配最大内存空间,极端情况下虚拟机会耗尽所有的可用系统内存,如果元空间区发生溢出,虚拟机会抛出异常:OutOfMenoryError:Metaspace
- -XX:MetaspaceSize 设置初始的元空间大小,对于一个 64 位的服务器端 JVM 来说,其默认的 -XX:MetaspaceSize 值为 21M ,这就初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载掉没用的类,然后这个高水位线将会被重置,新的高水位线的值取决于 GC 后释放了多少内存空间,如果释放的空间不足,那么在不超过 MaxMetaspaceSize 值的情况下,会适当提高该值;如果释放的内存过多,会适当的降低该值。
- 如果初始化的高水位线值设置过低,上述的高水位线调整情况会发生很多次,也会导致多次触发 Full GC ,为了避免频繁的 GC,所以在调优时,会建议将该值设置得相对高一点。
4、方法区的内部结构
它用于存储已被虚拟机加载的 类型信息、常量、静态变量、即时编译器编译之后的代码缓存等。
4.1、类型信息:
对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在方法区存储以下类型信息:
①、这个类型的完整有效的名称(包名.类名);
②、这个类型直接父类的完整有效的名称;
③、这个类型的修饰符(public、abstract、final 等一个或多个修饰符);
④、这个类型直接接口的一个序列表;
4.2、域(Field)信息:
JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient 的某个子集)。
4.3、方法(Method)信息:
JVM 必须保存所有方法的以下信息,同域信息一样声明顺序:
①、方法名称;
②、方法的返回值类型(或 void);
③、方法参数的数量和类型(按顺序);
④、方法的修饰符(public、private、protected、static、final、synchronized、native、abstract 的一个子集);
⑤、方法的字节码、操作数栈、局部变量表的大小(abstract 和 native 除外);
⑥、异常表(abstract 和 native 除外)。
表中记录了:异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。
4.4、运行时常量池—常量池:
常量池:
为什么需要有常量池:一个 java 源文件中的类、接口,编译之后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大,以至于不能直接存储在字节码里,因此需要换成另一种方式,将其存储到常量池中。这个字节码里包含有指向常量池的符号引用,在动态链接的时候会将这个符号引用替换成具体的类型引用。
常量池中存储的数据类型包括:
①、数量值
②、字符串值
③、类引用
④、字段引用
⑤、方法引用
常量池可以看做一张表,虚拟机在执行字节码指令的时候,可以根据字节码指令中的引用找到具体的要执行的类型、方法、参数、字面量等信息。
运行时常量池:
- 常量池是 Class 文件中的一部分,用于存放编译期生成的各种字面量与符合引用,这部分内容将在类加载后存放到方法区的运行时常量池中;
- 运行时常量池是方法区的一部分,在类或接口加载到虚拟机中后,就会在方法区中创建对应的运行时常量池。池中的数据项像数组一样,是通过索引进行访问的,并且其索引位的起始值为 1 ;
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址;
- 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
4.5、运行时常量池、常量池、字符串常量池存储位置:
常量池在程序运行时,变成运行时常量池;
运行时常量池存存储在方法区中;
字符串常量池存储在堆中。
5、方法区的演进细节
jdk1.6 及之前 | 有永久代(permanent generation),静态变量存放在永久代上 |
---|---|
jdk1.7 | 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移动到堆内存中 |
jdk1.8 及之后 | 无永久代,改为元空间,类信息、字段、方法、常量、运行时常量池保存在本地内存的元空间中,字符串常量池、静态变量任然保存在堆中 |
为什么要淘汰永久代转而换成元空间?
1、Oracle 收购 JRockit 之后,推动 JRockit 与 HotSpot 融合,而 JRockit 虚拟机没有永久代这个概念;
2、为永久代设置空间大小是很难确定的;
3、为永久代进行调优是很困难的。
String Table 为什么要调整?
jdk7 中将 StringTable 放到了堆空间中,因为永久代的回收效率非常的低,在 full gc 的时候才会触发。而 full gc 是老年代的空间不足、永久代空间不足时才会触发,这就导致了 StringTable 的回收效率不高。而在开发中我们会大量的创建字符串,如果其回收效率低,会导致永久代内存不足,放到堆里则能及时回收。在 jdk8 及之后,使用元空间来替代永久代,但是 StringTable 并没有一起放到元空间中,主要也是因为元空间的 GC 少。
6、方法区的垃圾回收
在《Java 虚拟机规范》中,并没有明确要求方法区一定要有垃圾回收;
方法区的垃圾回收主要回收:常量池中废弃的常量和不再使用的类型。