【基础篇】十一、JVM方法区

1、方法区

方法区线程共享,存了以下几部分:

  • 类的元信息
  • 运行时常量池
  • 字符串常量池

类的元信息,即类生命周期的加载阶段的InstanceKlass对象。PS:图中InstanceKlass对象里的常量池、方法等,实际存的只是引用,JVM会把它们摘出来统一安排在一块内存上。

在这里插入图片描述

运行时常量池,和类生命周期的连接阶段的操作,把编号变为内存地址:

在这里插入图片描述

2、方法区的位置

方法区是一个概念,不同版本的JDK有不同的实现,对JDK7来说,永久代是其对方法区的落地实现(且此时永久代在堆区),对JDK8来说,则给方法区换了一种实现:元空间(元空间在本地内存)

在这里插入图片描述

方法区是一个虚拟概念,不同的虚拟机有不同的实现,对于HotSpot:

  • JDK7及以前,方法区在堆区的永久代空间里
  • JDK8及以后,永久代被移除,用元空间代替,方法区在元空间,而元空间在操作系统的直接内存里,理论上可以一直分配

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

使用阿尔萨斯查看:JDK8时,max为-1,即不设上限,但自然不会超过操作系统的内存上限

在这里插入图片描述

3、模拟方法区的溢出

通过ByteBuddy框架,生成类的字节码,然后往内存(方法区)中加载。首先引入依赖:

<!--ByteBuddy是一个用于生成和操作Java字节码的框架-->
<dependency>
	<groupId>net.bytebuddy</groupId>
	<artifactId>byte-buddy</artifactId>
	<version>1.12.23</version>
</dependency>

基本使用方式:

//创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(0);
//生成字节码数据
classWriter.visit(Opcodes.V1_7,Opcodes.ACC_PUBLIC,name,null ,"java/lang/Object",null);
byte[] bytes = classWriter.toByteArray();
//visit方法的形参中,第一个为编译类的JDK版本,name为类名,批量生成时,注意别重复,第五个为父类

Demo代码:

public class Demo1 extends ClassLoader {
    public static void main(String[] args) throws Exception {
        int count = 1;
        Demo1 demo1 = new Demo1();
        while (true) {
            ClassWriter classWriter = new ClassWriter(0);
            //参数1为JDK版本(JDK8),参数2为public修饰符,参数3为类名,参数4为包名,参数5为父类,参数6为接口
            classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + count, null, "java/lang/Object", null);
            byte[] bytes = classWriter.toByteArray();
            //加载字节码
            demo1.defineClass("Class" + count, bytes, 0, bytes.length);
            System.out.println(count++);
        }

    }
}

JDK7的JVM上运行,报错PermGen Space,而JDK8的JVM下则只是看到系统内存一直在涨:

在这里插入图片描述

-XX:MaxMetaspaceSize=值将元空间最大大小进行限制,再运行:

在这里插入图片描述

4、方法区的字符串常量池

字符串常量池存储在代码中定义的常量字符串的内容,比如"123"

在这里插入图片描述

关于字符串常量池和运行时常量池的关系:

在这里插入图片描述

图示:JDK6时:

在这里插入图片描述

JDK7时:

在这里插入图片描述
JDK8时:

在这里插入图片描述

5、常量池案例

如下,根据字节码,c指向字符串常量池,而a+b实际是用StringBuilder,得到一个String对象,指向堆内存,c不等于d
在这里插入图片描述

调整变量d的代码,现在输出为true,字节码中不再用StringBuilder:

在这里插入图片描述

+的两边是变量还是常量的区别为:

在这里插入图片描述

6、String的intern方法

intern方法手动将字符串放入字符串常量池中,如下:常量池中只是存了一份,结果为true:

在这里插入图片描述
案例2:
在这里插入图片描述
JDK6下运行:

false
false

JDK8下运行:

true
false

分析前置Tip:JVM启动时就会把java加入到常量池中。

原因:JDK6下的intern方法,第一次遇到字符串实例时,复制到永久代的字符串常量池中,并返回常量池中的引用,即s1.intern是一个指向字符串常量池的引用,而s1后面是个对象,因此s1是指向堆的一个引用。s1 不等于 s1.intern。同理,java字符串对象,s2.intern,发现常量池已有java,直接返回引用(地址),也是false。

在这里插入图片描述

JDK7及之后版本中由于字符串常量池在堆上,所以intern 方法会把第一次遇到的字符串的引用放入字符串常量池,此时,s1和s1.intern都指向堆里的think123对象,为true

在这里插入图片描述
而对于s2,常量池中已有java,因此s2.intern直接是字符串常量池中java的地址,不等于s2.

JDK7及以后,在堆上创建的字符串(对象),去调用intern时,只是在常量池中存放了这个对象的引用,而不是将字符串搬运到常量池中。

7、静态变量的存放位置

和JDK版本有关:

  • JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代
    在这里插入图片描述
  • JDK7及之后的版本中,静态变量是存放在中的Class对象中,脱离了永久代

在这里插入图片描述

PS:

在这里插入图片描述

8、常量池和运行时常量池的区别

常量池(Constant Pool)

常量池 是指在编译阶段生成的,用于存储编译期确定的一些常量,包括字符串常量、基本类型常量(如 int、float、long 等)、类引用、方法引用、字段引用等。常量池存在于每个 .class 文件中,具体来说,可以分为以下几个部分:

  • 字面量:包括字符串常量和基本类型的常量
  • 符号引用:包括类和接口的全限定名、字段名称和描述符、方法名称和描述符等

这些常量在 .class 文件中以常量池表(constant pool table)的形式存在,每个常量都有一个唯一的索引

运行时常量池(Runtime Constant Pool)

运行时常量池 是 JVM 在类加载过程中,从 .class 文件的常量池中提取出来并放入方法区中的一部分数据结构。它主要用于存储运行时需要用到的各种常量和符号引用。运行时常量池的特点:

  • 动态性:除了从编译期常量池中获取的常量外,运行时常量池可以动态地添加新的常量,比如通过 String.intern() 方法
  • 符号引用解析:在类加载阶段,JVM 会将符号引用解析为直接引用,这个过程可能会触发类加载、连接和初始化

区别总结

位置和存储时间:

  • 常量池:存在于每个 .class 文件中,是编译期生成的,在磁盘上存储
  • 运行时常量池:存在于 JVM 的方法区中,是运行时生成的,在内存中存储

内容:

  • 常量池:包含编译期确定的字面量和符号引用
  • 运行时常量池:包含编译期常量池中的内容,并且可以在运行时动态扩展,包含运行时解析后的符号引用

修改性:

  • 常量池:不可修改,一旦编译完成就固定了
  • 运行时常量池:可以在运行时动态添加新的常量

用途:

  • 常量池:用于支持编译期的常量表达式和符号引用
    运- 行时常量池:用于支持运行时的类加载和动态链接
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-代号9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值