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

发布时间:2024年01月04日

1、方法区

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

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

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

在这里插入图片描述

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

在这里插入图片描述

2、方法区的位置

方法区是一个虚拟概念,不同的虚拟机有不同的实现,对于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);
            //JDK版本为8
            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对象中,脱离了永久代

在这里插入图片描述

文章来源:https://blog.csdn.net/llg___/article/details/135371507
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。