一文理解JVM的方法区

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文理解JVM的方法区相关的知识,希望对你有一定的参考价值。

5.1 栈、堆、方法区的交互关系

当我们写出如下的代码:

f()
   Person person=new Person();

这里Person是我们生成的字节码,也就是.class文件,此时JVM到底是怎么存的呢?

 方法区主要存放的是 Class,而堆中主要存放的是实例化的对象,而栈里存的是堆中对象的地址,针对上面的例子,具体的存储策略是:

  1. Person 类的 .class 信息存放在方法区中

  2. 真正创建的 person 对象存放在 Java 堆中

  3. person 变量存放在 Java 栈的局部变量表中,就是常见的reference字段。其实就是堆中person对象的地址。

  4. 在 person 对象中,有个指针指向方法区中的 Person 类型数据,表明这个 person 对象是用方法区中的 Person 类 new 出来的。

首先,方法区在哪里呢?对于不同的虚拟机是略有不同,对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。所以,方法区可以看作是一块独立于Java堆的内存空间。

方法区(Method Area)是各个线程共享的内存区域。多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等待该线程加载完毕,然后直接使用该类,即类只能加载一次。

方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。而且方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace。常见的问题有如下几种情况:加载大量的第三方的jar包、Tomcat部署的工程过多(30~50个)或者大量动态的生成反射类。

执行一个程序需要加载的类数量可能远超我们的想象,例如下面这个简单的类:

public class MethodAreaDemo 
    public static void main(String[] args) 
        System.out.println("start...");
        try 
            Thread.sleep(1000000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
​
        System.out.println("end...");
    

执行时,通过jvisualvm观察,可以发现其加载了1600多个类。

方法区与永久代、元空间的关系

在 JDK7 及以前习惯上把方法区称为永久代,对于Hotspot来说,可以将方法区和永久代看作等价的。 到了JDK8就完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存

永久代、元空间二者并不只是名字变了,内部结构也调整了。根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。

6.2 方法区的内部结构

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

本部分的内容,我们不需要全记住,只要理解清楚,分析的时候知道到哪里找就可以了。

6.2.1 类型信息

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

  1. 这个类型的完整有效名称(全名=包名.类名)

  2. 这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)

  3. 这个类型的修饰符(public,abstract,final的某个子集)

  4. 这个类型直接接口的一个有序列表

6.2.2 域(Field)信息

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

  1. JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

  2. 域的相关信息包括:域名称,域类型,域修饰符例如public,private,protected,static,final,volatile,transient等等。

6.2.3 方法(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 MethodStructTest extends Object implements Comparable<String>,Serializable 
    //属性
    public int num = 10;
    public String info = "info";
    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 MethodStrucTest.class

此时可以看到生成一段特别长的代码,我们将其中的几个重要部分截取出来看看。

1.类型信息

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

public class ch3_JMM.topic3_Dump.MethodStructTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable

2.域信息(成员的信息)

我们在上面定义了两个成员变量num和info,其相关信息是:

   public int num;
    descriptor: I
    flags: ACC_PUBLIC
​
  public java.lang.String info;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC

这里的descriptor: I 表示字段类型为 Integer,而info对应的就是java.lang下的String类型。

这里的flags: ACC_PUBLIC表示类型是public的,ACC_PRIVATE表示是private的,ACC_STATIC表示是静态的

//域信息
  public int num;
    descriptor: I
    flags: ACC_PUBLIC
​
  private static java.lang.String str;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC

3.方法信息

  1. descriptor: ()V 表示方法返回值类型为 void

  2. flags: ACC_PUBLIC 表示方法权限修饰符为 public

  3. stack=3 表示操作数栈深度为 3

  4. locals=2 表示局部变量个数为 2 个(实力方法包含 this)

  5. test1() 方法虽然没有参数,但是其 args_size=1 ,这是因为将 this 作为了参数

public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: bipush        20
         2: istore_1
         3: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: new           #6                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        13: ldc           #8                  // String count =
        15: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: iload_1
        19: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        22: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 13: 0
        line 14: 3
        line 15: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lch3_JMM/topic3_Dump/MethodStructTest;
            3      26     1 count   I

6.2.4 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!");
    

6.2.5 全局常量:static final

  1. 全局常量就是使用 static final 进行修饰

  2. 被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

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

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

执行javap -v Order.class可以看到与上述两个变量有关的信息如下:

 public static int count;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
​
  public static final int number;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2

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

6.2.6 运行时常量池

在我们上面使用javap查看class文件内容时总有这么长一段的内容:

Constant pool:
   #1 = Methodref          #13.#31        // java/lang/Object."<init>":()V
   #2 = Fieldref           #32.#33        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #34            // start...
   ·····
   #55 = Utf8               (J)V
   #56 = Utf8               printStackTrace

这就是字节码的常量池,而将其加载到内存的过程就是运行时常量池。为什么需要常量池呢?一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,因此可以将其存到常量池。这个字节码包含了指向常量池的地址,在动态链接的时候再将其链接到一起,比如如下的代码:

public class SimpleClass 
    public void sayHello() 
        System.out.println("hello");
    

虽然上述代码只有194字节,但是里面却使用了String、System、PrintStream及Object等结构。再比如我们这个文件中有6个地方用到了"hello"这个字符串,如果不用常量池,就需要在6个地方全写一遍,造成臃肿。我们可以将"hello"等所需用到的结构信息记录在常量池中,并通过引用的方式,来加载、调用所需的结构。如果代码多的话,引用的结构将会更多,这里就体现出常量池的好处了。

常量池可以看做是一张表,常量池内部的内容比较多,例如数量值、字符串值、类引用、字段引用、方法引用等等,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

而运行时常量池(Runtime Constant Pool)是方法区的一部分。JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址

以上是关于一文理解JVM的方法区的主要内容,如果未能解决你的问题,请参考以下文章

一文搞懂 JVM 架构和运行时数据区 (内存区域)

多图:一文带你入门掌握JVM所有知识点

一文了解JAVA虚拟机的重要组成

Java虚拟机系列一:一文搞懂 JVM 架构和运行时数据区

《JVM系列》 第三章 -- 深入理解JVM运行时数据区

JVM合理理解大总结_耐心阅读