7.方法区
7.1 定义
方法区是java虚拟机中所有线程共享的共享区域,主要存放类的结构相关信息(成员变量,方法、构造器的代码),运行时常量池,类加载器。方法区在虚拟机启动时被创建,在逻辑上属于堆的组成部分(具体产商实现时不一定遵守逻辑上的划分标准)。
在jdk1.8以前,方法区位于jvm的永久代,字符串存放在常量池。在jdk1.8以后,方法区则位于本地内存的元空间,不再占用JVM的内存空间,而字符串存在于堆。具体参考下图。
💡Tip:
方法区其实是逻辑上的概念,因为您可以发现,在jdk1.8以后,他甚至在物理存储空间上是拆分开的。
7.2 方法区内存溢出
通过下面代码可以演示方法区内存溢出。
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 public static void main(String[] args) { int j = 0; try { Demo1_8 test = new Demo1_8(); for (int i = 0; i < 10000; i++, j++) { // ClassWriter 作用是生成类的二进制字节码 ClassWriter cw = new ClassWriter(0); // 参数含义:版本号, 访问级别为public, 类名, 包名, 父类, 接口 cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 返回 byte[] byte[] code = cw.toByteArray(); // 执行了类的加载 test.defineClass("Class" + i, code, 0, code.length); // Class 对象 } } finally { System.out.println(j); } } }
在jdk1.8以前的版本,将永久代设置为8m: -XX:MaxPermSize=10m,在jdk1.8以后,将元空间设置为8m: -XX:MaxMetaspaceSize=8m,将会报出OutOfMemoryError.
在实际的工作场景中,spring、mybatis等框架都使用了cglib来动态生成class,因此框架使用不当是可能导致方法区内存溢出的。不过在jdk1.8以后方法区移到了元空间,空间充裕了很多,也由元空间进行垃圾回收,内存溢出的可能降低了。
7.3 常量池
下面是一个helloworld的代码。
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令) public class HelloWorld { public static void main(String[] args) { System.out.println("hello world"); } }
计算机最终会把这段代码转换为二进制代码后执行,这段二进制代码包含类基本信息、类方法定义(包含指令)、常量池。我们切换到out路径下对应的class文件所在目录,使用反编译命令javap -v xxx.class将二进制代码的内容转为可读的代码一探究竟。
Classfile /F:/资料 解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t5/helloworld.class Last modified 2021年9月9日; size 567 bytes SHA-256 checksum 37204bf6e654f64ae56660a1e8becfaa98b3ae7592b81b4b6e331de92a460b96 Compiled from "HelloWorld.java" public class cn.itcast.jvm.t5.HelloWorld minor version: 0 major version: 52 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #5 // cn/itcast/jvm/t5/HelloWorld super_class: #6 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // hello world #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #26 // cn/itcast/jvm/t5/HelloWorld #6 = Class #27 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #7:#8 // "<init>":()V #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 hello world #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 cn/itcast/jvm/t5/HelloWorld #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V { public cn.itcast.jvm.t5.HelloWorld(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/t5/HelloWorld; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello world 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 6: 0 line 7: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; } SourceFile: "HelloWorld.java"
如上,常量池原来就是一张常量表。注意到其中方法定义部分如#2等内容,这其实就对应着常量池(表)Constant pool的常量。
常量池是.class文件中的,当类被加载时,常量池的内容就会被放入运行时常量池,并且其中的符号地址将会变为真实地址。
7.4 String table
下面看一个面试题。
String s1 = "a"; String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; String s5 = "a" + "b"; System.out.println(s3 == s4); System.out.println(s3 == s4);
要回答这个问题,需要搞清楚string table,先从最简单的说起,反编译下列代码。
public class Demo1_22 { public static void main(String[] args) { String s1 = "a"; // 懒惰的 String s2 = "b"; String s3 = "ab"; } }
截取部分。可以看到jvm从#2取String a存放到了LocalVariableTable的Slot1,其他串与此类似。
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=4, args_size=1 0: ldc #2 // String a 2: astore_1 3: ldc #3 // String b 5: astore_2 6: ldc #4 // String ab 8: astore_3 9: return LineNumberTable: line 11: 0 line 12: 3 line 13: 6 line 21: 9 LocalVariableTable: Start Length Slot Name Signature 0 10 0 args [Ljava/lang/String; 3 7 1 s1 Ljava/lang/String; 6 4 2 s2 Ljava/lang/String; 9 1 3 s3 Ljava/lang/String;