JVM详解(五)——运行时数据区-方法区

一、概述

1、介绍

  《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。但对于HotSpot JVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
  所以,方法区看作是一块独立于Java堆的内存空间。官方文档:

  方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样,可以是不连续的。方法区的大小和堆一样,可选择固定大小可扩展。方法区的大小决定了系统可以保持多少个类,如果系统定义了太多的类,导致方法区溢出,会报OOM。比如:加载大量的第三方jar包,Tomcat部署的工程过多(30~50个),大量动态的生成反射类。关闭JVM,会释放这个区域的内存。

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

3、方法区的演进

  把方法区理解成接口,永久代和元空间是这个接口的落地实现。在JDK7及以前,习惯上把方法区称为永久代,JDK8开始,使用元空间取代了永久代。二者最大的区别是:元空间不在虚拟机设置的内存中,而是使用本地内存。
  本质上,方法区和永久代并不等价,仅是对HotSpot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求,像BEA JRockit,IBM J9中不存在永久代。
  如果方法区无法满足新的内存分配需求时,将报OOM。现在来看,当年使用永久代,不是好的idea,导致Java程序更容易OOM(超过-XX:MaxPermSize上限)。

4、设置方法区大小与OOM

  方法区的大小不是固定的,可以动态调整。
  JDK7及以前:
  -XX:PerSize来设置永久代初始空间大小。默认值是20.75M
  -XX:MaxPerSize来设置永久代最大空间大小。32位机器默认是64M,64位机器默认是82M。
  当JVM加载的类信息容量超过这个值,会报OOM:PerGen space
  JDK8及以后:
  元空间的大小可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定。
  Windows下,-XX:MetaspaceSize=约21M,-XX:MaxMetaspaceSize=-1,即没有限制。默认值依赖平台。对于一个64位的服务器端JVM来说,默认-XX:MetaspaceSize=21M。一旦触及这个水位线,Full GC将会触发,并卸载没用的类(即这些类对应的类加载器不再存活)。然后这个水位线将会重置,新的水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
  如果初始的水位线设置过低,上述水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁FC,建议将初始值设置为一个相对较高的值。
  代码示例:方法区OOM

 1 // jdk6/7中:
 2 // -XX:PermSize=10m -XX:MaxPermSize=10m
 3 
 4 // jdk8中:
 5 // -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 6 public class OOMTest extends ClassLoader {
 7     public static void main(String[] args) {
 8         int j = 0;
 9         try {
10             OOMTest test = new OOMTest();
11             for (int i = 0; i < 10_000; i++) {
12                 // 创建ClassWriter对象,用于生成类的二进制字节码
13                 ClassWriter classWriter = new ClassWriter(0);
14                 // 指明版本号,修饰符,类名,包名,父类,接口
15                 classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
16 
17                 byte[] code = classWriter.toByteArray();
18 
19                 // 类的加载
20                 // Class对象
21                 test.defineClass("Class" + i, code, 0, code.length);
22 
23                 j++;
24             }
25         } finally {
26             System.out.println(j);
27         }
28     }
29 }
30 
31 // 不设置参数
32 // 10000.无报错.使用动态的方法区,元空间.
33 // 在元空间创建并加载了10000个类.元空间做了一个动态的扩展.
34 
35 // JDK8设置参数
36 // 8466.
37 // Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

  如何解决这些OOM?
  (1)要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
  (2)如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots到引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确的定位出泄漏代码的位置。
  (3)如果不存在内存泄漏,内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx,-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

二、方法区的内部结构

1、介绍

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

  注意:这里只是一个经典的结构图,不完全正确。因为在jdk7以后,静态变量,运行时常量池里面的字符串常量池有变化,不再放在方法区了,而是放在了堆空间,这个后面会提到(看图)。

2、类型信息

  对每个加载的类型(类Class、接口Interface、枚举Enum、注解Annotation),JVM必须在方法区中存储以下类型信息:
  (1)这个类型的完整类路径(包名.类名)。
  (2)这个类型直接父类的完整类路径。
  (3)这个类型的修饰符(public,abstract,final的某个子集)。
  (4)这个类型直接接口的一个有序列表。
  类型信息里记录了,这个类是使用哪个类加载器加载进来的。

3、域(Field)信息

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

4、方法信息

  JVM必须保存所有方法以下信息,同域信息一样包括声明顺序:
  (1)方法名称。
  (2)方法的返回类型(或void)。
  (3)方法的参数列表。
  (4)方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)。
  (5)方法的字节码、操作数栈、局部变量表及大小(abstract和native方法除外)。
  (6)异常表(abstract和native方法除外),每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引。
  代码示例:方法区的内部构成

 1 public class Main extends Object implements Comparable<String>, Serializable {
 2     //属性
 3     public int num = 10;
 4     private static String str = "测试方法的内部结构";
 5     
 6     public void test1() {
 7         int count = 20;
 8         System.out.println("count = " + count);
 9     }
10 
11     public static int test2(int cal) {
12         int result = 0;
13         try {
14             int value = 30;
15             result = value / cal;
16         } catch (Exception e) {
17             e.printStackTrace();
18         }
19         return result;
20     }
21 
22     @Override
23     public int compareTo(String o) {
24         return 0;
25     }
26 }
  1 javap -v -p Main.class > temp.txt
  2 
  3 // 解析后的字节码文件.(删掉了部分无关的信息)
  4 Classfile /D:/workspace/Java/myweb/target/classes/com/lx/myweb/Main.class
  5   Last modified 2020-10-6; size 1670 bytes
  6   MD5 checksum 54aed047bf420df2ddb06256a1ed4a33
  7   Compiled from "Main.java"
  8 
  9 // 类型信息
 10 public class com.lx.myweb.Main extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
 11   minor version: 0
 12   major version: 52
 13   flags: ACC_PUBLIC, ACC_SUPER
 14 
 15 // 常量池.#1:符号引用
 16 Constant pool:
 17    #1 = Methodref          #18.#53        // java/lang/Object."<init>":()V
 18 ……
 19   #85 = Utf8               printStackTrace
 20 {
 21     
 22 // 域信息
 23   public int num;
 24     descriptor: I
 25     flags: ACC_PUBLIC
 26 
 27   private static java.lang.String str;
 28     descriptor: Ljava/lang/String;
 29     flags: ACC_PRIVATE, ACC_STATIC
 30 
 31 // 方法信息(构造器)
 32   public com.lx.myweb.Main();
 33     descriptor: ()V
 34     flags: ACC_PUBLIC
 35     Code:
 36       stack=2, locals=1, args_size=1
 37          0: aload_0
 38          1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 39          4: aload_0
 40          5: bipush        10
 41          7: putfield      #2                  // Field num:I
 42         10: return
 43       LineNumberTable:
 44         line 6: 0
 45         line 8: 4
 46       LocalVariableTable:
 47         Start  Length  Slot  Name   Signature
 48             0      11     0  this   Lcom/lx/myweb/Main;
 49 
 50 // 方法信息
 51   public void test1();
 52     descriptor: ()V
 53     flags: ACC_PUBLIC
 54     Code:
 55       stack=3, locals=2, args_size=1
 56          0: bipush        20
 57          2: istore_1
 58          3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
 59          6: new           #4                  // class java/lang/StringBuilder
 60          9: dup
 61         10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
 62         13: ldc           #6                  // String count =
 63         15: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
 64         18: iload_1
 65         19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
 66         22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
 67         25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
 68         28: return
 69       LineNumberTable:
 70         line 12: 0
 71         line 13: 3
 72         line 14: 28
 73       LocalVariableTable:
 74         Start  Length  Slot  Name   Signature
 75             0      29     0  this   Lcom/lx/myweb/Main;
 76             3      26     1 count   I
 77 
 78 // 方法信息
 79   public static int test2(int);
 80     descriptor: (I)I
 81     flags: ACC_PUBLIC, ACC_STATIC
 82     Code:
 83       stack=2, locals=3, args_size=1
 84          0: iconst_0
 85          1: istore_1
 86          2: bipush        30
 87          4: istore_2
 88          5: iload_2
 89          6: iload_0
 90          7: idiv
 91          8: istore_1
 92          9: goto          17
 93         12: astore_2
 94         13: aload_2
 95         14: invokevirtual #12                 // Method java/lang/Exception.printStackTrace:()V
 96         17: iload_1
 97         18: ireturn
 98       Exception table:
 99          from    to  target type
100              2     9    12   Class java/lang/Exception
101       LineNumberTable:
102         line 17: 0
103         line 19: 2
104         line 20: 5
105         line 23: 9
106         line 21: 12
107         line 22: 13
108         line 24: 17
109       LocalVariableTable:
110         Start  Length  Slot  Name   Signature
111             5       4     2 value   I
112            13       4     2     e   Ljava/lang/Exception;
113             0      19     0   cal   I
114             2      17     1 result   I
115       StackMapTable: number_of_entries = 2
116         frame_type = 255 /* full_frame */
117           offset_delta = 12
118           locals = [ int, int ]
119           stack = [ class java/lang/Exception ]
120         frame_type = 4 /* same */
121     MethodParameters:
122       Name                           Flags
123       cal
124 
125 // 子类方法信息
126   public int compareTo(java.lang.String);
127     descriptor: (Ljava/lang/String;)I
128     flags: ACC_PUBLIC
129     Code:
130       stack=1, locals=2, args_size=2
131          0: iconst_0
132          1: ireturn
133       LineNumberTable:
134         line 29: 0
135       LocalVariableTable:
136         Start  Length  Slot  Name   Signature
137             0       2     0  this   Lcom/lx/myweb/Main;
138             0       2     1     o   Ljava/lang/String;
139     MethodParameters:
140       Name                           Flags
141       o
142 
143 // 父类方法信息
144   public int compareTo(java.lang.Object);
145     descriptor: (Ljava/lang/Object;)I
146     flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
147     Code:
148       stack=2, locals=2, args_size=2
149          0: aload_0
150          1: aload_1
151          2: checkcast     #13                 // class java/lang/String
152          5: invokevirtual #14                 // Method compareTo:(Ljava/lang/String;)I
153          8: ireturn
154       LineNumberTable:
155         line 6: 0
156       LocalVariableTable:
157         Start  Length  Slot  Name   Signature
158             0       9     0  this   Lcom/lx/myweb/Main;
159     MethodParameters:
160       Name                           Flags
161       o                              synthetic
162 
163 // 静态变量信息
164   static {};
165     descriptor: ()V
166     flags: ACC_STATIC
167     Code:
168       stack=1, locals=0, args_size=0
169          0: ldc           #15  // String 测试方法的内部结构
170          2: putstatic     #16  // Field str:Ljava/lang/String;
171          5: return
172       LineNumberTable:
173         line 9: 0
174 }
175 Signature: #50  // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
176 SourceFile: "Main.java"
字节码文件

5、non-final的类变量

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

 1 public class Main {
 2     public static void main(String[] args) {
 3         Order order = null;
 4         order.method();
 5 
 6         System.out.println(order.count);
 7     }
 8 }
 9 
10 class Order {
11     public static int count = 1;
12 
13     public static void method() {
14         System.out.println("count = " + count);
15     }
16 }
17 
18 // IDEA会提示有编译错误.但是直接执行不会报错,不会有空指针.
19 // count = 1
20 // 1

6、final的类变量:全局常量

  被声明为 final static 的变量的处理方法不同,每个全局常量在编译的时候就被分配了。
  代码示例:

1 public class Main {
2 
3     public static int count = 1;
4     public static final int num = 100;
5 
6     public static void method() {
7         System.out.println("count = " + count);
8     }
9 }
 1 javap -v -p Main.class > temp.txt
 2 
 3 // 解析后的字节码文件.(删掉了部分无关的信息)
 4 {
 5   public static int count;
 6     descriptor: I
 7     flags: ACC_PUBLIC, ACC_STATIC
 8 
 9   public static final int num;
10     descriptor: I
11     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
12     ConstantValue: int 100  // 声明为final的静态变量在编译时就有值了
13 }

7、常量池

  方法区,内部包含了运行时常量池;字节码文件,内部包含了常量池。要理解方法区,就需要理解清楚字节码文件,因为加载类的信息都在方法区。要理解方法区的运行时常量池,就需要理解字节码文件的常量池。

  一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息,就是常量池(constant pool),包括各种字面量和对类型、域、方法的符号引用。
  (1)为什么需要常量池?
  一个Java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里。换一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。
  代码示例:

1 public class Main {
2     public static void main(String[] args) {
3         System.out.println("hello world");
4     }
5 }

  这里面使用了String、System、PrintStream及Object等结构。这里代码量已经很小了,如果代码多,引用到的结构会更多,就需要常量池了!
  (2)常量池中有什么?
  常量池内存储的数据类型包括:数量值,字符串值,类引用,字段引用,方法引用。

  小结:常量池,可以看做一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

8、运行时常量池

  运行时常量池是方法区的一部分。
  常量池表(Constant Pool)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
  JVM为每个已加载的类型(类或接口)都维护一个常量池,池中的数据项像数组项一样,是通过索引访问的。
  运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或字段引用,此时不再是常量池中的符号地址了,这里换为真实地址。
  运行时常量池,相对于Class文件常量池的另一重要特征是,具备动态性。
  运行时常量池类似于传统编程语言中的符号表,但是它所包含的数据却比符号表要更加丰富一些。
  字节码文件中的常量池经过类加载器加载放到方法区以后,对应的结构就称为运行时常量池。

三、方法区的使用举例

  代码示例:方法区使用举例

1 public class Main {
2     public static void main(String[] args) {
3         int x = 500;
4         int y = 100;
5         int a = x / y;
6         int b = 50;
7         System.out.println(a + b);
8     }
9 }

  下面讲解在程序执行的过程中,main方法的指令在一个一个执行的过程当中,程序计数器,虚拟机栈之间的协作关系。由于没有new对象,就不看堆空间的情况了。

四、方法区的演进细节(重要)

1、介绍

  首先明确,只有HotSpot才有永久代,BEA JRockit,IBM J9中不存在永久代。方法区如何实现属于虚拟机实现细节,《Java虚拟机规范》并不要求统一。
  JDK6及以前、JDK7、JDK8及以后,方法区的变化:

  StringTable,字符串常量池。
  注意:JDK7,此时的方法区(永久代)还是用的虚拟机的内存,和本地内存还有一个映射的概念。JDK8用的本地内存,不再使用虚拟机的内存。

2、永久代为什么被元空间替换?

  官网给的理由是因为 JRockit 没有,为了整合JRockit和Hotspot,所以去掉了。这里也很明确的可以看到,字符串常量池静态变量移动到堆中。

  http://openjdk.java.net/jeps/122

  随着Java8 的到来,HotSpot VM中再也见不到永久代了。但这并不意味着类的元数据信息消失了,这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫元空间。
  由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。这项变动是很有必要的,原因:
  (1)为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
  (2)对永久代进行调优是很困难的。对永久代的 Full GC 需要去判断这个类或者这个常量,不再被使用了,也挺花时间的。所以调优也会变得很困难。

3、StringTable(字符串常量池)为什么要调整位置?

  因为永久代的回收效率很低,在Full GC的时候才会触发。而Full GC是老年代的空间不足、永久代不足时才会触发,这就导致StringTable回收效率不高。而开发中会有大量的字符串被创建,回收效率低,将导致永久代内存不足,放到堆里,能及时回收内存。

4、如何证明静态变量存在哪?

  代码示例:

 1 // jdk7:-Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 2 // jdk8:-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 3 public class Main {
 4     // 100MB
 5     private static byte[] arr = new byte[1024 * 1024 * 100];
 6 
 7     public static void main(String[] args) {
 8         System.out.println(Main.arr);
 9 
10 //        try {
11 //            Thread.sleep(1000_000);
12 //        } catch (InterruptedException e) {
13 //            e.printStackTrace();
14 //        }
15     }
16 }

  结果:基于jdk8。jdk6,jdk7没有演示。

  结论:jdk6,7,8,静态引用对应的对象实体始终都存在堆空间。
  注意:这里指的new的对象实体,始终都在堆空间中。上面(图示)指的变化,是静态变量 arr 这个变量名。
  那么如何验证呢?
  案例:一个静态属性,一个非静态属性,一个局部变量。这三个变量的对象放在哪? 这三个变量本身放在哪?这是《深入理解Java虚拟机》中的案例,staticObj、instanceObj、localObj存放在哪里?
  代码示例:

 1 // staticObj、instanceObj、localObj存放在哪里?
 2 public class StaticObjTest {
 3 
 4     static class Test {
 5         static ObjectHolder staticObj = new ObjectHolder();
 6         ObjectHolder instanceObj = new ObjectHolder();
 7 
 8         void foo() {
 9             ObjectHolder localObj = new ObjectHolder();
10             System.out.println("done");
11         }
12     }
13 
14     private static class ObjectHolder {
15     }
16 
17     public static void main(String[] args) {
18         Test test = new StaticObjTest.Test();
19         test.foo();
20     }
21 }

  这里需要使用一个工具,JHSDB,这个工具JDK9才有。结论:

  这三个地址都在eden区,也就是在堆中。也就证明了这三个对象实体都在堆中。即:只要是new出来的对象实体都在堆中。

五、方法区的垃圾回收

1、介绍

  《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上,也确实有未实现或未能完整实现方法区类型卸载的收集器存在,如JDK 11时期的ZGC收集器就不支持类卸载。
  一般来说,这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
  方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

2、回收内容

  方法区常量池中主要存放的两大类常量:字面量和符号引用。
  字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。
  符号引用则属于编译原理方面的概念,包括下面三类常量:
  (1)类和接口的全限定名
  (2)字段的名称和描述符
  (3)方法的名称和描述符
  HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃常量与回收Java堆中的对象非常类似。

3、判定需要回收

4、小结

  方法区要不要垃圾回收?Java虚拟机规范中没有明说,可以回收,也可以不回收。平时说的HotSpot是要的。
  回收的话,主要针对的是什么?不再使用的类型信息,运行时常量池中废弃的常量。

  动态链接,指向了运行时常量池当前方法的引用。

posted @ 2021-10-20 10:19  Craftsman-L  阅读(155)  评论(0编辑  收藏  举报