一. 方法区,堆,虚拟机栈的合作关系
为了解析方法区,堆,虚拟机栈的合作关系,我写了一个非常简单的测试代码,如下
public static void main(String[] args) {
Person person = new Person();
}
public class Person {
}
Person person = new Person();
这一语句可以分为3个部分来解释,Person,person,new Person()。
- Person:对象的类型数据,加载到方法区
- person:对象的引用,存储在虚拟机栈的栈帧的局部变量表中
- new Person():创建实例对象,存储在堆区
这也就是三者的合作关系,在运行时使用person离不开这三个区域的合作。其关系如下图(常量池信息可先不关注)
二. 类是如何加载到方法区,方法区的存储结构
类加载到方法区的过程参见图解JVM(一)—— 概述&类加载子系统。在类加载过程中,其实并没有提及其存储结构,是因为会放在方法区中学习此相关内容。
方法区:存储虚拟机加载类型信息,常量,静态变量,即时编译的代码缓存。
即时编译的代码缓存不是我们关注的重点,所以有这个印象即可。其次,我们必须首先有一个整体结构的印象,如前面的图:
方法区中保存着众多的类型信息,每个类型信息都对应一个运行时常量池,所有的运行时常量池共享一个字符串常量池
在JDK1.7及之后,字符串常量池和类的静态变量实例都是存储在堆区的,而在逻辑上仍属于方法区
接下来我们看类型信息的细节,如下图所示。
此外还有一些细节:
- 类信息包括class,enum,annotation,interface;
- 域指的是变量,域修饰符:public,private,protected,static,final,volatile,transient
- 方法的字节码,操作数栈,局部变量表及大小:操作数栈和局部变量表之前学习栈的时候都知道,存储在虚拟机栈中。这里的操作数栈和局部变量表指的是编译后的class文件中的操作数栈和局部变量表的固定信息,并不是指实际运行过程中的操作数栈和局部变量表。
- 异常表指的是方法中catch的异常列表,并不是抛出的异常
通过代码来看下上面说的比较抽象的东西
public class TestMethodAreaV2 extends Test implements Cloneable, Comparable<Integer> {
private String string = "testMethodArea";
public int integer = 5;
public Long aLong = 100L;
public void test(int num, int num2, String string) throws InterruptedException,
IndexOutOfBoundsException {
int a = 3;
int c = a + 4;
Thread.sleep(1000);
try {
System.out.print("1111");
} catch (NumberFormatException e) {
System.out.print("NumberFormatException");
}
try {
System.out.print("1111");
} catch (ArithmeticException e) {
System.out.print("ArithmeticException");
}
}
@Override
public int compareTo(Integer o) {
return 0;
}
}
通过javap -v -p TestMethodAreaV2.class(-p:显示私有属性的字节码)。对应信息都在注释中标注了。为了看的更清楚,去掉了部分信息,实际反编译信息比这个更多。
// 类信息= 类的修饰符 + 类的完整有效名称 + 直接父类的完整有效名称 + 类的直接接口列表
public class com.yu.other.TestMethodAreaV2 extends com.yu.other.Test implements java.lang.Cloneable, java.lang.Comparable<java.lang.Integer>
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
{
//域信息=域修饰符 + 域类型 + 域名称
private java.lang.String string;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
public int integer;
descriptor: I
flags: ACC_PUBLIC
public java.lang.Long aLong;
descriptor: Ljava/lang/Long;
flags: ACC_PUBLIC
//方法信息=方法修饰符 + 返回类型(void也算)+ 方法名称 + 参数类型
public void test(int, int, java.lang.String) throws java.lang.InterruptedException, java.lang.IndexOutOfBoundsException;
descriptor: (IILjava/lang/String;)V
flags: ACC_PUBLIC
Code:
//操作数栈大小,局部变量表大小
stack=2, locals=7, args_size=4 //args_size参数个数,包括了this。
//字节码
0: iconst_3
1: istore 4
3: iload 4
5: iconst_4
6: iadd
7: istore 5
9: ldc2_w #9 // long 1000l
12: invokestatic #11 // Method java/lang/Thread.sleep:(J)V
15: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc #13 // String 1111
20: invokevirtual #14 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
23: goto 36
26: astore 6
28: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #16 // String NumberFormatException
33: invokevirtual #14 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
36: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #13 // String 1111
41: invokevirtual #14 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
44: goto 57
47: astore 6
49: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
52: ldc #18 // String ArithmeticException
54: invokevirtual #14 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
57: return
//异常表
Exception table:
from to target type
15 23 26 Class java/lang/NumberFormatException
36 44 47 Class java/lang/ArithmeticException
LineNumberTable:
line 18: 0
line 19: 3
line 20: 9
line 22: 15
line 25: 23
line 23: 26
line 24: 28
line 28: 36
line 31: 44
line 29: 47
line 30: 49
line 32: 57
//局部变量表
LocalVariableTable:
Start Length Slot Name Signature
28 8 6 e Ljava/lang/NumberFormatException;
49 8 6 e Ljava/lang/ArithmeticException;
0 58 0 this Lcom/yu/other/TestMethodAreaV2;
0 58 1 num I
0 58 2 num2 I
0 58 3 string Ljava/lang/String;
3 55 4 a I
9 49 5 c I
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 26
locals = [ class com/yu/other/TestMethodAreaV2, int, int, class java/lang/String, int, int ]
stack = [ class java/lang/NumberFormatException ]
frame_type = 9 /* same */
frame_type = 74 /* same_locals_1_stack_item */
stack = [ class java/lang/ArithmeticException ]
frame_type = 9 /* same */
Exceptions:
throws java.lang.InterruptedException, java.lang.IndexOutOfBoundsException
}
接下来,我们再来看看方法区剩下的静态变量和常量信息,在看这个之前,我们先了解一下HotSpot中方法区的演变过程。
三. HotSpot方法区的演变
方法区,永久代和元空间的关系
为什么说是HotSpot方法区的演变,而不是方法区的演变呢?是因为方法区的变化不大,变化的是HotSpot对方法区实现的变化。怎么理解呢?JVM有一套统一的规范,Oracle根据JVM规范实创建了JVM程序叫做HotSpot,此外还有其他公司实现了,比如J9,JRockit(三者是目前应用范围最多的三款虚拟机)。
演进过程
永久代和元空间是HotSpot在不同版本上对方法区的不同实现,举例来说:方法区是MethodInterface, PermGen和MetaSpace是MethodInterface的两个实现,所以才有了这个演进过程。
版本 | 对方法区的实现 | |
---|---|---|
1.6及以前 | 永久代,静态变量存储在永久代 | |
1.7 | 永久代,静态变量和字符串常量池存储在堆中 | |
1.8及以后 | 无永久代,改为元空间,静态变量和字符串常量池存储在堆中 |
为了对方法区有更进一步的认识,可以参考下图:乍看起来,在JDK1.7和JDk1.8时,似乎除了换了个名字,并没有什么区别。实则不然,元空间使用的是本地内存,而不是JVM分配的内存,这个会在后面说方法区的GC时再详细解释。
四. 运行时常量池和静态变量
运行时常量池
接下来,我们开始学习方法区的剩余两个内容:运行时常量池和静态变量。从上一个问题可知,从1.7及以后,静态变量和字符串常量池都已经放到堆上了,但在逻辑上其仍属于方法区。我们来看JVM规范对运行时常量池的解释
2.5.5. Run-Time Constant Pool
A run-time constant pool is a per-class or per-interface run-time representation of the
constant_pool
table in aclass
file (§4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time. The run-time constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table.Each run-time constant pool is allocated from the Java Virtual Machine’s method area (§2.5.4). The run-time constant pool for a class or interface is constructed when the class or interface is created (§5.3) by the Java Virtual Machine.
翻译过来:运行时常量池是每一个类或接口编译成的class文件中的constant_pool的表示,它包括在编译时期知道的数字常量(字面量),在运行时期必须解析的方法和域的引用(符号引用)。我们来看下之前测试代码的常量池信息:
Constant pool:
#1 = Methodref #22.#64 // com/yu/other/Test."<init>":()V
#2 = String #65 // testMethodArea
#3 = Fieldref #21.#66 // com/yu/other/TestMethodAreaV2.string:Ljava/lang/String;
#4 = Fieldref #21.#67 // com/yu/other/TestMethodAreaV2.integer:I
#5 = Long 100l
#7 = Methodref #68.#69 // java/lang/Long.valueOf:(J)Ljava/lang/Long;
#8 = Fieldref #21.#70 // com/yu/other/TestMethodAreaV2.aLong:Ljava/lang/Long;
#9 = Long 10000l
#11 = Methodref #71.#72 // java/lang/Thread.sleep:(J)V
#12 = Fieldref #73.#74 // java/lang/System.out:Ljava/io/PrintStream;
#13 = String #75 // 1111
#14 = Methodref #76.#77 // java/io/PrintStream.print:(Ljava/lang/String;)V
#15 = Class #78 // java/lang/NumberFormatException
#16 = String #79 // NumberFormatException
#17 = Class #80 // java/lang/ArithmeticException
#18 = String #81 // ArithmeticException
#19 = Class #82 // java/lang/Integer
#20 = Methodref #21.#83 // com/yu/other/TestMethodAreaV2.compareTo:(Ljava/lang/Integer;)I
#21 = Class #84 // com/yu/other/TestMethodAreaV2
#22 = Class #85 // com/yu/other/Test
#23 = Class #86 // java/lang/Cloneable
#24 = Class #87 // java/lang/Comparable
#25 = Utf8 string
#26 = Utf8 Ljava/lang/String;
#27 = Utf8 integer
#28 = Utf8 I
#29 = Utf8 aLong
#30 = Utf8 Ljava/lang/Long;
#31 = Utf8 <init>
#32 = Utf8 ()V
#33 = Utf8 Code
#34 = Utf8 LineNumberTable
#35 = Utf8 LocalVariableTable
#36 = Utf8 this
#37 = Utf8 Lcom/yu/other/TestMethodAreaV2;
#38 = Utf8 test
#39 = Utf8 (IILjava/lang/String;)V
#40 = Utf8 e
#41 = Utf8 Ljava/lang/NumberFormatException;
#42 = Utf8 Ljava/lang/ArithmeticException;
#43 = Utf8 num
#44 = Utf8 num2
#45 = Utf8 a
#46 = Utf8 c
#47 = Utf8 StackMapTable
#48 = Class #84 // com/yu/other/TestMethodAreaV2
#49 = Class #88 // java/lang/String
#50 = Class #78 // java/lang/NumberFormatException
#51 = Class #80 // java/lang/ArithmeticException
#52 = Utf8 Exceptions
#53 = Class #89 // java/lang/InterruptedException
#54 = Class #90 // java/lang/IndexOutOfBoundsException
#55 = Utf8 compareTo
#56 = Utf8 (Ljava/lang/Integer;)I
#57 = Utf8 o
#58 = Utf8 Ljava/lang/Integer;
#59 = Utf8 (Ljava/lang/Object;)I
#60 = Utf8 Signature
#61 = Utf8 Lcom/yu/other/Test;Ljava/lang/Cloneable;Ljava/lang/Comparable<Ljava/lang/Integer;>;
#62 = Utf8 SourceFile
#63 = Utf8 TestMethodAreaV2.java
#64 = NameAndType #31:#32 // "<init>":()V
#65 = Utf8 testMethodArea
#66 = NameAndType #25:#26 // string:Ljava/lang/String;
#67 = NameAndType #27:#28 // integer:I
#68 = Class #91 // java/lang/Long
#69 = NameAndType #92:#93 // valueOf:(J)Ljava/lang/Long;
#70 = NameAndType #29:#30 // aLong:Ljava/lang/Long;
#71 = Class #94 // java/lang/Thread
#72 = NameAndType #95:#96 // sleep:(J)V
#73 = Class #97 // java/lang/System
#74 = NameAndType #98:#99 // out:Ljava/io/PrintStream;
#75 = Utf8 1111
#76 = Class #100 // java/io/PrintStream
#77 = NameAndType #101:#102 // print:(Ljava/lang/String;)V
#78 = Utf8 java/lang/NumberFormatException
#79 = Utf8 NumberFormatException
#80 = Utf8 java/lang/ArithmeticException
#81 = Utf8 ArithmeticException
#82 = Utf8 java/lang/Integer
#83 = NameAndType #55:#56 // compareTo:(Ljava/lang/Integer;)I
#84 = Utf8 com/yu/other/TestMethodAreaV2
#85 = Utf8 com/yu/other/Test
#86 = Utf8 java/lang/Cloneable
#87 = Utf8 java/lang/Comparable
#88 = Utf8 java/lang/String
#89 = Utf8 java/lang/InterruptedException
#90 = Utf8 java/lang/IndexOutOfBoundsException
#91 = Utf8 java/lang/Long
#92 = Utf8 valueOf
#93 = Utf8 (J)Ljava/lang/Long;
#94 = Utf8 java/lang/Thread
#95 = Utf8 sleep
#96 = Utf8 (J)V
#97 = Utf8 java/lang/System
#98 = Utf8 out
#99 = Utf8 Ljava/io/PrintStream;
#100 = Utf8 java/io/PrintStream
#101 = Utf8 print
#102 = Utf8 (Ljava/lang/String;)V
虽然Java代码很简单,但是可以看到常量池中有很多数据。根据前面JVM的解释,我们划分为字面量和符号引用。我们先来理解一下字面量(开始一直弄不清楚),测试类中有下面两个变量:
//integer是类的变量,是一个域引用, 5就是字面量
public int integer = 5;
//aLong是类的变量,是一个域引用, 100L就是字面量
public Long aLong = 100L;
#4 = Fieldref #21.#67 // com/yu/other/TestMethodAreaV2.integer:I
#5 = Long 100l
#7 = Methodref #68.#69 // java/lang/Long.valueOf:(J)Ljava/lang/Long;
#8 = Fieldref #21.#70 // com/yu/other/TestMethodAreaV2.aLong:Ljava/lang/Long;
我把对应常量池的信息copy过来,也放在上面了, #4 就是integer的符号引用的索引,#8就是aLong的符号引用的索引。#5表示的就是100L这个字面量,我们会发现好像没有integer的5这个值。基本数据类型中除了long,double,其余都是直接在类的构造器方法方法中直接赋值,而没有存储在常量池中。为此我在测试代码中新增了几个基本类型做测试。
private String string = "testMethodArea";
public int aInt = 1000;
public boolean aBoolean = true;
public byte aByte = 10;
public long bLong = 101L;
public Long aLong = 100L;
// class文件中的构造函数,在运行时即转为<init>方法。
public com.yu.other.TestMethodAreaV2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/yu/other/Test."<init>":()V
4: aload_0
5: ldc #2 // String testMethodArea
7: putfield #3 // Field string:Ljava/lang/String;
10: aload_0
11: sipush 1000
14: putfield #4 // Field aInt:I
17: aload_0
18: iconst_1
19: putfield #5 // Field aBoolean:Z
22: aload_0
23: bipush 10
25: putfield #6 // Field aByte:B
28: aload_0
29: ldc2_w #7 // long 100l
32: invokestatic #9 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
35: putfield #10 // Field aLong:Ljava/lang/Long;
38: return
LineNumberTable:
line 7: 0
line 9: 4
line 11: 10
line 13: 17
line 15: 22
line 17: 28
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 this Lcom/yu/other/TestMethodAreaV2;
在构造函数中可以看到11: sipush 1000,18: iconst_1(boolean用1,0表示true,false),23: bipush 10都是直接写死的数值,而无论是long声明的101L,或Long声明的100L都是存储在常量池中的。
符号引用就比较简单了,Methodref,Fieldref就是对应的符号引用。会在类加载的Linking步骤中的Resolve将符号引用转为直接引用。
静态变量
学完常量池之后,就来学习静态变量了。静态变量是和类关联的变量,也叫做类变量,其与实例对象无关。对类加载还有印象的话,应该会记得在Linking的Prepare阶段,会为类变量分配内存,并赋默认值,在initialize阶段,执行方法,把类变量赋初始值。
类变量与实例对象无关
我们先来验证一下类变量与实例对象无关这件事,我新建了一个Person类。
public class Person {
public static void main(String[] args) {
Person person = null;
//第一反应,应该是会报空指针
System.out.println(person.age);
person.printAge();
}
public static int age = 15;
public static Integer gender = 1;
public static final long longTest = 100L;
public static final String desc = new String("student");
public static final Integer type = 10;
public static final Order finalOrder = new Order(3);
public String grade;
public int score;
public Order order = new Order(10);
public static void printAge () {
System.out.println(age);
}
}
在main方法中将声明person变量为null,然后打印person.age,第一反应是会报空指针异常的。我们运行下,看结果。
15
15
实则并不会报空指针异常,这也就证明了类变量是与实例变量无关的,在类加载到方法区时,类变量也会同步加载。
静态变量赋值过程
编译器会自动收集类中所有类变量的赋值操作和静态代码块,合并成下方的static方法,转化成方法,在类加载initialize阶段执行,也就对其类变量做了赋值。
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: bipush 15
2: putstatic #6 // Field age:I
5: iconst_1
6: invokestatic #9 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: putstatic #10 // Field gender:Ljava/lang/Integer;
12: new #11 // class java/lang/String
15: dup
16: ldc #12 // String student
18: invokespecial #13 // Method java/lang/String."<init>":(Ljava/lang/String;)V
21: putstatic #14 // Field desc:Ljava/lang/String;
24: bipush 10
26: invokestatic #9 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
29: putstatic #15 // Field type:Ljava/lang/Integer;
32: new #2 // class com/yu/other/Order
35: dup
36: iconst_3
37: invokespecial #3 // Method com/yu/other/Order."<init>":(I)V
40: putstatic #16 // Field finalOrder:Lcom/yu/other/Order;
43: return
这里就能看到
- 无论成员变量或实例变量,基础类型变量都会直接在指令中写死指定的值,而未在常量池中声明(除去long和double),long和double的值是写死在常量池中的。
- 静态变量的对象和数组是存储在堆中的,也就是静态变量区域。
- final修饰的对象的创建和赋值是在中完成,但final修饰的基础类型变量并没有,而是在域信息下带有
ConstantValue
标记,并有指定的值。
public static final int finalInt;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 16
public static final long longTest;
descriptor: J
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: long 100l
字符串常量池
字符串常量池的意义:字符串在java程序中被大量使用,为了避免每次都创建相同的字符串对象及内存分配,JVM对其进行了优化,单独开辟一块区域用于存储字符串实例,所有的字符串都存储在字符串常量池。
存储位置:在JDK1.7及以后,存储在堆区。
理解:字符串常量池和类的常量池以及类变量中的字符串并无关系,所有声明的字符串都会保存在字符串常量池中。留下一个测试代码后面再单独解释。
private static String staticString = "testString";
public String testString = new String("testString");
public static void main(String[] args) {
String fString = "testString";
String pString = "test" + "String";
String string1 = "test" ;
String string2 = "String";
TestString testString = new TestString();
System.out.println(staticString == testString.testString);
System.out.println(staticString.equals(testString.testString));
System.out.println(fString == staticString);
System.out.println(pString == staticString);
System.out.println((string1 + string2) == staticString);
}
false
true
true
true
false
五. 方法区的垃圾回收和OOM
方法区的OOM
- 在JDK1.7及以前设置永久代的大小:-XX:PermSize设置永久代初始大小(默认20.75M),-XX:MaxPermSize设置永久代最大空间(32位机器默认64M,64位机器默认82M)
- 在JDK1.8设置元空间大小:-XX:MetaSpaceSize设置元空间大小(Windows默认是21M),-XX:MaxMetaSpaceSize设置元空间最大空间(默认值是-1,表示不受限制,可使用全部可用系统内存)。
在加载大量的类时,超出永久代或元空间大小时,即会报出OutOfMemoryError:PermGenSpace/MetaSpace。一般来说,元空间不太会出现OOM,因为元空间使用的是全部系统内存,而不是JVM申请的内存。这也是为何永久代转变成元空间。总结原因如下:
- 永久代空间大小很难确定。在某些场景下,会动态加载很多类,就会导致永久代OOM
- 对永久代的调优很难。
字符串常量池和静态变量
字符串常量池和静态变量在JDK1.7及以后就存储在堆区,所以字符串常量池和静态变量的回收是按照堆的GC策略进行GC
。HotSpot在JDK1.7把两者放到堆区的原因:永久代的回收效率很低,只有在full gc(老年代空间不足,永久代空间不足)时才会对永久代进行GC,而Java程序中会生成大量的字符串,无法回收会导致内存不足。
方法区的垃圾回收
方法区的垃圾回收主要是运行时常量池中废弃的常量和不再使用的类型
运行时常量池中废弃的常量
常量包括:字面量和符号引用。只要两者没有被任何地方引用,就可以被回收
不再使用的类型
判断一个类是否不再使用的条件
- 该类的所有实例均被回收。即堆中不存在该类及其派生子类的所有实例
- 加载该类的类加载器被回收(及其难达成)
- 该类对应的java.lang.Class对象没有被引用,即该对象无法通过反射访问
在满足上述三个条件以后,该类型才允许被回收。同时HotSpot提供了参数-Xnoclassgc控制是否对类型信息进行回收。
总结
- 方法区中存储类信息,堆中存储实例对象,方法中定义的引用存储在局部变量表,三者合作紧密。
- 方法区的存储结构:类型信息,运行时常量池,字符串常量池,静态变量,即时编译的代码缓存。类型信息包括类信息,方法信息,域信息。
- HotSpot虚拟机在1.7及以后实现中,字符串常量池,静态变量存储在堆中。
- 运行时常量池和class文件中的常量池是对应的。字符串常量池是保存所有字符串。
- 方法区的垃圾回收效率较低,只有在full gc时才会回收。废弃的常量只需判断是否有引用,而类型的卸载条件严苛。