jvm方法区的内部结构

245 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24天,点击查看活动详情

方法区存储什么?

概念

image.png

《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

image.png

类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  1. 这个类型的完整有效名称(全名=包名.类名)
  2. 这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
  3. 这个类型的修饰符(public,abstract,final的某个子集)
  4. 这个类型直接接口的一个有序列表

域(Field)信息

也就是我们常说的成员变量,域信息是比较官方的称呼

  1. JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
  2. 域的相关信息包括:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

方法(Method)信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

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

举例

**
 * 测试方法区的内部构成
 */
public class MethodInnerStrucTest extends Object implements Comparable<String>,Serializable {
    //属性
    public int num = 10;
    private static String str = "测试方法的内部结构";
    //构造器
    //方法
    public void test1(){
        int count = 20;
        System.out.println("count = " + count);
    }
    public static int test2(int cal){
        int result = 0;
        try {
            int value = 30;
            result = value / cal;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public int compareTo(String o) {
        return 0;
    }
}

javap -v -p MethodInnerStrucTest.class > test.txt

  • 反编译字节码文件,并输出值文本文件中,便于查看。参数 -p 确保能查看 private 权限类型的字段或方法

太长了,就贴部分字节码,如下图:

image.png

类型信息

在运行时方法区中,类信息中记录了哪个加载器加载了该类,同时类加载器也记录了它加载了哪些类

//类型信息      
public class com.atguigu.java.MethodInnerStrucTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable

域信息

  1. descriptor: I 表示字段类型为 Integer
  2. flags: ACC_PUBLIC 表示字段权限修饰符为 public
//域信息
  public int num;
    descriptor: I
    flags: ACC_PUBLIC

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

方法信息

  1. descriptor: ()V 表示方法返回值类型为 void
  2. flags: ACC_PUBLIC 表示方法权限修饰符为 public
  3. stack=3 表示操作数栈深度为 3
  4. locals=2 表示局部变量个数为 2 个(实力方法包含 this)
  5. test1() 方法虽然没有参数,但是其 args_size=1 ,这时因为将 this 作为了参数

image.png

non-final 类型的类变量

  1. 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
  2. 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它

举例

  1. 如下代码所示,即使我们把order设置为null,也不会出现空指针异常
  2. 这更加表明了 static 类型的字段和方法随着类的加载而加载,并不属于特定的类实例
public class MethodAreaTest {
    public static void main(String[] args) {
        Order order = null;
        order.hello();
        System.out.println(order.count);
    }
}

class Order {
    public static int count = 1;
    public static final int number = 2;


    public static void hello() {
        System.out.println("hello!");
    }
}

输出结果:

hello!
1

全局常量:static final

  1. 全局常量就是使用 static final 进行修饰
  2. 被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

查看上面代码,这部分的字节码指令

class Order {
    public static int count = 1;
    public static final int number = 2;
    ...
}    

image.png

可以发现 staitc和final同时修饰的number 的值在编译上的时候已经写死在字节码文件中了。