Skip to content

方法区

1. 简介

方法区是存放基础信息的位置,线程共享,主要包含三部分内容:

  • 类的元信息: 保存了所有类的基本信息。
  • 运行时常量池: 保存了字节码文件中的常量池内容。
  • 字符串常量池: 保存了字符串常量。

2. 存储类的元信息

方法区是用来存储每个类的基本信息(元信息),一般称之为InstanceKlass对象。在类的加载阶段完成。
Alt text

3. 存储运行时常量池

方法区还存放了运行时常量池。常量池中存放的是字节码中的常量池内容。字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池。
Alt text

4. 方法区的变化

方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同。Hotspot设计如下:

  • JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。
  • JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。
    Alt text arthas中查看JDK8方法区
    Alt text

5. 模拟方法区溢出

通过ByteBuddy框架,动态生成字节码数据,加载到内存中。通过死循环不停地加载到方法区,观察方法区是 否会出现内存溢出的情况。分别在JDK7和JDK8上运行上述代码。引入依赖:

xml
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.16.1</version>
</dependency>

编写代码:

java
 public static void main(String[] args) {
    Jvm03 jvm03 = new Jvm03();
    String name = "Class";
    int count = 1;
    while (true) {
        // 类名不断变化,可以让类加载器不断加载
        name += count;
        // 创建ClassWriter对象
        ClassWriter classWriter = new ClassWriter(0);
        // 调用visit方法,创建字节码数据
        // Opcodes.V1_7表示生成jdk1.7版本的字节码
        classWriter.visit(
                Opcodes.V1_7,
                Opcodes.ACC_PUBLIC,
                name,
                null ,
                // 指明父类
                "java/lang/Object",
                null);
        byte[] bytes = classWriter.toByteArray();
        jvm03.defineClass(name, bytes, 0, bytes.length);
        System.out.println(count++);
    }
}

运行的话需要交给JDK7进行执行,然后修改生成字节码为1.8,再在JDK8环境运行。
实验发现,JDK7上运行大概十几万次,就出现了错误。在JDK8上运行百万次,程序都没有出现任何错误,但是内 存会直线升高。这说明JDK7和JDK8在方法区的存放上,采用了不同的设计。
Alt text

提示

ByteBuddy是一个基于Java的开源库,用于生成和操作Java字节码。

6. 方法区大小设置

  1. JDK7将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数-XX:MaxPermSize=值来控制。
  2. JDK8将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受 的上限,可以一直分配。可以使用-XX:MaxMetaspaceSize=值将元空间最大大小进行限制。
    为何要设置方法区大小呢,毕竟已经交由操作系统管理,JVM又不能回收?
    主要考虑到在一个操作系统里面部署多个应用的场景,防止某个应用的方法区不合理导致其他应用不可用,比如后部署的应用启动不起来,运行一段时间就所有应用都挂掉等等。
    在JDK8中设置-XX:MaxMetaspaceSize参数执行上面的程序:
    Alt text

7. 字符串常量池

方法区中除了类的元信息、运行时常量池之外,还有一块区域叫字符串常量池(StringTable)。字符串常量池存储在代码中定义的常量字符串内容。比如“123” 这个123就会被放入字符串常量池。
Alt text 字符串常量池和运行时常量池有什么关系?
早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整,将字符串常量池和运行时常量池做了拆分。
Alt text

  1. 通过字节码指令分析如下代码的运行结果? Alt text 答案是false true。原因是String d=a+b编译为使用StringBuilder进行字符串拼接,StringBuilder对象创建时存放在堆内存的,而String c="12"是在方法区字符串常量池创建,自然地址不同为false。而String d="1"+"2"编译为编译阶段直接连接,也就是String d="12",对于前面变量c已经创建的字符串常量12, 后面相同就不会创建而是使用引用,因为比较结果为true。

8. 神奇的intern

String.intern()方法是可以手动将字符串放入字符串常量池中,分别在JDK6、JDK8下执行代码,JDK6中结果是false false ,JDK8中是true false
Alt text jdk6中容易理解,使用intern()方法后两个字符串一个存在堆上一个存在字符串常量池,所以都是不等的都是false。
但是为何在jdk8中会第一个为true呢?原因在于JDK7及之后版本中由于字符串常量池在堆上,所以intern ()方法会把第一次(只是第一次才这样)遇到的字符串的引用放入字符串常量池。所以字符串常量池中存放的还是堆上面的对象,所以为true。
问题:运行时数据区都学完了,静态变量存储在哪里呢?
JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代。JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。
Alt text