JVM运行时数据区篇(方法区进阶掌握)

Posted ProChick

tags:

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

1.方法区的内部结构


1.类型信息

JVM必须在方法区保存类的信息,类型种类包括( 类class、接口interface、枚举enum、注解annotation )

  • 类型的完整有效名称
  • 类型的直接父类的完整有效名称
  • 类型的修饰符
  • 类型的接口有序列表

2.域信息

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

  • 域名称
  • 域类型
  • 域修饰符

3.方法信息

JVM必须在方法区保存所有方法的所有相关信息以及方法的声明顺序

  • 方法名称
  • 方法的返回类型
  • 方法参数的数量和类型
  • 方法的修饰符
  • 方法的字节码、操作数栈、局部变量表及大小( 抽象方法和本地方法除外
  • 异常表信息,每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引( 抽象方法和本地方法除外 )

4.静态变量

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

    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!");
        }
    }
    
  • 当静态变量又被 final 关键字修饰后,类变量的处理方法则不同。它被称为全局静态常量,在编译的时候就被分配了值,而静态常量是在类加载器的链接( 准备 )阶段初始化赋值,然后在初始化阶段分配了值

5.常量池

  • 常量池包含在字节码文件中,包括各种字面量、类型域、方法的符号引用

  • 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表( Constant Pool Table )

  • 常量池存放的数据类型一般有:数量值、字符串值、类引用、字段引用、方法引用

  • 总之,常量池可以看做是一张表,虚拟机指令则根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息

为什么需要常量池呢?

  • 其实哪怕是最简单的代码结构,比如对于输出语句来讲,它也需要加载大量的 System、Printstream 及 Object 等结构才能运行,所以我们不可能把这些信息全部存储到字节码文件中

  • 换句话来说,一个Java源文件中的类、接口,编译后会产生一个字节码文件,而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码文件里。所以我们需要单独开辟一块空间,把这些信息存到常量池中去,而字节码则只需存储指向该常量池的引用即可,这就减少了字节码文件的大小

6.运行时常量池

  • 运行时常量池( Runtime Constant Pool )是方法区的一部分
  • 前面说常量池表( Constant Pool Table )是字节码文件中的一部分,用于存放编译期生成的各种字面量与符号引用,而这部分内容在经过类加载后就会存放到方法区的运行时常量池中
  • 当类或者接口被类加载器加载到虚拟机后,就会创建对应的运行时常量池。所以JVM为每个已加载的类或接口都维护着一个常量池,池中的数据像数组一样,是通过索引访问的。注意,当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OOM异常。
  • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。所以说运行时常量池相比于常量池,最大的不同就是具备了动态性
  • 运行时常量池中存储的已经不再是常量池中的符号地址了,而是这些引用的真实地址

2.方法区的调用过程示例

public class MethodAreaDemo {
    public static void main(String[] args) {
        int x = 500;
        int y = 100;
        int a = x / y;
        int b = 50;
        System.out.println(a + b);
    }
}

字节码信息

调用过程

3.方法区的演进细节

  • JDK6及之前:有永久代,而且静态变量也存放在永久代上

  • JDK7:有永久代,但已经逐步有“去永久代的思想”,字符串常量池、静态变量被转移到了堆中存储

  • JDK8及之后: 无永久代,类型信息、字段信息、方法信息、常量池保存在本地内存的元空间,但字符串常量池、静态变量仍然保存在堆中

  • 为什么将永久代替换为元空间

    • 首先,对于永久代设置空间大小是很难确定的。如果设置的小,那么当动态加载类过多时,则容易产生Full GC进行垃圾回收或者OOM异常。如果设置的大,那么又会造成空间的浪费。
    • 其次,对永久代进行调优的过程也是错综复杂的,是很困难的,这也必然会浪费很多时间
    • 所以基于上面两个原因, 在JDK8之后,这些数据就被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间。由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,所以大大减轻了上面出现的问题
  • 为什么要将字符串常量(String Table)和静态变量放在堆中

    • 首先,在JDK7中将 String Table 放到了堆空间中。因为永久代的回收效率很低,只有当老年代的空间不足、永久代空间不足时,才会触发 Full GC 对其进行回收,这就导致了 String Table 的回收效率不高。而我们开发中经常会有大量的字符串被创建,那么回收效率低的话,就会导致永久代内存不足。所以将它转移到堆中存放,这就能够保证它可以及时被回收

    • 其次,怎么证明静态变量到底放在哪?

      public class StaticObjTest {
          static class Test {
              // 变量1:静态变量
              static ObjectHolder staticObj = new ObjectHolder();
              // 变量2:成员变量
              ObjectHolder instanceObj = new ObjectHolder();
      
              void foo() {
                  // 变量3:局部变量
                  ObjectHolder localObj = new ObjectHolder();
                  System.out.println("done");
              }
          }
      
          private static class ObjectHolder {
          }
      
          public static void main(String[] args) {
              Test test = new StaticObjTest.Test();
              test.foo();
          }
      }
      
      

      首先通过 JHSDB 工具查看对象的地址,发现三个变量所指向的对象地址都是在堆空间的Eden区域范围内,所以这也说明了所有对象的分配都是在堆空间中

      通过 Inspector 查看该对象实例,发现变量名和实例对象都被存储在 java.lang.Class 类型的对象实例中,进而可推断出静态变量也被存储在堆空间中

4.方法区的垃圾回收

  • 关于方法区回收
    • 有些人认为方法区是没有垃圾收集行为的,其实不然。JVM规范对于方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在
    • 一般来说方法区的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。比如:以前在 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 Hotspot 虚拟机对方法区未完全回收而导致的重大内存泄漏
    • 方法区的垃圾收集主要回收两部分内容:常量池中的常量和不再使用的类型(类信息、域信息、方法信息)
  • 对于常量的回收
    • 方法区内常量池之中主要存放的两大类常量:字面量和符号引用。 字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等, 而符号引用则属于编译原理方面的概念
    • 常量一般包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
    • HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收,这和回收Java堆中的对象类似
  • 对于类型的回收
    • 要判定一个类型是否属于“不再被使用的类”,需要同时满足下面三个条件
      • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
      • 加载该类的类加载器已经被回收
      • 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就是无法在任何地方通过反射访问该类的方法
    • Java虛拟机被允许对满足上述三个条件的无用类进行回收,但这里说的也仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会被回收。关于进一步确定是否要对类型进行回收,HotSpot虚拟机还提供了 -Xnoclassgc 参数对其进行控制
    • 在大量使用反射、动态代理、CGLib等字节码框架,动态生成 JSP 以及 OSGi 这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力

以上是关于JVM运行时数据区篇(方法区进阶掌握)的主要内容,如果未能解决你的问题,请参考以下文章

JVM运行时数据区篇(方法区基本概述)

JVM运行时数据区篇(本地方法栈)

JVM运行时数据区篇(堆空间基本概述)

JVM运行时数据区篇(虚拟机栈帧结构)

JVM运行时数据区篇(基础认知)

JVM运行时数据区篇(程序计数器)