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 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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是要的。
回收的话,主要针对的是什么?不再使用的类型信息,运行时常量池中废弃的常量。
动态链接,指向了运行时常量池当前方法的引用。
作者:Craftsman-L
本博客所有文章仅用于学习、研究和交流目的,版权归作者所有,欢迎非商业性质转载。
如果本篇博客给您带来帮助,请作者喝杯咖啡吧!点击下面打赏,您的支持是我最大的动力!