JVM07-运行时数据区-方法区

1 方法区

1.1 堆,虚拟机栈,方法区的交互关系

运行时数据区可大致分为五部分:
方法区,堆,虚拟机栈,程序计数器,本地方法栈
在这里插入图片描述
从线程共享与否的角度看:
方法区和堆是线程共享的,虚拟机栈,程序计数器,本地方法栈是线程私有的
元空间(JDK8)/永久代(JDK7及之前)是方法区的落地实现
在这里插入图片描述
看这样一个简单的代码:

public static void main(String[] args) {
	Person person = new Person();        
}

其中Person是类型信息      存放在方法区
person是引用变量          存放在main线程的虚拟机栈的局部变量表中 是一个指针 它指向堆中真实的对象
new Person()是具体的对象  真正的对象存放在堆中

在这里插入图片描述

1.2 方法区的理解

JVM规范中明确说到尽管方法区在逻辑上是堆的一部分,但对于HotSpot VM来说,方法区还有一个别名叫Non-Heap(非堆),目的就是和堆分开,为此方法区可以看成一块独立的内存空间

方法区的特点:
1,方法区与堆一样,是线程共享的内存区域

2,方法区在JVM启动时被创建,JVM关闭时销毁,且它实际的物理内存和堆一样都可以是不连续的

3,方法区的大小,可以选择固定大小或使用参数设置

4,方法区的大小决定了系统可以保存多少个类信息,如果系统定义了太多类或加载了大量第三方jar文件,这些都可能导致方法区溢出,JVM同样会抛出OOM

看一个简单的示例,打开Java VisualVM看看执行该方法,方法区中保存了多少类信息:
在这里插入图片描述
可以看到一个非常简单的程序,方法区中保存了1600个类的信息,包括当前类的信息,当前类使用的其它类的信息,当前类的父类信息等等

1.3 方法区在HotSpot VM中的演进过程

在JDK7及之前,方法区的实现称为永久代(PermGen Space),在JDK8到现在称为元空间(Meta Space),这二者是对方法区的不同实现

在这里插入图片描述
在JDK8中及以后,永久代的概念被废弃,改用了JRockit,J9一样在本地内存中实现的元空间来代替

元空间的本质和永久代相似,都是方法区的落地实现,不过元空间和永久代最大的区别在于
元空间不再像永久代一样使用JVM的内存,而使用本地物理内存,二者不但名字不同,使用的内存不同,内部结构也进行了调整

1.4 设置方法区的大小

方法区的大小不是固定的,可以通过参数调整

在JDK7及以前,永久代大小的设置:

通过-XX:PermSize=x来设置永久代初始分配空间 默认值是20.75M

通过-XX:MaxPermSize=x来设置永久代的最大空间 32位机器最大64M 64位机器最大82M
当JVM加载的类信息超过了永久代的最大空间,会抛出
OutOfMemoryError:PermGen space

在JDK8到现在,元空间大小的设置:

通过-XX:MetaspaceSize=x来设置元空间初始分配空间 默认是21M

通过-XX:MaxMetaspaceSize=x来设置永久代的最大空间 默认为-1 即没有限制 可用物理内存多大 就能用多大
但当本地物理内存也不够方法区使用时,会抛出
OutOfMemoryError:Metaspace

开发中方法区一般只设置初始分配空间,不设置最大空间
对于64位的服务器端JVM来说,其默认的-XX:MetaspaceSize21M,这是初始的高水位线
一旦方法区的大小触及到这个高水位线,会开启Full GC对堆和方法区进行垃圾回收
将方法区中没有的类信息回收,然后这个高水位线将会重置
新的高水位线的值取决于Full GC后方法区释放了多少空间
如果释放的空间不足,在不超过MaxMetaspace时,适当提高该值
如果释放空间较多,则适当降低该值

如果方法区初始内存设置的较低,高水位线的调整会发生很多次
也会就说Full GC进行了很多次,为此应该将方法区初始内存大小设置稍大一些,避免多余的Full GC

可以看到元空间和永久代最大的差距是最大内存的设置,永久代有最大限制,元空间默认会使用机器的所有可用物理内存,极大的减少了方法区发生OOM的概率应该将元空间的初始大小设置的较大一些,避免多余的Full GC

1.5 OOM的排查

1,要解决方法区或堆出现的OOM,一般手段是通过内存映像分析工具(如Java VisualVM)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是说先要分析出是出现了“内存泄漏” 还是 “内存溢出”

内存泄漏: 已申请的内存空间,但该空间无法被释放
一次内存泄漏问题不大,但多次内存泄漏就会导致内存溢出

内存溢出: 申请空间时,可用空间不足

2,如果是出现了内存泄漏,可以通过工具查看泄漏对象到GC Roots的引用链,于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的,掌握了泄漏对象的信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置

3,如果不存在内存泄漏,就是说内存中的对象确实还必须存活,那就应该检查虚拟机的堆参数,与机器物理内存,从代码上检查对象生命周期是否过长,持有时间是否过长,尝试减少程序运行期间的内存消耗

1.6 方法区存储的信息

class文件将被类加载子系统加载到方法区,方法区用于存储:

类型信息 成员信息 方法信息 JIT代码缓存 运行时常量池

不同JDK版本方法区的结构稍有差别,但从JDK8到现在元空间没有变动过

对class文件使用javap -v -p xxx.class查看反编译后的完整结构

1,类型信息:

对每个加载的类型(class,interface,enum,annotation),JVM必须在方法区中存储以下类型信息:
1,这个类型的完整名称(包名.类名)
2,这个类型的直接父类的完整名称(interfaceObject类没有直接父类)
3,这个类型的修饰符(public,abstract,final)的某个子集
4,这个类型直接接口的一个有序列表

如:
public class com.coisini.methodarea.MethodAreaStruct implements java.lang.Runnable,java.io.Serializable

2,字段(成员)信息:

JVM必须在方法区中保存类型所有成员的信息及声明顺序

成员信息包括: 成员名称,成员类型
			 成员修饰符(public,protected,static,volatile,transient)的某个子集

如:
private int num;
    descriptor: I
    flags: ACC_PRIVATE

  private static java.lang.String str;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC

  private static final int i;
    descriptor: I
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: int 12

3,方法信息:

JVM必须在方法区保存所有方法的信息及声明顺序

方法信息包括:
1,方法名称
2,方法返回类型
3,方法的参数和类型(按顺序)
4,方法的修饰符(public,private,protected,static,final,synchronized,native,abstract)的某个子集
5,方法的字节码(bytecodes),操作数栈,局部变量表及大小(abstractnative方法除外)
6,方法的异常表(abstractnative方法除外)
	每个异常处理的开始位置,结束位置
	代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引

如:
 private void test1();
    descriptor: ()V
    flags: ACC_PRIVATE
    Code:
      stack=2, locals=2, args_size=1
         0: bipush        40
         2: istore_1
         3: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: iload_1
         7: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        10: return
      LineNumberTable:
        line 17: 0
        line 18: 3
        line 19: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/coisini/methodarea/MethodAreaStruct;
            3       8     1 tmpCount   I

1.7 class文件中的常量池 Constant pool

方法区中还有一个重要的结构运行时常量池,而在字节码文件中包含了常量池
字节码文件被类加载子系统加载到方法区后,常量池中的内容就被加载到了方法区对应的运行时常量池,在理解运行时常量池前,先分析字节码文件中的常量池

在这里插入图片描述
一个有效的字节码文件中除了包含类的版本信息,成员,方法以及接口等描述信息外,还有一项信息就是常量池,

常量池保存各种字面量(文本字符串,final修饰的常量,基本数据类型的值,变量名,方法名…),以及类型,成员和方法的符号引用

常量池中保存的字面量 (CONSTANT_Utf8_info都是字面量)
在这里插入图片描述
常量池中保存的符号引用(类型,成员,方法):
在这里插入图片描述

1.7.1 为什么需要常量池

一个Java源文件中的类,接口等信息编译后产生一个字节码文件
字节码文件才是JVM需要的原材料,字节码文件也需要大量的数据支持
通常这些数据很大以至于不能直接存放到字节码文件中
如果将这些数据全直接放到字节码文件中,字节码文件会很庞大
因此采用常量池来“节省空间地”保存这些数据,以压缩整个字节码文件

比如下面的代码:
在这里插入图片描述
这是一个非常简单的类,但是里面却用到了String,System,PrintStream等结构,这已经是一个很简单的代码了,如果这个类方法很多,很复杂,那么引用到的结构会更多,如果将这些引用信息直接全部存放在class文件中,那么class文件会很庞大,为此
使用常量池,将类中出现的字面量保存起来,并把类型信息,成员信息,方法信息转换成对应的符号,存储在常量池中,这些符号就是符号引用,当执行时将符号引用转换成直接引用,即指向数据真实地址的指针

在这里插入图片描述
如上述类中的test2()方法,观察其对应的JVM指令,可以看到有 #5 #6 #7 这样的数据出现,这样的数据就对应常量池中索引为5,6,7的常量

#5在常量池中是一个符号引用 代表类型信息<java/lang/RuntimeException>
在这里插入图片描述
#6在常量池中是一个字面量 代表字符串 "除0错"
在这里插入图片描述
在这里插入图片描述

#7 是一个符号引用 用到了#5和#51 代表<java/lang/RuntimeException>类中的init方法
在这里插入图片描述
常量池可以看成一张表,虚拟机指令根据这张常量表找到要使用的字符串,常量,基本数据类型值,类型信息,方法信息,成员信息等来执行JVM指令

1.8 方法区中的运行时常量池

常量池是字节码文件中的一部分,用于存放编译期间生成地各种字面量和符号引用
当字节码文件经过类加载子系统加载后,常量池中的内容被加载到方法区对应的运行时常量池

运行时常量池的特点:
1,当某个类型(类,接口,枚举,注解)被加载到JVM后,就会在方法区为它创建对应的运行时常量池,方法区中存储若干个运行时常量池,运行时常量池通过索引访问数据

2,当在方法区创建类型的运行时常量池时,如果创建运行时常量池所需的空间超过了方法区剩余的空间,就会抛出OOM

3,运行时常量池中包括编译期已经明确的字面量,和运行期将符号引用解析后获得的类,方法,成员的直接引用(真实地址指针)

4,运行时常量池,相对于class文件常量池的最大特征是 “具备动态性”,也就是运行期间也能将常量放入运行时常量池,而不是class文件常量池编译后内容就不可更改

1.9 执行一个方法的大致过程

跟踪下面main方法的指令来看看执行指令的过程
在这里插入图片描述
在这里插入图片描述

 0 sipush 500
 // 指令0 将500压入操作数栈
 
 3 istore_1
 // 指令3 将栈顶数据500出栈 并存储到局部变量表的1号Slot
 
 4 bipush 100
 // 指令4 将100压入操作数栈

 6 istore_2
 // 指令6 将栈顶数据100出栈 并存储到局部变量表的2号Slot

 7 iload_1
 // 读取局部变量表1号Slot的数据500 并压入操作数栈

 8 iload_2
 // 读取局部变量表2号Slot的数据100 并压入操作数栈

 9 idiv
 // 将栈底数据除以栈顶数据的结果5 压入操作数栈

10 istore_3
// 将操作数栈栈顶的数据5 存储到局部变量表的3号Slot

11 bipush 50
// 将50压入操作数栈

13 istore 4
// 将操作数栈顶的数据50 存储到局部变量表的4号Slot

15 getstatic #2 <java/lang/System.out>
// 获取System类的out成员
// 运行时 运行时常量池中不再存储#2 
// 而是<java/lang/System.out>在方法区对应的地址

18 iload_3
// 将局部变量表中3号Slot的数据5取出 压入操作数栈

19 iload 4
// 将局部变量表中4号Slot的数据50取出 压入操作数栈

21 iadd
// 将操作数栈中栈底和栈顶的数据相加55 压入操作数栈

22 invokevirtual #3 <java/io/PrintStream.println>
// 执行out对象的println方法 打印栈顶的数55
// 运行时 运行时常量池中不再存储#3 
// 而是<java/io/PrintStream.println>在方法区对应的地址

25 return
// 方法结束 栈帧消耗

1.10 方法区在HotSpot VM中的演进细节

1.10.1 不同版本JDK中方法区的构成

HotSpot VM中不同版本方法区的变化:

jdk 1.6及之前: 有永久代 
	静态变量和字符串常量池(String Table)存放在永久代中(只有HotSpot才有永久代)

在这里插入图片描述

jdk 1.7: 有永久代,但已经逐步“去永久代”
	将永久代中的字符串常量池,静态变量移除,保存在堆里

在这里插入图片描述

jdk 1.8及之后: 无永久代
	类型信息,成员信息,方法信息,运行时常量池保存在本地内存的元空间
	将字符串常量池和静态变量保存在堆里

在这里插入图片描述
为什么要用元空间替换永久代?
永久代的思路就是所有的内存都由JVM分配管理,让JVM控制程序运行时的一切,而为了融合JRockit(JRockit没有永久代,方法区应该存储的数据都保存在物理内存),并且减少方法区出现OOM的问题,就使用元空间替换永久代,将方法区应该存储的数据放在物理内存

1,永久代的空间大小很难确定,在加载的类过多的情况下易出现OOM
如一个庞大的Web工程,运行时要不断动态地加载很多类,就容易出现OOM
而元空间使用物理内存,仅元空间大小仅限于本地机器可用物理内存大小

2,对永久代的调优很困难,很难避免多次的Full GC

1.10.2 字符串常量池为什么要放在堆里

JDK7中将字符串常量池(StringTable)放到了堆空间中,因为永久代的回收效率很低,在Full GC时才会触发,而Full GC是因为老年代的空间不足和永久代空间不足才触发的,这就导致StringTable回收效率不是很高

因为在开发中有大量的字符串被创建,如果将字符串常量池放在永久代,那么就会导致字符串回收效率低,导致永久代空间不足,而将StringTable放到堆里,能及时回收字符串

1.11 方法区的垃圾回收

JVM规范对方法区的回收是没有明确规定的,因为方法区的回收效果比较难令人满意,尤其是类的卸载,条件相当苛刻但是对方法区的回收又是必要的,不对方法区进行回收,就会导致方法区的可用容量越来越少,在老版本的HotSpot VM中常常因为对方法区未完全回收导致内存泄漏,进而内存溢出

方法区的GC主要回收两部分内容:废弃常量+不再使用的类型信息

废弃常量
即方法区中运行时常量池内不被任何地方引用的字面量和直接引用
不再使用的类型信息
即方法区中不再使用的类型信息,成员信息,方法信息
类型信息的回收条件很苛刻,需要同时满足:
1,该类所有的对象已经被回收,即堆中不存在该类和该类的子类的对象

2,加载该类的类加载器已经被回收,通常很难达成

3,该类的Class对象没有在任何地方被引用

即使同时满足了上述三个条件,该类型信息也只是允许被回收,而不是和对象一样没有引用就直接被回收
但是在大量的使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OGSi这类
频繁自定义类加载器的场景中,通常需要JVM具备类型卸载的功能,以保证不会对方法区造成太大压力
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值