JVM系列(七):方法区(元空间)

本文详细介绍了JVM内存中的方法区,包括线程共享区域、元空间与永久代的区别、方法区的动态调整以及在不同JDK版本中的变化。讨论了类信息、域信息、方法信息、静态变量和常量池等内容,并阐述了方法区的内存溢出问题及解决方案。重点分析了JDK7及8中方法区的优化,如字符串和常量池的迁移,以及为何使用元空间替代永久代。
摘要由CSDN通过智能技术生成

1、栈、堆、方法区交互关系

1.1、线程共享

  • 线程共享:堆、元空间
  • 线程私有:栈(虚拟机栈、本地方法栈)、程序计数器

1.2、各区域存储

  • 类字节码存储在方法区(也就是存类啦),实例化对象存储在Java堆,对象引用存储在栈中。


2、理解方法区

  • 尽管方法区在逻辑上属于堆,但是方法区可以看做一块独立于Java堆的内存空间。
  • 方法区与Java堆一样,属于线程共享区域
  • 方法区在JVM启动时创建,和堆一样可以物理内存不连续
  • 方法区大小和堆一样,可以固定或拓展
  • 方法区大小决定保存多少类,类过多方法区溢出,抛出错误java.lang.OutOfMemoryError:PermGen space(永久代,JDK7及以前)或者java.lang.OutOfMemoryError:Metaspace(元空间,JDK8开始)
  1. 加载大量的第三方jar包;
  2. Tomcat部署工程过多(30-50),大量动态反射类
  • 关闭JVM就会释放方法区内存
  • 方法区的实现有永久代、元空间等,JDK7及以前,将方法区称为永久代;JDK8开始,元空间取代永久代。

  • HotSpot中,JDK8完全摒弃了永久代概念,使用在本地内存中实现元空间(而不是JVM内存空间)
  • 元空间和永久代都是方法区的实现,区别是元空间不在JVM内存中,使用内存;永久代使用JVM内存
  • 方法区无法满足新内存分配需求,则OOM异常

3、设置方法区大小与OOM

  • 方法区大小不必固定,JVM可以动态调整

3.1、JDK7及之前

  • -XX:PermSize设置永久代初始分配空间,默认20.75M
  • -XX:MaxPerSize设置永久代最大分配空间,32位机子默认64M,64位机子82M
  • 加载的类超过最大值,那么报异常OutOfMemoryError:PermGen space

3.2、JDK8及以后

  • -XX:MetaspaceSize和 –XX:MaxMetaspaceSize替代JDk7中的参数
  • 默认值依赖于平台,windows下,-XX:MetaspaceSize默认21M-XX:MaxMetaspaceSize值默认为-1,即没有限制
  • 与永久代不同,元空间不指定大小,会消耗系统可用内存,发生溢出,照样有OutOfMemoryError:Metaspace
  • -XX:MetaspaceSize=21M设置的值,对于64位服务器JVM来说,如果内存使用到21M,那么Full GC被触发,卸载无用类,然后21M会被调整。如果GC后释放的空间大,那么降低21M;如果GC后释放的小,那么提升21M。
  • -XX:MetaspaceSize设置略大比较好,减少Full GC

3.3、如何解决OOM

  • 通过内存影响分析工具分析,确认内存中的对象是否有必要,搞清楚是出现了内存泄漏还是内存溢出
  1. 内存泄漏(Memory Leak):很多对象不使用了,但是还保存着引用
  2. 内存溢出(Memory Overflow):对象过多
  • 内存泄漏,那么用工具查看对象到GC Roots的引用链,看为啥垃圾回收无法自动回收,确定泄漏的代码位置
  • 内存溢出,那么查看堆参数-Xmx和-Xmn,看物理内存是否可以调大,减少生命周期长的对象等

4、方法区内部结构

  • 方法区存储:类型信息、常量、静态变量、即时编译器后的代码缓存

4.1、类型信息

  • 是类class、接口interface、枚举enum、注解annotation中哪一种
  • 完整有效名称(包名.类名)
  • 直接父类的完整名称(接口和java.lang.Object没有父类)
  • 类型的修饰符(public、abstract、final等)
  • 类型直接接口的有序列表(实现的接口构成列表)

4.2、域(Field、属性)信息

  • 保存类型所有域(属性)的相关信息和声明顺序
  • 相关信息包含:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient等)

4.3、方法(method)信息:按顺序保存

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

4.4、Non-final类变量:(static修饰的变量,静态变量)

  • 逻辑上是类数据一部分
  • 在类的加载过程中链接的准备阶段设置默认初始值,初始化阶段赋予真实值
  • 类变量(non-final)被所有实例共享,没有实例化类对象也可访问(全局常量,staticfinal一起修饰)
  • 与final修饰的类变量不同,每个全局常量在编译时就分配了

4.5、Class文件常量池

  • 一个有效的字节码文件除了包含类的版本信息、字段、方法以及接口描述信息外,还包含一个常量池表。常量池表中包含字面量、域和方法的符号引用。
  • 字面量就是int i=5;String=”Hello World!”中的5和”Hello World!”

  • 一个JAVA源原文件中的类、接口,编译后生成字节码文件,Java中的字节码需要数据,但是这些数据很多很大,不能直接存到内存中,可以将其存到常量池中,字节码中包含了指向常量池的引用。
  • 常量池中包含:数量值、字符串引用、类引用、字段引用、方法引用

  • #2在常量池中是Object,#3是<init>方法
  • 常量池,可以看做一张表,虚拟机根据这个表找到要执行的类名、方法名、参数类型、字面量等类型

4.6、运行时常量池

  • 运行时常量池是方法区的一部分
  • 常量池表示Class中的一部分,用于存放编译器生成的各种字面量和符号引用,在加载类和接口到虚拟机后,就创建相应的运行时常量池
  • JVM为每个加载的类或接口维护一个运行时常量池,池中数据类似数组项,通过索引访问
  • 运行时常量池中含多种不同常量,包含编译器就明确的数值字面量,也包含运行期的方法或者字段引用,此时不再是常量池中的符号地址,而是真实地址。
  • 运行时常量池,相对于Class文件中的常量池,还有一个特征就是具备动态性,可以动态添加数据到运行时常量池
  • 当创建运行时常量池时,如果所需内存空间大于方法区能提供的最大值,那么JVM抛出OutOfMemoryError异常

5、方法区的演进细节

  • 只有HotSpot才有永久代,BEA JRockit、IBM J9是没有永久代概念的

  • HotSpot中方法区的演变:

  • 永久代是存于JVM虚拟机内存(堆也在虚拟内存)的,元空间是直接存在物理内存

5.1、为什么jdk7将字符串和常量变量从永久代转移到堆中

  • 永久代中垃圾回收效率低,在Full GC时才会回收永久代垃圾,而Full GC在老年代、永久代不足时触发,导致STW,效率低。而且开发中String多,而永久代垃圾回收效率低,所以将其放到堆中,回收较快

5.2、为什么元空间取代永久代?

  • 永久代空间大小很难确定,设置JVM内存小,加载的动态类多容易OOM;设置JVM内存大,浪费内存,于是可以直接将永久代中的东西存到直接物理内存
  • 对永久代调优困难,调整永久代中的类,要查看是否有引用,比较难

5.3、为什么jdk8以后,字符串常量池和静态变量还在堆中?

  • Jdk7中刚挪过去,立马挪回来不好吧,保持一致性,后面之后也没有再改了,说明挺合理
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值