五、方法区

【目录】 【上一篇: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 虚拟机规范》中,并没有明确要求方法区一定要有垃圾回收;

方法区的垃圾回收主要回收:常量池中废弃的常量和不再使用的类型。

【目录】 【上一篇:JVM 堆】 【下一篇:对象的实例化内存布局与访问定位】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值