方法区(线程共享)
一、基本概念
1、概述
- 方法区(Method Area)与 Java 堆一样,是线程共享的内存区域。
- 方法区在 JVM 启动的时候被创建,关闭 JVM 就会释放这个区域的内存。
- 方法区的大小,跟堆空间一样,可以选择 固定大小 或者 可扩展。
- 在 jdk7 及以前,习惯上把方法区,称为永久代。jdk8 开始,使用元空间取代了永久代。
- 元空间与永久代最大的区别在于:元空间不在虚拟机的内存中,而是使用本地内存
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出OOM:
- jdk8之前:
java.lang.OutOfMemoryError: PermGen space
- jdk8开始:
java.lang.OutOfMemoryError: Metaspace
- jdk8之前:
2、方法区属于堆?
《Java 虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。” 但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。
所以,对于 HotSpotJVM 而言,方法区看作是一块独立于 Java 堆的内存空间。
3、栈、堆、方法区(关系)
4、设置方法区大小
方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。
1)JDK7及以前(永久代)
设置永久代的大小:
-XX:Permsize
来设置永久代初始分配空间。默认值是20.75M-XX:MaxPermsize
来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
查看永久代的大小:
jinfo -flag PermSize 进程id
(通过jps
获取进程id)jinfo -flag MaxPermsize 进程id
(通过jps
获取进程id)
2)JDK8及以后(元空间)
设置元空间的大小:
-XX:MetaspaceSize
来设置元空间初始分配空间。默认值约为21M-XX:MaxMetaspaceSize
来设定元空间最大可分配空间。默认值是-1,即没有限制。
查看元空间的大小:
jinfo -flag MetaspaceSize 进程id
(通过jps
获取进程id)jinfo -flag MaxMetaspaceSize 进程id
(通过jps
获取进程id)
3)元空间的设置建议
-XX:MetaspaceSize
默认值约为21M,这就是初始的高水位线,一旦触及这个水位线,将会触发 Full GC 并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。
- 如果释放的空间不足,那么在不超过
MaxMetaspaceSize
的情况下,适当提高该值。 - 如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。
为了避免频繁地GC调高水位线,建议将-XX:MetaspaceSize
设置为一个相对较高的值(保持默认值-1即可)。
5、方法区的OOM
永久代:当 JVM 加载的类信息容量超过了 MaxPermsize
,就会报OutOfMemoryError:PermGen space
异常。
元空间:MaxMetaspaceSize
默认无限制,JVM耗尽所有的可用系统内存,才会报OutOfMemoryError:Metaspace
异常
/**
* jdk6/7中:
* -XX:PermSize=10m -XX:MaxPermSize=10m
*
* jdk8中:
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
*/
public class OOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTest test = new OOMTest();
for (int i = 0; i < 10000; i++) {
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,修饰符,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("Class" + i, code, 0, code.length);//Class对象
j++;
}
} finally {
System.out.println(j);
}
}
}
3331
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:757)
at java.lang.ClassLoader.defineClass(ClassLoader.java:636)
at methodArea.OOMTest.main(OOMTest.java:20)
二、方法区的内部结构
方法区(Method Area)用于存储以下信息:
注意:静态变量只有 jdk1.7 之前存放在方法区,从 jdk1.7 开始存放在堆中(这里的静态变量指的是静态变量引用,而不是实例)
1、类型信息
对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation),JVM 必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于 interface 或是 java.lang.Object,都没有父类)
- 这个类型的修饰符(public,abstract,final 的某个子集)
- 这个类型实现接口的一个有序列表
2、域信息(Field)
JVM 必须在方法区中保存 类型的所有域的相关信息 以及 域的声明顺序。域的相关信息包括:
- 域名称
- 域类型
- 域修饰符(public,private,protected,static,final,volatile,transient 的某个子集)
3、方法信息(Method)
JVM 必须保存所有方法的以下信息,同域信息一样包括声明顺序:
-
方法名称
-
方法的返回类型(或 void)
-
方法参数的数量和类型(按顺序)
-
方法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
-
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
-
异常表(abstract 和 native 方法除外)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
4、运行时常量池
- 方法区,内部包含了运行时常量池
- 字节码文件,内部包含了常量池
- 要弄清楚方法区的运行时常量池,先要理解清楚 字节码文件 中的常量池。
1)常量池
常量池(Constant Pool)是 class 文件的一部分,用于存放编译期生成的各种 字面量 与 符号引用。
- 字面量:Java语言层面的常量,如:文本字符串、基础数据、声明为final的常量等
- 符号引用:编译原理方面的常量,如:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
JVM 为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的 [1, count-1]。
字节码文件反编译的结果中,那些带#
的,都是引用的常量池中的内容。下面以本章 案例分析 的 构造方法 为例:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field num:I
10: return
Constant pool:
#1 = Methodref #18.#54 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#55 // methodArea/MethodAreaStructTest.num:I
...
#17 = Class #71 // methodArea/MethodAreaStructTest
#18 = Class #72 // java/lang/Object
...
#21 = Utf8 num
#22 = Utf8 I
...
#28 = Utf8 <init>
#29 = Utf8 ()V
...
#54 = NameAndType #28:#29 // "<init>":()V
#55 = NameAndType #21:#22 // num:I
...
#71 = Utf8 methodArea/MethodAreaStructTest
#72 = Utf8 java/lang/Object
常量池、可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
2)运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分,是常量池在运行期的一个表示形式。
- 在加载类和接口到虚拟机后,就会创建对应的运行时常量池,并将常量池的内容加载到运行时常量池中。
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
- 运行时常量池,相对于 Class 文件中常量池的另一重要特征是:具备动态性。( 例如
String.intern()
) - 运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些。
- 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,也会造成OOM
3)整型常量池
整数常量池是运行时常量池的一部分,是运行时常量池中专门用于存储整型常量的部分。
为了提高程序的执行效率,将 [-128, 127]
之间256个整数所有的包装对象提前创建好了,放在了整数常量池中。
- 范围在
[-128, 127]
内的整数,装箱成包装类时,底层不会new对象。共用在整数常量池当中的256个Integer对象。
public class IntConstantPool {
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b); // true
System.out.println(c == d); // false
}
}
4)为什么需要常量池?
一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候就会用到运行时常量池。
比如下的代码:
public class SimpleClass {
public void sayHello() {
System.out.println("hello");
}
}
虽然只有 194 字节,但是里面却使用了 String、System、PrintStream 及 Object 等结构。这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这里就需要用到常量池了。
- 常量池的作用:就是为了提供一些符号和常量,便于指令的识别
- 在不同的方法,都可能调用常量或者方法,只需要存储一份,然后记录其引用即可,节省了空间。
5、案例分析
- 要弄清楚方法区,需要理解清楚 ClassFile,因为方法区中的类信息是从 ClassFile 加载的。
- 要弄清楚方法区的运行时常量池,需要理解清楚 ClassFile 中的常量池。
package methodArea;
import java.io.Serializable;
public class MethodAreaStructTest implements Comparable<String>, Serializable {
public int num = 10;
private static String str = "测试方法区的内部结构";
private static final int count = 2;
public void test1() {
int count = 20;
System.out.println("count = " + count);
}
public static int test2(int numerator, int denominator) {
int result = 0;
try {
result = numerator / denominator;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
@Override
public int compareTo(String o) {
return 0;
}
}
通过 javap -v -p MethodInnerStrucTest.class
反编译(这里就不放完整的了,截取其中相关部分说明)
0)常量池
字节码文件反编译的结果中,那些带#
的,都是引用的常量池中的内容
Constant pool:
#1 = Methodref #18.#54 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#55 // methodArea/MethodAreaStructTest.num:I
#3 = Fieldref #56.#57 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #58 // java/lang/StringBuilder
#5 = Methodref #4.#54 // java/lang/StringBuilder."<init>":()V
#6 = String #59 // count =
#7 = Methodref #4.#60 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #4.#61 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#9 = Methodref #4.#62 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Methodref #63.#64 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #65 // java/lang/Exception
#12 = Methodref #11.#66 // java/lang/Exception.printStackTrace:()V
#13 = Class #67 // java/lang/String
#14 = Methodref #17.#68 // methodArea/MethodAreaStructTest.compareTo:(Ljava/lang/String;)I
#15 = String #69 // 测试方法区的内部结构
#16 = Fieldref #17.#70 // methodArea/MethodAreaStructTest.str:Ljava/lang/String;
#17 = Class #71 // methodArea/MethodAreaStructTest
#18 = Class #72 // java/lang/Object
#19 = Class #73 // java/lang/Comparable
#20 = Class #74 // java/io/Serializable
#21 = Utf8 num
#22 = Utf8 I
#23 = Utf8 str
#24 = Utf8 Ljava/lang/String;
#25 = Utf8 count
#26 = Utf8 ConstantValue
#27 = Integer 2
#28 = Utf8 <init>
#29 = Utf8 ()V
#30 = Utf8 Code
#31 = Utf8 LineNumberTable
#32 = Utf8 LocalVariableTable
#33 = Utf8 this
#34 = Utf8 LmethodArea/MethodAreaStructTest;
#35 = Utf8 test1
#36 = Utf8 test2
#37 = Utf8 (II)I
#38 = Utf8 e
#39 = Utf8 Ljava/lang/Exception;
#40 = Utf8 numerator
#41 = Utf8 denominator
#42 = Utf8 result
#43 = Utf8 StackMapTable
#44 = Class #65 // java/lang/Exception
#45 = Utf8 compareTo
#46 = Utf8 (Ljava/lang/String;)I
#47 = Utf8 o
#48 = Utf8 (Ljava/lang/Object;)I
#49 = Utf8 <clinit>
#50 = Utf8 Signature
#51 = Utf8 Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
#52 = Utf8 SourceFile
#53 = Utf8 MethodAreaStructTest.java
#54 = NameAndType #28:#29 // "<init>":()V
#55 = NameAndType #21:#22 // num:I
#56 = Class #75 // java/lang/System
#57 = NameAndType #76:#77 // out:Ljava/io/PrintStream;
#58 = Utf8 java/lang/StringBuilder
#59 = Utf8 count =
#60 = NameAndType #78:#79 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#61 = NameAndType #78:#80 // append:(I)Ljava/lang/StringBuilder;
#62 = NameAndType #81:#82 // toString:()Ljava/lang/String;
#63 = Class #83 // java/io/PrintStream
#64 = NameAndType #84:#85 // println:(Ljava/lang/String;)V
#65 = Utf8 java/lang/Exception
#66 = NameAndType #86:#29 // printStackTrace:()V
#67 = Utf8 java/lang/String
#68 = NameAndType #45:#46 // compareTo:(Ljava/lang/String;)I
#69 = Utf8 测试方法区的内部结构
#70 = NameAndType #23:#24 // str:Ljava/lang/String;
#71 = Utf8 methodArea/MethodAreaStructTest
#72 = Utf8 java/lang/Object
#73 = Utf8 java/lang/Comparable
#74 = Utf8 java/io/Serializable
#75 = Utf8 java/lang/System
#76 = Utf8 out
#77 = Utf8 Ljava/io/PrintStream;
#78 = Utf8 append
#79 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#80 = Utf8 (I)Ljava/lang/StringBuilder;
#81 = Utf8 toString
#82 = Utf8 ()Ljava/lang/String;
#83 = Utf8 java/io/PrintStream
#84 = Utf8 println
#85 = Utf8 (Ljava/lang/String;)V
#86 = Utf8 printStackTrace
1)类型信息
public class MethodAreaStructTest implements Comparable<String>, Serializable {}
public class methodArea.MethodAreaStructTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #17 // methodArea/MethodAreaStructTest
super_class: #18 // java/lang/Object
interfaces: 2, fields: 3, methods: 6, attributes: 2
注意:class文件是静态的,所以反编译看不到classLoader的信息,运行时方法区的类型信息中还会包含classLoader的信息:
- 类的类型信息中,还会记录哪个类加载器加载了这个类。
- 同时,类加载器的类也会加载到方法区,其类型信息中,也会记录这个加载器加载了哪些类。
2)域信息
public int num = 10;
private static String str = "测试方法区的内部结构";
private static final int count = 2;
public int num;
descriptor: I
flags: (0x0001) ACC_PUBLIC
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
private static final int count;
descriptor: I
flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: int 2
3)方法信息 - 构造器
代码中没有单独给出构造器,这里是默认的空参构造
public methodArea.MethodAreaStructTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 5: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this LmethodArea/MethodAreaStructTest;
4)方法信息 - 实例方法
public void test1() {
int count = 20;
System.out.println("count = " + count);
}
public void test1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: bipush 20
2: istore_1
3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: ldc #6 // String count =
15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: iload_1
19: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this LmethodArea/MethodAreaStructTest;
3 26 1 count I
descriptor: ()V
表示方法返回值类型为 voidflags: ACC_PUBLIC
表示方法权限修饰符为 publicstack=3
表示操作数栈深度为 3locals=2
表示局部变量个数为 2 个(实例方法包含 this)args_size=1
表示方法参数个数为 1 个test1()
方法虽然没有参数,但是其 args_size=1 ,这是因为非静态方法默认将 this 作为了参数
5)方法信息 - 静态方法
public static int test2(int numerator, int denominator) {
int result = 0;
try {
result = numerator / denominator;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public static int test2(int, int);
descriptor: (II)I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=2
0: iconst_0
1: istore_2
2: iload_0
3: iload_1
4: idiv
5: istore_2
6: goto 14
9: astore_3
10: aload_3
11: invokevirtual #12 // Method java/lang/Exception.printStackTrace:()V
14: iload_2
15: ireturn
Exception table:
from to target type
2 6 9 Class java/lang/Exception
LineNumberTable:
line 16: 0
line 18: 2
line 21: 6
line 19: 9
line 20: 10
line 22: 14
LocalVariableTable:
Start Length Slot Name Signature
10 4 3 e Ljava/lang/Exception;
0 16 0 numerator I
0 16 1 denominator I
2 14 2 result I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ int, int, int ]
stack = [ class java/lang/Exception ]
frame_type = 4 /* same */
args_size=2
表示方法参数个数为 2 个- 和实例方法不同的是,静态方法不会有 this 占一个参数个数
Exception table
就是异常表- 2、6、9 对应的是字节码的行数,可以通过
LineNumberTable
查看对应代码的行数
- 2、6、9 对应的是字节码的行数,可以通过
6、类变量分析
1)non-final static
- 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
- 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
public class MethodAreaTest {
public static void main(String[] args) {
Order order = null;
order.hello(); // 这里不会抛NPE
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!");
}
}
hello!
1
可以看到这里没有出现NPE,表明了 static 类型的字段和方法随着类的加载而加载,并不属于特定的类实例
2)final static
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
class Order {
public static int count = 1;
public static final int number = 2;
}
public static int count;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static final int number;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 2 // 可以发现final修饰的static变量在编译的时候就已经确定值了
三、方法区的结构演进
1、方法区的演进过程
首先明确:只有 Hotspot 才有永久代。对于 BEA JRockit、IBMJ9 等虚拟机来说,是不存在永久代的概念的。
Hotspot 中方法区的演进:
- jdk7 及以前,习惯上把方法区,称为永久代。
- jdk8 开始,使用元空间取代了永久代。
下面用几张图说明 Hotspot 中方法区的演进:
-
主要的区别一个在于
永久代 --> 元空间
;还有一个在于静态变量
和字符串常量池
的位置变化注意:这里的静态变量指的是静态引用,静态引用对应的对象实体始终都存在堆空间中的。(不理解的话看第3小节)
2、字符串常量池的位置
JDK1.6 及之前,StringTable放在永久代;JDK1.7及之后,StringTable放在堆空间
字符串常量池 StringTable 为什么要调整位置?
因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。
这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。
放到堆里,能及时回收这块的内存。
3、静态变量放在哪里
- 静态引用对应的对象实体(也就是下面的
new byte[1024 * 1024 * 100]
)始终都存在堆空间, - 只是那个变量(也就是下面的
arr
)在JDK6、JDK7、JDK8存放位置中有所变化
1)对象实体在哪里放着?
public class StaticFieldTest {
private static byte[] arr = new byte[1024 * 1024 * 100]; // 100MB
public static void main(String[] args) {
System.out.println(StaticFieldTest.arr);
}
}
JDK6 & JDK7:-Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
JDK8:-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
2)变量本身存放在哪里?
/**
* 《深入理解Java虚拟机》中的案例:
* staticObj、instanceObj、localObj存放在哪里?
*/
public class StaticObjTest {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
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();
}
}
JDK6及之前:
staticObj
随着Test的类型信息存放在方法区
instanceObj
随着Test的对象实例存放在Java堆
localObject
则是存放在foo()方法栈帧的局部变量表
中。
JDK7及之后:
- 选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中。
用JHSDB工具来进行分析(JDK9开始自带,在JDK9的bin目录下可以找到,JDK9以前没有)
- 0x00007f32c7800000(Eden区的起始地址)~ 0x00007f32c7b50000(Eden区的终止地址)
- 可以发现三个变量都在这个范围内
四、方法区的垃圾回收
有些人认为方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。
《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 ZGC 收集器就不支持类卸载)。
一般来说,这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量 和 不再使用的类型。
HotSpot 虚拟机对常量池的回收策略是很明确的:只要常量池中的常量没有被任何地方引用,就可以被回收。
而要判定一个类型是否属于“不再使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类所有的实例都已经被回收。也就是 J a v a 堆中不存在该类及其任何派生子类的实例。 \color{red}{该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。} 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收。除非是经过精心设计的可替换类加载器的场景,如 O S G i 、 J S P 的重加载等,否则通常是很难达成的。 \color{red}{加载该类的类加载器已经被回收。除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。} 加载该类的类加载器已经被回收。除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的 j a v a . l a n g . C l a s s 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 \color{red}{该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。} 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
Java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。
# 对类加载器的引用
jvm必须知道一个类型是由什么类型的类加载器加载的。
如果一个类型是由<用户类加载器>加载的,jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。
jvm在动态链接的时候需要这个信息。
当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的。
这对jvm区分命名空间也是至关重要的。
# 对Class类的引用
jvm必须以某种方式把 Class实例 和存储在方法区中的 Class类型信息 联系起来。
jvm会为每个加载的类型(包括类和接口)都创建一个java.lang.Class的实例。(在堆中)
jvm需要获取堆中的Class对象,然后通过 Class对象 获得 相应类型存储在方法区中的Class类型信息。
关于是否要对类型进行回收,HotSpot 虚拟机提供了-Xnoclassgc
参数进行控制,还可以使用-verbose:class
以及 -XX:+TraceClassLoading
、-XX:+TraceClassUnLoading
查看类加载和卸载信息
在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 以及 OSGi 这类频繁自定义类加载器的场景中,通常都需要 Java 虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
五、综合案例分析
下面我们通过一个案例来分析一下程序计数器、虚拟机栈、方法区的执行流程
package methodArea;
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);
}
}
{
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: return
LineNumberTable:
line 5: 0
line 6: 4
line 7: 7
line 8: 11
line 9: 15
line 10: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 args [Ljava/lang/String;
4 22 1 x I
7 19 2 y I
11 15 3 a I
15 11 4 b I
}
六、运行时数据区小结
从线程共享与否的角度来看运行时数据区