六、运行时数据区(四)方法区

栈,堆,方法区的交互关系

栈,堆,方法区的交互关系

栈,堆,方法区的交互关系

方法区

<Java虚拟机规范> 中明确说明: “尽管所有的方法区在逻辑上是属于堆的一部分, 但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩” . 但对于HotspotJVM而说,方法区还有个别名叫做 Non-Heap(非堆), 目的就是要和堆分开. 所以, 方法区看作是一块独立于Java堆的内存空间

  • 方法区与Java堆一样,是各个线程共享的内存区域
  • 方法区在JVM启动的时候就被创建,并且他的事迹物理内存空间中的Java堆区一样都是可以不连续的
  • 方法区的大小,跟堆空间一样,可以选择固定大小伙子可扩展
  • 方法区的大小决定了系统可以保持多少类,如果系统定义了太多的类,导致方法区疫溢出,虚拟机同样会抛出内存溢出错误 OutOfMemoryError : PermGen space 或Metaspace
    • 加载大量的第三方的jar包; Tomact部署的工程过多(30-50个);大量动态的生成反射类等
  • 关闭JVM就会释放改区域的内存
  • JDK7及以前,把方法区称为永久代,JDK8开始,使用元空间取代永久代
  • 本质上,方法区和永久代并不等价.仅是对于Hotspot虚拟机而言. <Java虚拟机规范>中对如何实现方法区没有做统一要求. JRockit / IBM J9 中不存在永久代的概念
    • 使用永久代不是好的主意,这样会导致Java程序更容易OOM(超过-XX:MaxPermSize 上限)

JDK7及JDK8实现

方法区实现

  • 在JDK8中,终于完全废弃了永久代的概念,改用与JRockit , J9一样在本地内存中实现的元空间(Metaspace)来代替
  • 元空间的本质与永久代类似,都是对JVM规范中方法区的实现.不过元空间与永久代最大的区别在于: 元空间不在虚拟机设置的内存中,而是使用本地内存.
  • 永久代,元空间内部结构也调整了
  • 根据<Java虚拟机规范>的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常

设置方法区内存大小

方法区的大小不必是固定的,JVM可根据需要动态调整

JDK7及以前

  • 通过-XX:PermSize 来设置永久代初始分配空间.默认是 20.75M
  • -XX:MaxPermSize 来设置永久代最大可分配空间. 32位机器默认 64M, 64位机器默认 82M
    当JVM加载的类信息容量超过这个值,会报异常OutOfMemoryError : PermGen space

JDK8及以后

  • 元数据区大小使用 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定,代替原有参数
  • 默认值依赖于平台. Windows : -XX:MetaspaceSize是21M, -XX:MaxeMetaspaceSize 的值是 -1, 没有限制
  • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存.如果元数据区发生溢出,虚拟机一样会抛出异常 OutOfMemoryError: Metaspace
  • -XX:MetaspaceSize 设置初始的元空间大小. 对于一个64位的服务器JVM来说,默认的-XX:MetaspaceSize值为21MB.这就是初始的高水位线.一旦触及这个水位线, Full GC将会被处罚并卸载没用的类(即使这些类对于的类加载器不再存活),然后这个高水位线会重置.新的高水位线的值取决于GC后释放了多少元空间.如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值.释放空间过多,则适当降低该值
  • 如果初始化的高水位线设置过低,调整情况发生多次,通过垃圾回收器的日志可以看到Full GC 多次调用.为了避免频繁GC,建议将 -XX:MetaspaceSize 设置为一个相对较高的值

解决OOM

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

内部结构与储存信息

在这里插入图片描述

<深入理解Java虚拟机>中对方法区储存内容描述: 它用于存储已被虚拟机加载的类型信息, 常量, 静态变量, 即时编译器编译后的代码缓存等

在这里插入图片描述

类型信息

对每个加载的类型(类class, 接口,枚举,注解), JVM必须在方法区中存储一下类型信息

  1. 这个类型的完整有效名称(全名=包名.类名)
  2. 这个类型直接父类的完整有效名(对于接口或者object都没有父类)
  3. 这个类型的修饰符(public, abstract, final的某个子集)
  4. 这个类型直接接口的一个有序列表

域信息

  1. JVM不虚在方法区中保存内存的所有域的相关信息以及域的声明顺序
  2. 包括: 域名称,域类型,域修饰符(public, private, static,final等)

方法信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序

  1. 方法名称
  2. 方法的返回类型
  3. 方法参数的数量和类型
  4. 方法的修饰符
  5. 方法的字节码, 操作数栈, 局部变量表及大小
  6. 异常表

non-final的类变量

  • 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
  • 类变量被类的所有实例共享, 即使没有类实例时也可以访问它
  • 全局常量 static final 被声明为final的类变量的处理方法则不同,每个全局变量在编译的时候就分配好了

运行时常量池与常量池

常量池

一个有序的字节码文件中除了包含类的版本信息,字段,方法以及接口等描述信息外,还包含一项信息那就是常量池表, 包括各种字面量和堆类型,域和方法的符号引用

在这里插入图片描述

为什么需要常量池

一个Java源文件中的类,接口,编译后产生一个字节码文件.而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换一种方式,可以存到常量池,这个字节码包含了指向常量池的引用.在动态链接的时候会用到运行时常量池

常量池存储的数据类型

常量池,可以看做一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型

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

运行时常量池

  • 运行时常量池是方法区的一部分
  • 常量池表示class文件的一部分,用于存放编译器生成的各种字面量与符号引用.这部分内容将在类加载后存放到方法区的运行时常量池中
  • 运行时常量池,在类加载和接口到虚拟机后,就会创建对应的运行时常量池
  • JVM为每个已加载的类型都维护一个常量池,池中的数据项像数组项一样,通过索引访问
  • 运行时常量池中包含多种不同的常量,包括编译期就明确的数值字面量,也包括运行期解析后才能获取的方法或者字段引用.此时不再是常量池中的符号地址了.换位真实地址
    • 运行时常量池,相对于class文件常量池的另一重要特征是: 具备动态性,如String的intern()>方法
  • 运行时常量池类似于传统编程语言中的符号表,但所包含的数据却比符号表更丰富一下
  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,会抛OOM异常

方法区演进细节

  • 只有Hotspot才有永久代, JRockit, IBM J9 不存在永久代的概念.原则上如何实现方法区属于虚拟机实现细节,并不要求统一
  • Hotspot方法区的变化
    • JDK1.6及以前: 有永久代,静态变量存放在永久代上
    • JDK1.7: 有永久的,但已逐步去永久代, 字符串常量池,静态变量移除,保存在堆中
    • JDK1.8及之后: 无永久代,类型信息,字段,方法,常量报错在本地内存的元空间,但字符串常量池, 静态变量仍在堆中

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

元空间替换永久代原因

  • 随着Java8的到来,Hotspot VM中再也没有永久代了, 但不意味着类的元数据信息也小时了.这些数据别移到了一个与堆不相连的本地内存区域,这个区域叫元空间
  • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间
  • 这项改动是必要的:
    • 为永久代设置空间大小是很难确定的,在某些场景下,如果动态加载类过多,容易产生Perm区的OOM.如某个实际Web工程中,功能的较多,在运行过程中,要不断动态加载很多类,容易出现致命错误.而元空间和永久代之间最大的区别在于: 元空间并不在虚拟机中,而是在本地内存.默认情况下,元空间的大小仅受本地内存限制
    • 对永久代进行调优很困难

StringTable调整原因

JDK7中将StringTable放到了堆空间中,因为永久代的回收效率很低,在Full GC 的时候才会触发,而Full GC是老年代的空间不足,永久代不足时才会触发.这导致StringTable 回收效率不高.而我们开发中会有大量的字符串被创建.回收效率低,导致内存不足.放到堆中,能及时回收内存

方法区的垃圾收集

方法区的垃圾收集只要回收两部分内容: 常量池中废弃的常量和不在使用的类型.

一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻.但是这部分区域的回收又是又确实是必要的.

总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值