运行时常量区-方法区

  • Post author:
  • Post category:其他


金樽清酒斗十千,玉盘珍羞直万钱。



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

从内存结构来看:

运行时数据区大致可分为:

程序计数器



虚拟机栈



本地方法栈







方法区

五部分。

在这里插入图片描述

从线程共享与否的角度来看:


方法区





是线程共享的;

程序计数器



虚拟机栈



本地方法栈

是线程私有的。而

元空间(jdk8及以上)

/

永久代(JDK7及以前)

是方法区的实现落地。

在这里插入图片描述

从一个常见的代码来看栈、堆、方法区的关系

Person person = new Person();

//第一个Person是类信息,存在于方法区
//第二个person是成员变量,存在于虚拟机栈的局部变量表中,指向堆中真正的对象。
//new Person()是具体的一个对象,存放在堆中

在这里插入图片描述



2、方法区的理解

《Java虚拟机规范》明确提出:尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择区进行

垃圾收集

或者

进行压缩



但对HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开

,为此

方法区可以看成一块独立的内存空间

方法区的特点:

  1. 方法区和堆一样,是线程共享的内存区域
  2. 方法区在JVM启动的时候被创建,并且它的实际的物理内存中和Java堆区一样都可以是不连续的。
  3. 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展

  4. 方法区的大小决定了系统可以保存多少个类信息

    ,如果系统定义了太多类或加载了大量第三方jar文件,这些都可能导致方法区溢出,JVM同样会抛出OOM

一个很神奇的事情,一个普通的类运行起来,JVM到底加载了多少个类?

public class TestClassLoader {
    public static void main(String[] args) {
        System.out.println("start");

        try {
            Thread.sleep(1000000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }
}

在这里插入图片描述

竟然有1633个类!!!这里面不但包括了自身的类信息,也包括类父类信息,类使用的其他类的信息等等等…



3、方法区在HotSpot VM中的演变

在JDK7及之前,方法区的实现称为

永久代(PermGen Space)

,在JDK8到现在称为

元空间(Meta Space)

,这二者是对方法区的不同实现

在这里插入图片描述

在JDK8中及以后,永久代的概念被废弃,改用了JRockit,J9一样在本地内存中实现的元空间来代替

元空间的本质和永久代相似,都是方法区的落地实现,不过元空间和永久代最大的区别在于:

元空间不再像永久代一样使用JVM的内存,而使用本地物理内存

,二者不但名字不同,使用的内存不同,内部结构也进行了调整



4、设置方法区大小

方法区的大小是不固定的,JVM可以根据应用的需要动态调整


在JDK7及以前

  • 通过

    -XX:PermSize=x

    来设置永久代初始分配空间,默认值是20.75M
  • 通过

    -XX:MaxPermSize=x

    来设置永久代的最大空间 32位机器最大64M 64位机器最大82M


在JDK8到现在:

  • 通过

    -XX:MetaspaceSize=x

    来设置元空间初始分配空间,默认是21M
  • 通过

    -XX:MaxMetaspaceSize=x

    来设置永久代的最大空间,默认为-1 即没有限制,可用物理内存多大就能用多大
  • 在开发中,方法区一般只设置初始分配空间,不设置最大空间。
  • 对于

    64位

    的服务器端JVM来说,其默认的

    -XX:MetaspaceSize



    21M

    ,这是初始的高水位线。一旦方法区的大小触及到这个高水位线,会开启

    Full GC

    对堆和方法区进行垃圾回收,将方法区中没有的类信息回收,然后这个高水位线将会重置。新的高水位线的值取决于Full GC后方法区释放了多少空间,如果释放的空间不足,在不超过MaxMetaspace时,适当提高该值,如果释放空间较多,则适当降低该值

可以看见元空间和永久代最大的差距是

内存方面的实现

,元空间使用

本地内存

,本地内存有多大,元空间就可以有多大,而永久代的内存空间

由JVM管辖

,受JVM的内存大小的限制。



5、OOM的排查

  1. 要解决方法区或堆出现的OOM,一般手段是通过内存映像分析工具(如Java VisualVM)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是说

    先要分析出是出现了“内存泄漏” 还是 “内存溢出”
  2. 如果是出现了内存泄漏,可以通过工具查看泄漏对象到GC Roots的引用链,于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的,掌握了泄漏对象的信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置
  3. 如果不存在内存泄漏,就是说内存中的对象确实还必须存活,那就应该检查虚拟机的堆参数,与机器物理内存,从代码上检查对象生命周期是否过长,持有时间是否过长,尝试减少程序运行期间的内存消耗

内存溢出和内存泄露

内存泄漏:已申请的内存空间无法释放

内存溢出:申请空间是,可用内存不够。



6、方法区的内部结构

方法区存储的信息包括:

类型信息

,

常量

,

静态变量

,

即时编译器编译后的代码缓存

,

运行时常量池



6.1 类型信息


类型信息的详细内容

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

  1. 这个类型的完整名称(包名.类名)
  2. 这个类型的直接父类的完整名称(interface和Object类没有直接父类)
  3. 这个类型的修饰符(public,abstract,final)的某个子集
  4. 这个类型直接接口的一个有序列表


字段(成员)信息:

(JVM必须在方法区中保存类型所有成员的信息及声明顺序):

  1. 成员名称
  2. 成员类型
  3. 成员修饰符(public,protected,static,volatile,transient)的某个子集


方法信息:

(JVM必须在方法区保存所有方法的信息及声明顺序)

  1. 方法名称
  2. 方法返回类型
  3. 方法的参数和类型(按顺序)
  4. 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract)的某个子集
  5. 方法的字节码(bytecodes),操作数栈,局部变量表及大小(abstract和native方法除外)
  6. 方法的异常表(abstract和native方法除外)

对下面的程序进行分析,查看字节码文件

public class MethodInnerStructTest extends ArrayList implements Runnable, Serializable {
    //属性
    public int num = 10;
    private static String str = "方法区内部结构";

    //构造器

    //方法
    public void test1() {
        int num = 20;
        System.out.println("count= " + num);
    }

    public static int test2(int cal) {
        int result = 0;
        try {
            int value = 30;
            result = value / cal;
        }catch (Exception e) {

        }

        return result;
    }

    //接口方法
    @Override
    public void run() {
    }
}

对其进行反编译:

//类型信息(完整包名,父类完整包名,接口完整包名
public class com.shang.jvm.runtimedata.MethodArea.MethodInnerStructTest extends java.util.ArrayList implements java.lang.Runnable,java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER  //类的修饰符
      
// 常量池
Constant pool:
   #1 = Methodref          #15.#41        // java/util/ArrayList."<init>":()V
   #2 = Fieldref           #14.#42        // com/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest.num:I
   #3 = Fieldref           #43.#44        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Class              #45            // java/lang/StringBuilder
   #5 = Methodref          #4.#41         // java/lang/StringBuilder."<init>":()V
   #6 = String             #46            // count=
   #7 = Methodref          #4.#47         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #4.#48         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #9 = Methodref          #4.#49         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #50.#51        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #52            // java/lang/Exception
  #12 = String             #53            // 方法区内部结构
  #13 = Fieldref           #14.#54        // com/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest.str:Ljava/lang/String;
  #14 = Class              #55            // com/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest
  #15 = Class              #56            // java/util/ArrayList
  #16 = Class              #57            // java/lang/Runnable
  #17 = Class              #58            // java/io/Serializable
  #18 = Utf8               num
  #19 = Utf8               I
  #20 = Utf8               str
  #21 = Utf8               Ljava/lang/String;
  #22 = Utf8               <init>
  #23 = Utf8               ()V
  #24 = Utf8               Code
  #25 = Utf8               LineNumberTable
  #26 = Utf8               LocalVariableTable
  #27 = Utf8               this
  #28 = Utf8               Lcom/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest;
  #29 = Utf8               test1
  #30 = Utf8               test2
  #31 = Utf8               (I)I
  #32 = Utf8               value
  #33 = Utf8               cal
  #34 = Utf8               result
  #35 = Utf8               StackMapTable
  #36 = Class              #52            // java/lang/Exception
  #37 = Utf8               run
  #38 = Utf8               <clinit>
  #39 = Utf8               SourceFile
  #40 = Utf8               MethodInnerStructTest.java
  #41 = NameAndType        #22:#23        // "<init>":()V
  #42 = NameAndType        #18:#19        // num:I
  #43 = Class              #59            // java/lang/System
  #44 = NameAndType        #60:#61        // out:Ljava/io/PrintStream;
  #45 = Utf8               java/lang/StringBuilder
  #46 = Utf8               count=
  #47 = NameAndType        #62:#63        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #48 = NameAndType        #62:#64        // append:(I)Ljava/lang/StringBuilder;
  #49 = NameAndType        #65:#66        // toString:()Ljava/lang/String;
  #50 = Class              #67            // java/io/PrintStream
  #51 = NameAndType        #68:#69        // println:(Ljava/lang/String;)V
  #52 = Utf8               java/lang/Exception
  #53 = Utf8               方法区内部结构
  #54 = NameAndType        #20:#21        // str:Ljava/lang/String;
  #55 = Utf8               com/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest
  #56 = Utf8               java/util/ArrayList
  #57 = Utf8               java/lang/Runnable
  #58 = Utf8               java/io/Serializable
  #59 = Utf8               java/lang/System
  #60 = Utf8               out
  #61 = Utf8               Ljava/io/PrintStream;
  #62 = Utf8               append
  #63 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #64 = Utf8               (I)Ljava/lang/StringBuilder;
  #65 = Utf8               toString
  #66 = Utf8               ()Ljava/lang/String;
  #67 = Utf8               java/io/PrintStream
  #68 = Utf8               println
  #69 = Utf8               (Ljava/lang/String;)V
{
  //域信息:域名称,域类型,域修饰符
  public int num;
    descriptor: I
    flags: ACC_PUBLIC

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

  //方法信息:在字节码角度来看,构造器和普通方法都是方法,没有什么不同
  public com.shang.jvm.runtimedata.MethodArea.MethodInnerStructTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/util/ArrayList."<init>":()V
         4: aload_0
         5: bipush        10
         7: putfield      #2                  // Field num:I
        10: return
      LineNumberTable:
        line 12: 0
        line 14: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest;

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

  public static int test2(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        30
         4: istore_2
         5: iload_2
         6: iload_0
         7: idiv
         8: istore_1
         9: goto          13
        12: astore_2
        13: iload_1
        14: ireturn
      Exception table:
         from    to  target type
             2     9    12   Class java/lang/Exception
      LineNumberTable:
        line 26: 0
        line 28: 2
        line 29: 5
        line 32: 9
        line 30: 12
        line 34: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            5       4     2 value   I
            0      15     0   cal   I
            2      13     1 result   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = [ int, int ]
          stack = [ class java/lang/Exception ]
        frame_type = 0 /* same */

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 40: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/shang/jvm/runtimedata/MethodArea/MethodInnerStructTest;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #12                 // String 方法区内部结构
         2: putstatic     #13                 // Field str:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 15: 0
}
SourceFile: "MethodInnerStructTest.java"



6.2 类变量 (non-final)

静态变量和类变量关联在一起,随着类加载而加载,它们成为类数据在逻辑上的一部分。

类变量被所有的实例共享,即使没有类使用也可以进行访问。

public class MethodAreaTest {
    public static void main(String[] args) {
        Order order = null;
        System.out.println(order);
        order.hello();
        System.out.println(order.count);
    }
}

class Order {
    public static int count = 10;

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

//输出结果
null
hello
10



7、class文件中的常量池 Constant Pool

方法区中还有一个重要的结构

运行时常量池

,而在字节码文件中包含了

常量池


字节码文件被类加载子系统加载到方法区后,常量池中的内容就被加载到了方法区对应的运行时常量池

,在理解运行时常量池前,先分析字节码文件中的常量池

在这里插入图片描述

一个有效的字节码文件中除了包含类的版本信息,成员,方法以及接口等描述信息外,还有一项信息就是常量池,

常量池保存各种字面量(文本字符串,final修饰的常量,基本数据类型的值,变量名,方法名…),以及类型,成员和方法的符号引用

常量池中保存的字面量 (CONSTANT_Utf8_info都是字面量)



7.1 为什么需要常量池

一个Java源文件中的类,接口等信息编译后产生一个字节码文件。

字节码文件才是JVM需要的原材料,字节码文件也需要大量的数据支持,通常这些数据很大以至于不能直接存放到字节码文件中。如果将这些数据全直接放到字节码文件中,字节码文件会很庞大,因此采用常量池来

“节省空间”

保存这些数据,以压缩整个字节码文件

一句简单的话理解:

多个类都使用到了输出流,那么这些类加载过程中都需要将输出流加载到方法区中吗?不一定把!其实只需要一份,这些类只要保存输出流的一个引用就可以了。刚好,常量池中保存的就是引用,这样不就节省了大量了资源了吗?



8、方法区的运行时常量池

常量池是字节码文件中的一部分,用于存放编译期间生成地各种字面量和符号引用。

当字节码文件经过类加载子系统加载后,常量池中的内容被加载到方法区对应的运行时常量池

运行时常量池的特点:


  1. 当某个类型(类,接口,枚举,注解)被加载到JVM后,就会在方法区为它创建对应的运行时常量池

    ,方法区中存储若干个运行时常量池,运行时常量池通过索引访问数据

  2. 当在方法区创建类型的运行时常量池时,如果创建运行时常量池所需的空间超过了方法区剩余的空间,就会抛出OOM

  3. 运行时常量池中包括

    编译期已经明确的字面量,和运行期将符号引用解析后获得的类,方法,成员的直接引用(真实地址指针)


  4. 运行时常量池,相对于class文件常量池的最大特征是 “具备动态性”

    ,也就是运行期间也能将常量放入运行时常量池,而不是class文件常量池编译后内容就不可更改



9、方法区在HotSpot VM中的演进细节



9.1 不同版本JDK中方法区的构成

HotSpot VM中不同版本方法区的变化

  1. jdk 1.6及之前: 有永久代

    静态变量和字符串常量池(String Table)存放在永久代中(只有HotSpot才有永久代)

    在这里插入图片描述

  2. jdk 1.7: 有永久代

    但已经逐步“去永久代” 将永久代中的字符串常量池,静态变量移除,保存在堆里

    在这里插入图片描述

  3. jdk 1.8及之后: 无永久代

    类型信息,成员信息,方法信息,运行时常量池保存在本地内存的元空间,将字符串常量池和静态变量保存在堆里

    在这里插入图片描述



为什么要用元空间替代永久代?

永久代的思路就是所有的内存都由JVM分配管理,让JVM控制程序运行时的一切,而为了融合JRockit(JRockit没有永久代,方法区应该存储的数据都保存在物理内存),并且减少方法区出现OOM的问题,就使用元空间替换永久代,将方法区应该存储的数据放在物理内存

原因:

  1. 永久代的空间大小很难确定,在加载的类过多的情况下易出现OOM 如一个庞大的Web工程,运行时要不断动态地加载很多类,就容易出现OOM 而元空间使用物理内存,仅元空间大小仅限于本地机器可用物理内存大小
  2. 对永久代的调优很困难,很难避免多次的Full GC



9.2 字符串常量池为什么要放在堆里?

JDK7中将字符串常量池(StringTable)放到了堆空间中,

因为永久代的回收效率很低,在Full GC时才会触发

,而Full GC是因为老年代的空间不足和永久代空间不足才触发的,这就导致StringTable回收效率不是很高


因为在开发中有大量的字符串被创建,如果将字符串常量池放在永久代,那么就会导致字符串回收效率低,导致永久代空间不足,而将StringTable放到堆里,能及时回收字符串



10、方法区的垃圾回收

JVM规范对方法区的回收是没有明确规定的,因为

方法区的回收效果比较难令人满意,尤其是类的卸载,条件相当苛刻



但是对方法区的回收又是必要的

,不对方法区进行回收,就会导致方法区的可用容量越来越少,在老版本的HotSpot VM中常常因为对方法区未完全回收导致内存泄漏,进而内存溢出


方法区的GC主要回收两部分内容

:废弃常量+不再使用的类型信息


废弃常量是指

:方法区中运行时常量池内不被任何地方引用的字面量和直接引用

**不再使用的类型信息是指:**方法区中不再使用的类型信息,成员信息,方法信息

但是类型信息的回收条件很苛刻,需要同时满足:

  1. 该类所有的对象已经被回收,即堆中不存在该类和该类的子类的对象
  2. 加载该类的类加载器已经被回收,通常很难达成
  3. 该类的Class对象没有在任何地方被引用

即使同时满足了上述三个条件,该类型信息也只是允许被回收,而不是和对象一样没有引用就直接被回收。

但是在大量的使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OGSi这类 频繁自定义类加载器的场景中,通常需要JVM具备类型卸载的功能,以保证不会对方法区造成太大压力



版权声明:本文为weixin_45483328原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。