目录
背景
栈、堆、方法区三者间的交互关系
方法区的理解
设置方法区大小与OOM
方法区的内部结构
方法区使用举例
方法区演进细节
方法区的GC
总结
整理一下关于JVM方法区的学习笔记,基于jdk8,所以方法区的实际实现都是堆中的元空间
三者在运行时数据区的中的分布如下图所示
从是否线程私有的角度上看的关系如下图所示,其中元空间是数据区的实现
三者的交互关系如下图所示
其中的reference就是person变量,对象实例数据就是new Person()创建出来的对象,对象类型数据就是Person类
线程共享,存储每个类的运行时常量池、属性和方法的数据、方法和构造器的字节码、包括特殊方法、类的初始化和接口的初始化
方法区在JVM启动时就创建了,不必实现GC或压缩
jdk8的方法区独立于堆,也没有要求管理字节码的管理策略
和堆一样,大小可以固定也可以自适应,物理地址可以不连续
方法区的大小决定了系统可以保存多少个类,如果定义或加载了太多的类,就会导致OOM
关闭JVM就会释放方法区的内存
dk8及以后,设置参数为-XX:MetaspaceSize和-XX:MaxMetaspaceSize,分别设置元空间大小和最大元空间大小
默认值依赖于平台,windows下,前者默认值为21M,后者值为-1,表示无限制
D:\develop\ideaWorkspace\kafka-demo\target\classes\jvm\heap>jps
20208 RemoteMavenServer36
18148 Launcher
4900
15432 Jps
18696 HeapDemo0
D:\develop\ideaWorkspace\kafka-demo\target\classes\jvm\heap>jinfo -flag MetaspaceSize 18696
-XX:MetaspaceSize=21807104
D:\develop\ideaWorkspace\kafka-demo\target\classes\jvm\heap>jinfo -flag MaxMetaspaceSize 18696
-XX:MaxMetaspaceSize=18446744073709486080
设置时,格式为-XX:MetaspaceSize=100M, -XX:MaxMetaspaceSize=100M
-XX:MetaspaceSize值是一个水位线,当堆空间大小超过这个值,就会触发一个Full GC。Full GC后,如果释放的内存不大,就会提高-XX:MetaspaceSize值;如果释放了很大内存,就会降低-XX:MetaspaceSize值。一般将此值设置得比较高。
方法区的OOM实例,利用以下代码不断创建类,提前设置好元空间大小和最大大小都是100M
package jvm.heap;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
public class HeapDemo0 extends ClassLoader {
public static void main(String[] args) throws InterruptedException {
System.out.println("start..");
int i = 0;
HeapDemo0 demo = new HeapDemo0();
while (true) {
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,
"Class" + i, null, "java/lang/Object", null);
// 创建类,给定jdk版本号、类访问修饰符、类名、包名、父类签名、实现接口
byte[] code = writer.toByteArray();
demo.defineClass("Class" + i, code, 0, code.length);
// 定义类
i++;
}
}
}
运行后,报错如下
start..
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at jvm.heap.HeapDemo0.main(HeapDemo0.java:20)
Process finished with exit code 1
如何解决这些OOM:
1)、要解决元空间或堆空间的oom,一般先使用内存分析工具,对dump出来的堆转储快照进行分析,重点是确定内存中的对象是否是必要的,也就是要分清楚到底是发生了内存泄漏还是内存溢出
2)、如果内存泄漏,可进一步通过工具查看泄露对象到GC Roots的引用链,掌握对象的类型信息,以及引用链的信息,比较准确定位泄漏代码的位置
3)、如果时内存溢出,那就要调大堆空间和元空间大小,或者缩短对象的生命周期
运行时数据区的内部结构如下图所示
详细的方法区内部结构如下图所示,可见方法区存储已经被JVM加载的类型信息、常量、静态变量、JIT编译后的代码缓存等
类型信息:(类class、接口interface、枚举enum、注解annotation),JVM中必须在方法区存储以下类型信息:
1)、类的全限定类名(包名.类名)
2)、直接父类的完整有效名(对于接口或java.lang.Object,都没有父类)
3)、访问修饰符(public、abstract、final等)
4)、实现接口的有序列表
域信息:属性名、属性类型、修饰符(public、static、volatile、transient等)
方法信息:方法名称、返回类型、参数数量和类型(按顺序)、修饰符(public、native等)、字节码、操作数栈、局部变量表及大小(abstract和native方法除外)、异常表(abstract和native方法除外)
以以下代码为例
package jvm.heap.methodArea;
import java.io.Serializable;
public class InnerStructureTest extends Object implements Comparable, Serializable {
private String s = "s";
public static int num = 0;
public static void f() {
}
public void g(int i) {
i++;
int b = 7;
int c = 9;
try {
i = b * c;
} catch (Exception e) {
e.printStackTrace();
}
}
public int compareTo(String o) {
return 0;
}
}
对其字节码如下
D:\develop\ideaWorkspace\kafka-demo\target\classes\jvm\heap\methodArea>javap -v -p InnerStructureTest.class
Classfile /D:/develop/ideaWorkspace/kafka-demo/target/classes/jvm/heap/methodArea/InnerStructureTest.class
Last modified 2020-5-15; size 1150 bytes
MD5 checksum 08e84e5816db3cf034b9439033bec7f8
Compiled from "InnerStructureTest.java"
public class jvm.heap.methodArea.InnerStructureTest extends java.lang.Object implements java.lang.Comparable, java.io.Serializable
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#41 // java/lang/Object."":()V
#2 = String #13 // s
#3 = Fieldref #9.#42 // jvm/heap/methodArea/InnerStructureTest.s:Ljava/lang/String;
#4 = Class #43 // java/lang/Exception
#5 = Methodref #4.#44 // java/lang/Exception.printStackTrace:()V
#6 = Class #45 // java/lang/String
#7 = Methodref #9.#46 // jvm/heap/methodArea/InnerStructureTest.compareTo:(Ljava/lang/String;)I
#8 = Fieldref #9.#47 // jvm/heap/methodArea/InnerStructureTest.num:I
#9 = Class #48 // jvm/heap/methodArea/InnerStructureTest
#10 = Class #49 // java/lang/Object
#11 = Class #50 // java/lang/Comparable
#12 = Class #51 // java/io/Serializable
#13 = Utf8 s
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 num
#16 = Utf8 I
#17 = Utf8
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Ljvm/heap/methodArea/InnerStructureTest;
#24 = Utf8 f
#25 = Utf8 g
#26 = Utf8 (I)V
#27 = Utf8 e
#28 = Utf8 Ljava/lang/Exception;
#29 = Utf8 i
#30 = Utf8 b
#31 = Utf8 c
#32 = Utf8 compareTo
#33 = Utf8 (Ljava/lang/String;)I
#34 = Utf8 o
#35 = Utf8 (Ljava/lang/Object;)I
#36 = Utf8
#37 = Utf8 Signature
#38 = Utf8 Ljava/lang/Object;Ljava/lang/Comparable;Ljava/io/Serializable;
#39 = Utf8 SourceFile
#40 = Utf8 InnerStructureTest.java
#41 = NameAndType #17:#18 // "":()V
#42 = NameAndType #13:#14 // s:Ljava/lang/String;
#43 = Utf8 java/lang/Exception
#44 = NameAndType #52:#18 // printStackTrace:()V
#45 = Utf8 java/lang/String
#46 = NameAndType #32:#33 // compareTo:(Ljava/lang/String;)I
#47 = NameAndType #15:#16 // num:I
#48 = Utf8 jvm/heap/methodArea/InnerStructureTest
#49 = Utf8 java/lang/Object
#50 = Utf8 java/lang/Comparable
#51 = Utf8 java/io/Serializable
#52 = Utf8 printStackTrace
{
private java.lang.String s;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
public static int num;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public jvm.heap.methodArea.InnerStructureTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: ldc #2 // String s
7: putfield #3 // Field s:Ljava/lang/String;
10: return
LineNumberTable:
line 5: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Ljvm/heap/methodArea/InnerStructureTest;
public static void f();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 11: 0
public void g(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=2
0: iinc 1, 1
3: bipush 7
5: istore_2
6: bipush 9
8: istore_3
9: iload_2
10: iload_3
11: imul
12: istore_1
13: goto 23
16: astore 4
18: aload 4
20: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
23: return
Exception table:
from to target type
9 13 16 Class java/lang/Exception
LineNumberTable:
line 14: 0
line 15: 3
line 16: 6
line 18: 9
line 21: 13
line 19: 16
line 20: 18
line 22: 23
LocalVariableTable:
Start Length Slot Name Signature
18 5 4 e Ljava/lang/Exception;
0 24 0 this Ljvm/heap/methodArea/InnerStructureTest;
0 24 1 i I
6 18 2 b I
9 15 3 c I
public int compareTo(java.lang.String);
descriptor: (Ljava/lang/String;)I
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iconst_0
1: ireturn
LineNumberTable:
line 25: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this Ljvm/heap/methodArea/InnerStructureTest;
0 2 1 o Ljava/lang/String;
public int compareTo(java.lang.Object);
descriptor: (Ljava/lang/Object;)I
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #6 // class java/lang/String
5: invokevirtual #7 // Method compareTo:(Ljava/lang/String;)I
8: ireturn
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Ljvm/heap/methodArea/InnerStructureTest;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #8 // Field num:I
4: return
LineNumberTable:
line 7: 0
}
Signature: #38 // Ljava/lang/Object;Ljava/lang/Comparable;Ljava/io/Serializable;
SourceFile: "InnerStructureTest.java"
其中下面内容为类信息
public class jvm.heap.methodArea.InnerStructureTest extends java.lang.Object implements java.lang.Comparable, java.io.Serializable
包括类的全限定类型、直接父类全限定类名、实现的接口的全限定类名以及泛型的全限定类名,对应
public class InnerStructureTest extends Object implements Comparable, Serializable {..}
然后以下为域信息
private java.lang.String s;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
public static int num;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
描述的是类的字段,包括字段访问修饰符、类型全限定名、字段名;
下面的就是方法信息了,比如默认的构造方法
public jvm.heap.methodArea.InnerStructureTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: ldc #2 // String s
7: putfield #3 // Field s:Ljava/lang/String;
10: return
LineNumberTable:
line 5: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Ljvm/heap/methodArea/InnerStructureTest;
指明了方法全限定名、方法描述信息、方法修饰符、字节码、行号表、局部变量表
方法描述信息格式为(参数列表)返回类型,V表示void
字节码第一行分别表示操作数栈最大深度、局部变量表大小、参数数量(非静态方法多一个this),下面才是真正的方法字节码
行号表描述的是字节码行号和代码行号的对应关系
局部变量表描述的是局部变量的作用范围、槽号、名字、类型签名等信息
静态方法f的方法信息
public static void f();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 11: 0
可见参数数量args_size为0
带有异常的方法信息
public void g(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=2
0: iinc 1, 1
3: bipush 7
5: istore_2
6: bipush 9
8: istore_3
9: iload_2
10: iload_3
11: imul
12: istore_1
13: goto 23
16: astore 4
18: aload 4
20: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
23: return
Exception table:
from to target type
9 13 16 Class java/lang/Exception
LineNumberTable:
line 14: 0
line 15: 3
line 16: 6
line 18: 9
line 21: 13
line 19: 16
line 20: 18
line 22: 23
LocalVariableTable:
Start Length Slot Name Signature
18 5 4 e Ljava/lang/Exception;
0 24 0 this Ljvm/heap/methodArea/InnerStructureTest;
0 24 1 i I
6 18 2 b I
9 15 3 c I
方法字节码里有个异常表,表示从字节码9行到字节码13行有异常,异常类型为java/lang/Exception,捕获异常跳到字节码16行
非final类变量
首先静态公有变量或方法可以被对应类型的null对象访问
package jvm.heap.methodArea;
public class InnerStructureTest {
public static void main(String[] args) {
Order o = null;
o.show();
}
}
class Order {
public static int count = 1;
public static final int number = 2;
public static void show() {
System.out.println("....");
}
}
运行结果如下所示
....
Process finished with exit code 0
其中,对Order类的字节码如下所示
D:\develop\ideaWorkspace\kafka-demo\target\classes\jvm\heap\methodArea>javap -v -p Order.class
Classfile /D:/develop/ideaWorkspace/kafka-demo/target/classes/jvm/heap/methodArea/Order.class
Last modified 2020-5-15; size 622 bytes
MD5 checksum 8f3559bda36bdc6bc875d21370632a12
Compiled from "InnerStructureTest.java"
class jvm.heap.methodArea.Order
minor version: 0
major version: 49
flags: ACC_SUPER
Constant pool:
#1 = Methodref #7.#24 // java/lang/Object."":()V
#2 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #27 // ....
#4 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Fieldref #6.#30 // jvm/heap/methodArea/Order.count:I
#6 = Class #31 // jvm/heap/methodArea/Order
#7 = Class #32 // java/lang/Object
#8 = Utf8 count
#9 = Utf8 I
#10 = Utf8 number
#11 = Utf8 ConstantValue
#12 = Integer 2
#13 = Utf8
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Ljvm/heap/methodArea/Order;
#20 = Utf8 show
#21 = Utf8
#22 = Utf8 SourceFile
#23 = Utf8 InnerStructureTest.java
#24 = NameAndType #13:#14 // "":()V
#25 = Class #33 // java/lang/System
#26 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#27 = Utf8 ....
#28 = Class #36 // java/io/PrintStream
#29 = NameAndType #37:#38 // println:(Ljava/lang/String;)V
#30 = NameAndType #8:#9 // count:I
#31 = Utf8 jvm/heap/methodArea/Order
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (Ljava/lang/String;)V
{
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
jvm.heap.methodArea.Order();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 34: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvm/heap/methodArea/Order;
public static void show();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String ....
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 39: 0
line 40: 8
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #5 // Field count:I
4: return
LineNumberTable:
line 35: 0
}
SourceFile: "InnerStructureTest.java"
对于静态属性count和静态最终属性number,域信息如下所示
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
其中number的值为ConstantValue。对于非final的count,可以根据静态字节码块(也就是
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #5 // Field count:I
4: return
LineNumberTable:
line 35: 0
中的如下字节码,为count赋值为1,并设置为静态
0: iconst_1
1: putstatic #5 // Field count:I
运行时常量池与常量区
运行时常量池所在位置如下图所示
常量池,就是字节码中的Constant Pool部分
Constant pool:
#1 = Methodref #7.#24 // java/lang/Object."":()V
#2 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #27 // ....
#4 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Fieldref #6.#30 // jvm/heap/methodArea/Order.count:I
#6 = Class #31 // jvm/heap/methodArea/Order
#7 = Class #32 // java/lang/Object
#8 = Utf8 count
#9 = Utf8 I
#10 = Utf8 number
#11 = Utf8 ConstantValue
#12 = Integer 2
#13 = Utf8
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Ljvm/heap/methodArea/Order;
#20 = Utf8 show
#21 = Utf8
#22 = Utf8 SourceFile
#23 = Utf8 InnerStructureTest.java
#24 = NameAndType #13:#14 // "":()V
#25 = Class #33 // java/lang/System
#26 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#27 = Utf8 ....
#28 = Class #36 // java/io/PrintStream
#29 = NameAndType #37:#38 // println:(Ljava/lang/String;)V
#30 = NameAndType #8:#9 // count:I
#31 = Utf8 jvm/heap/methodArea/Order
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (Ljava/lang/String;)V
运行时常量池就是程序运行时的常量池,把符号地址转换为真实地址,其具备动态性,可以动态加载新的常量到常量池中。
常量池表包含了各种字面量(2、out、....、println等)、类型、属性和方法的符号引用(#36、#37:#38等)
常量池的存在意义:减少字节码文件大小,而且动态链接时会使用运行时常量池,参见我的文章JVM学习笔记上(概述-本地方法栈)里虚拟机栈中相关内容
参加我的文章JVM学习笔记上(概述-本地方法栈)里虚拟机栈的操作数栈部分
在jdk7及以前,习惯上把方法区称为永久代;jdk8开始,使用元空间取代了永久代。其他JVM,比如JRockit和J9里,就没有永久代的概念
当年使用永久代,容易导致程序OOM(超过-XX:MaxPermSize)
元空间和永久代的最大区别在于:元空间不使用虚拟机设置的内存,而是使用本地内存
jdk1.6、1.7和1.8中方法区的变化
jdk6的运行时数据区结构如下图所示
jdk7的运行时数据区如下图所示,把静态常量和StringTable(字面量表)移到了堆中,这是因为永久代回收效率很低,把字面量表移动到堆中,可以提高它的回收效率,及时回收内存
jdk8的运行时数据区如下图所示,把永久代从虚拟机内存移出到本地内存中,命名为元空间
把永久代换成元空间的原因:
1)、这是融合JRockit和HotSpot工作的一部分
2)、为永久代设置的空间大小是很难确定的:如果动态加载类过多,就会出现永久代的OOM;而元空间的大小仅受本地内存限制
3)、对永久代进行调优很困难
以如下代码为例,说明静态对象属性、对象属性和局部对象属性的对象和引用存放在哪里
public class StaticFieldTest {
private static String sTag = "tag";
private String name = "name";
public void f() {
String str = "s";
System.out.println(str);
}
}
三个字符串对象,都在堆里;静态属性sTag,随着Class对象存放在堆中;普通属性name,随着StaticFieldTest对象放到堆里;局部变量str,存放到f方法栈帧中的局部变量表里
方法区可以不实现GC,如果要实现GC,那么主要回收不再使用的类型和常量池中废弃的常量
判断常量是否不再使用:只要在任何地方都没有引用,就可以被回收
判断一个类型是否不再使用,有下面三个条件:
1)、此类所有实例都已经被回收,也就是堆中不存在此类及其子类的实例
2)、加载此类的类加载器已经被回收,此条件一般达不成,除非是精心设计过的可替换类加载器的场景,如OSGi、JSP的重加载等
3)、此类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问此类的方法
满足上面三个条件后,还要根据参数-Xnoclassgc值决定是否回收废弃的类
在大量使用反射、动态代理、CGLib等字节码框架、动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要JVM具备类型卸载的能力,以保证对方法区不会造成过大的内存压力
运行时数据区的内存结构如下所示
下一篇笔记是关于对象实例化的