1.定义
保存在着被加载过的每一个类的信息;包括类的成员变量,方法数据,成员方法以及构造器方法的代码部分,类的构造器。这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中;
方法区是线程共享的;当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
方法区在虚拟机启动时被创建,逻辑上是堆的一部分,但具体实现上不同的jvm厂商实现方式是不一样的。例如1.8之前Oracle厂商的方法区是放在堆内存中的,称为永久代,但在1.8以后,方法区被移出了堆内存,不在由java虚拟机管理,而是放到了本地操作系统的进程中,称为元空间。
方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。jvm也可以允许用户和程序指定方法区的初始大小,最小和最大限制;
方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展Java程序,这样可能会导致一些类,不再被使用,变为垃圾。这时候需要进行垃圾清理。
2.运行时常量池
我们在方法区中可以看到有一个组成结构是常量池,下面简单介绍一下什么是常量池。
2.1 常量池
定义:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
举个例子。首先,我们看一个简单程序的Hello程序,一个程序要运行需要先编译成二进制字节码,编译后生成的二进制字节码包括:类的基本信息,类的常量池,类中的一些方法定义(包含虚拟机指令).因为二进制字节码很难看懂,所以我们使用java提供的工具对编译后的class文件进行反编译。
javap -v HelloWorld.class #反编译指令
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
Classfile /E:/JavaWorkspace/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
Last modified 2021-8-31; size 567 bytes
MD5 checksum 8efebdac91aa496515fa1c161184e354
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
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: 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: 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"
在第三张图中有一行Constant pool,此处就是常量池,主要包括符号和地址。
我们看下面这段代码,这段代码对应于HelloWorld程序的执行流程。在第六行,我们可以看到
0: getstatic #2 // Field
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: 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;
}
意思是获取一个静态成员变量,获取地址在#2,此时他就会去Constant pool中找 #2 地址的信息,#2 信息如下:
#2 = Fieldref #21.#22
Fieldref表示引用成员变量,然后他会根据后面的地址#21 #22 继续走地址,直到找到需要的变量。
2.2 运行时常量池
常量池是*.class文件中的,当类被加载,它的常量池信息就会放入到运行时常量池,并把里面的符号地址变为真实地址。
注意:每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;
2.3 StringTable(串池) 面试题
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
String s4 = s1 + s2;
String s5 ="ab";
String s6 = s4.intern();
//问
System.out.printl(s3 == s4);
System.out.printl(s3 == s5);
System.out.printl(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
//问,如果调换了 【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.printl(x1 == x2);
3.元空间内存溢出演示代码
/**
* 环境:jdk1.8及以后
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* 需要将内存大小进行修改:-XX:MaxMetaspaceSize=8m
*/
public class Demo 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);
}
}
}
修改内存步骤
方法区和堆一样,可以设定方法区的大小,使用IDEA编译器,在右上角点击如下图标:
选择第一个Edit Configurations,然后出来以下界面: