JVM(七)方法区
1 方法区
- 方法区和Java堆一样,是各个线程共享的内存区域,用于存储编译后的字节码中的类的机构信息,如运行时常量池、属性方法数据以及方法、构造器的字节码
- 方法区在JVM启动的时候被创建,并且它的实际物理内存空间和Java堆区一样都是可以不连续的;大小可以选择固定大小或可扩展
- 方法区的大小决定系统可以保存多少个类,如果保存的类太多就会导致方法区溢出,虚拟机抛出内存溢出错误:
java.lang.OutOfMemoryError:PermGen
(jdk7 之前)或者java.lang.OutOfMemoryError:MetaSpace
(之后)- 加载大量Jar包的场景:TomCat部署的工程过多、大量动态地生成反射类
- 关闭JVM就会释放这个区域的内存
- 方法区不等价于永久代,只是在HotSpot中是这样的,元空间和永久代类似,都是JVM规范中方法区的实现,最大的区别在于永久代是在虚拟机设置的内存中,而元空间是使用本地内存(更不容易出现OOM);元空间和永久代的内存结构也不同
2 堆、栈和方法区的交互关系
从线程能否共享的角度来看:
- 堆和元空间(方法区的具体实现)属于是线程共享的,会发生GC(较多收集新生代,较少发生在老年代,基本不动元空间、永久代),也会出现内存溢出
- 虚拟机栈和本地方法栈以及程序计数器都是线程私有的,虚拟机栈和本地方法栈都属于是栈结构不存在GC,但会出现栈溢出异常;程序计数器不存在GC也不会出现异常
三个之间的关系:
-
虚拟机栈栈帧中的本地变量表的slot槽存储引用变量,指向java堆中new的对象实例
-
java堆中new的对象实例中又存储了指向对象类型数据的指针
-
对象类型数据在类加载的时候存储在方法区中
int等32位占1个slot,double等64位占两个
3 方法区大小设置
jdk7及以前
-XX:PermSize
:设置永久代初始分配空间。默认值20.75M-XX:MaxPermSize
:永久代最大可分配空间,32位机器默认是64M,64位机器模式是82M- 当JVM类加载信息大小超过
MaxPermSize
就会报java.lang.OutOfMemoryError:PermGen
jdk7及以后
-XX:MetaspaceSize
和-XX:MaxMetasapceSize
- 默认值依赖于平台,windows下
-XX:MetaspaceSize
是21M,-XX:MaxMetasapceSize
的值是-1,表示没有限制 - 与永久代不同,不过不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出
OutOfMemoryError:MetaSpace
MetaspaceSize
被称作是高水平线
,一旦触及到这个高水平线
,Full GC就会被触发并卸载没有用的类(这些类对应的类加载器不再存活),然后这个高水平线就会被重置,大小取决于GC后释放了多少元空间。如果释放的空间不足,那么就在不超过MaxMetasapceSize
的前提下适当提高该值,Full GC释放较多则降低该值。高水平线
较低会导致频繁GC,所以应该设置-XX:MetaspaceSize
为一个较高的值
C:\Users\admin>jps
15264 Launcher
2400
28392 Jps
9560 MetaSpaceTest
C:\Users\admin>jinfo -flag MetaspaceSize 9560
-XX:MetaspaceSize=104857600
4 PermGen和MetaSpce 的OOM问题
public class MetaSpaceTest extends ClassLoader {
public static void main(String[] args) {
int sum = 0;
MetaSpaceTest test = new MetaSpaceTest();
try {
for (int i = 0; i < 10000; i++) {
ClassWriter classWriter = new ClassWriter(0);
// 指明版本号、修饰符、类名、包名、父类、接口
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "class" + i, null, "java/lang/Object", null);
// 返回字节码文件
byte[] code = classWriter.toByteArray();
// 类的加载
test.defineClass("class" + i, code, 0, code.length);
sum++;
}
} finally {
System.out.println(sum);
}
}
}
设置方法区大小:
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
运行出现OOM:
3331
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at com.hikaru.java.MetaSpaceTest.main(MetaSpaceTest.java:18)
Process finished with exit code 1
如何解决这些OOM问题?
- 解决OOM异常或者Heap Space异常,一般的手段首先是通过内存镜像分析工具(如
JvisualVM
、Jprofile
、Eclipse Memory Analyzer
)对dumpy出来的堆转储快照进行分析,重点是确认内存的对象是否是必要的,也就是先分清楚是出现了内存泄露
还是内存溢出
- 如果是内存泄露,可以进一步使用相关工具查看泄露对象到GC Roots的引用链,于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握泄露对象的类型信息以及GC Roots引用链信息就能够定位到该泄露代码的位置了
- 如果不存在内存泄露,就应该检查虚拟机参数(-Xms 、-Xmx)与机器物理内存对比查看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期间的内存消耗
5 方法区的内部结构
方法区用于存储已经被虚拟机加载的类型信息、域信息、方法信息、常量、静态变量、即时编译器JIT
编译后的代码缓存等
5.1 类型信息、域信息和方法信息
-
其中
类型信息
主要是指对每个加载的类型(类型包含类class、接口interface、注解annotation以及枚举enum),JVM必须在方法区中存储它们的以下类型信息,包括:-
这个类型的全类名(全类名=包名.类名)
-
这个类型的直接父类的全类名(对于interface或是java.lang.Object没有父类)
-
这个类型的修饰符(public private final)
-
这个类型直接接口的一个有序列表
Classfile /E:/Program_workspace/JavaProject/JVM_demo/out/production/chapter05/com/hikaru/java/MethodInnerStructTest.class Last modified 2023-5-30; size 1627 bytes MD5 checksum 4e240dfbd5c61d2d4b9770bad2cc2e09 Compiled from "MethodInnerStructTest.java" // 类型信息 public class com.hikaru.java.MethodInnerStructTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable ...
-
-
域信息
:JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序,包括-
域名称、域类型、域修饰符(public private protected static final volatile transient)
... // 域信息 public int num; descriptor: I flags: ACC_PUBLIC private static java.lang.String str; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC ...
-
-
方法信息
:JVM必须在方法区中保存类型的所有方法的相关信息以及方法的声明顺序,包括-
方法名称
-
方法的返回类型
-
方法参数的数量和类型(按顺序)
-
方法的修饰符(public private protected static final synchronized native abstract)
-
方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
-
异常表(abstract和native方法除外)
- 每个异常处理的开始和结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
// 构造器 对应<init> public com.hikaru.java.MethodInnerStructTest(); descriptor: ()V flags: 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 6: 4 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/hikaru/java/MethodInnerStructTest; public void test1(); descriptor: ()V flags: ACC_PUBLIC // 方法字节码 Code: // 操作数栈深度、局部变量表变量数、参数个数(1表示非静态方法的this) 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 10: 0 line 11: 3 line 12: 28 LocalVariableTable: Start Length Slot Name Signature 0 29 0 this Lcom/hikaru/java/MethodInnerStructTest; 3 26 1 count I public static int test2(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: bipush 30 4: istore_2 5: iload_2 6: iload_0 7: idiv 8: istore_1 9: goto 17 12: astore_2 13: aload_2 14: invokevirtual #12 // Method java/lang/Exception.printStackTrace:()V 17: iload_1 18: ireturn // 异常表 Exception table: from to target type 2 9 12 Class java/lang/Exception LineNumberTable: line 15: 0 line 17: 2 line 18: 5 line 21: 9 line 19: 12 line 20: 13 line 22: 17 LocalVariableTable: Start Length Slot Name Signature 5 4 2 value I 13 4 2 e Ljava/lang/Exception; 0 19 0 cal I 2 17 1 result I StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 12 locals = [ int, int ] stack = [ class java/lang/Exception ] frame_type = 4 /* same */
-
-
non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分
- 类变量被所有的类实例所共享,即使没有类变量也可以访问
-
全局类常量:static final
-
被声明static final的全局类常量在编译的时候就被分配了,对比上面的普通类变量是在类加载的链接阶段的准备步骤才被分配
public class MethodSpaceTest1 { public static int num1 = 1; public static final int num2 = 2; } // javap 编译 ... public static int num1; descriptor: I flags: ACC_PUBLIC, ACC_STATIC public static final int num2; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 2 ...
-
.java结尾的源代码经过前端编译器编译后生成.class字节码文件,然后字节码文件的类信息由类加载器加载到方法区中,创建对象的时候需要在堆中进行,调用方法的时候需要在虚拟机栈进行分配栈帧,整个执行过程还需要用到程序计数器来记录每个线程执行到哪一行的字节码指令。
上面贴的代码是对下面的测试类的class文件
进行javap
反编译结果,-v表示输出附加信息,-p表示显示所有的类和成员(private):
javap -v -p MethodInnerStructTest.class > test.txt
public class MethodInnerStructTest implements Comparable<String>, Serializable {
public int num = 10;
private static String str = "测试方法的内部结构";
public void test1() {
int count = 20;
System.out.println("count = " + count);
}
public static int test2(int cal) {
int result = 0;
try {
int value = 30;
result = value / cal;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
@Override
public int compareTo(String o) {
return 0;
}
}
5.2 常量池、运行时常量池
- 字节码文件中包含了
常量池表
,包括编译期间
生成的各种字面量
和对类型
、域
和方法
的符号引用 - 虚拟机会根据
常量池表
找到要执行的类名、方法名、参数类型、字面量等类型 常量池表
的内容将在加载之后存放到方法区的运行时常量池
中(方法区内部包含了运行时常量池
,是方法区的一部分)- 在加载类或者接口到虚拟机之后,就会创建对应的运行时常量池,因此JVM是为每个加载的类型都维护一个常量池,池中的数据就像数组项一样(下图),可以通过索引访问
- 运行时常量池中既包含字节码常量池的数值字面量,还包含运行期间解析后才能获得的方法或者字段引用;并且经过
类加载的链接的解析阶段
会将这些符号引用转换为真实地址的直接引用 - 当JVM为加载的类型创建运行时常量池的时候,如果大小超过了方法区所提供的的最大值,则JVM会抛出
OutOfMemoryError
如下图,域信息和方法信息中的字节码指令的#都表示指向常量池的常量的符号引用
为什么需要常量池?
- 一个java源文件中的类、接口等类型编译后产生到一个字节码文件中,而如果把所需的数据直接放到字节码文件中就太大了,因此使用常量池存储这些数据,字节码中只包含指向常量池的引用。并且在动态链接的时候会用到运行时常量池
6 方法区在jdk6 7 8中的演进细节
-
只有
HotSpot
虚拟机存在过永久代的概念,JRockit
和IBM J9
不存在 -
HotSpot
虚拟机中方法区的变化:-
jdk1.6及之前:有永久代,
静态变量
存在于永久代(permanent generation)
上 -
jdk1.7:有永久代,但是
字符串常量池
、静态变量
移除,保存在堆里面 -
jdk1.8:无永久代,
类型信息
、字段(域信息)
、方法
、常量(final修饰)
保存在本地内存的元空间里面,但字符串常量池
、静态变量
仍在堆中jdk8及之后方法区基于元空间实现,不再使用虚拟机内存,因此元空间的最大可分配空间就是系统可用内存空间
-
上面放入堆的静态变量到底是什么?
private static byte[] arr = new byte[1024 * 1024 * 100];
- 静态变量不是赋值符号右边的new出来的字节数组,因为new出来的对象都是在堆里面(当然也有经过逃逸分析进行标量替换到栈分配的)
- 而是左边的这个
arr
,这个才是真正的静态变量!它在jdk7之后和类型的映射Class对象
一起被分配到堆里面了
为什么要用永久代替换元空间?
- 为永久代设置空间大小是很难确定的:在某些场景下,需要不断动态地加载很多类,这样就容易产生永久代的OOM Error,而如果使用元空间,则最大可分配空间就是系统可用内存空间,发生错误的概率就会小很多
- 永久代调优是很困难的:
full GC
需要判断常量池中的常量
和类元信息
是否可用进行垃圾回收,非常耗费时间,则使用本地内存就不会出现这种情况
String Table为什么要移动到堆中?
jdk7将String Table
放入了堆中,这是因为
- 永久代只有在Full GC的时候才会进行垃圾回收,而Full GC则是在永久代和老年代空间不足的时候触发
- 这样就导致
String Table
的回收效率不高,但是一般在开发的时候都会创建大量的字符串,这样就会使永久代的空间很容易不足,因此将String Table
放到堆里面,就能够即使回收内存
7 方法区的垃圾回收
- 方法区的垃圾回收主要回收两部分内容:
常量池中废弃的常量
和不再使用的类型
- 而
常量池
中主要存储两大类常量:字面量
:即java语言常量,如文本字符串、被声明为final的常量值符号引用
:包括 1 类和接口的全限定名 2 字段的名称和描述符 3 方法的名称和描述符
HotSpot虚拟机
只要常量池中的常量没有被其他地方引用,就可以回收,和回收堆中对象类似- 而判断类型是否应该被回收,需要同时满足下面三点条件:
- 该类的所有实例都已经被回收,也就是Java堆中不存在该类及其派生类的实例
- 加载改类的类加载器已经被回收,一般很难达成
- 改类对应的
java.lang.Class
对象没有再任何地方被引用,以保证无法在任何地方通过反射
访问该类的方法
- 即使满足上面的三点条件,也仅仅是
允许
进行垃圾回收