文章目录
1、栈、堆、方法区的交互关系
运行时数据区结构图
栈、堆、方法区的交互关系
- 将
Person
类的结构信息加载进方法区 person
是一个变量,表示堆空间对象实体的引用,保存在栈中,再具体点就是保存在某一栈帧中的局部变量表中- 而
new Person()
创建的对象实体保存在堆中
2、方法区的理解
方法区在哪里?
《Java虚拟机规范》中明确说明:“尽管方法区在逻辑上属于堆的一部分,但是一些简单的实现可能不会选择去进行垃圾回收或者进行压缩。” 但对于Hotspot JVM而言,方法区还有一个别名Non-Heap(非堆),目的就是为了和堆区别开来
所以,方法区可以看作是一块独立于Java堆的内存空间
方法区的基本概述:
-
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
-
多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等待该线程加载完毕,然后直接使用该类,即类只能加载一次
-
方法区在JVM启动的时候被创建,并且它的实际物理内存空间和Java堆区一样都可以是不连续的。方法区在JVM关闭时释放这个区域的内存
-
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
-
方法区的大小决定了系统可以保存多少个类,因为方法区主要就是用来存储类信息的。如果系统定义了太多的类,将导致方法区溢出,虚拟机同样会抛出内存溢出错误:
java.lang.OutOfMemoryError:PermGen space
(JDK7及之前)
java.lang.OutOfMemoryError:Metaspace
(JDK8及之后)
如下情况可能就会导致出现方法区溢出:- 加载了大量的第三方的 jar 包
- Tomcat 部署的工程过多(30-50个)
- 大量动态地生成反射类
运行下列简单几行代码,在命令行输入jvisualvm
打开工具,可以看到加载了很多类:
public class MethodAreaTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("start...");
Thread.sleep(1000000);
System.out.println("end...");
}
}
3、设置方法区大小和OOM
方法区的大小可以是固定的或者是动态扩展的
-
JDK 7及以前的版本
通过-XX:PermSize
来设置永久代初始分配空间。默认值是 20.75M通过
-XX:MaxPermSize
来设置永久代的最大可分配空间,32 位机默认是 64M,64位机默认是 82M当JVM加载的类信息容量超过了这个值,就会抛出
OutOfMemoryError:PermGenspace
异常
-
JDK 8及之后版本
元数据区大小可以使用参数-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
指定默认值依赖于平台,Windows下,
-XX:MetaspaceSize
约为21M,-XX:MaxMetaspaceSize
的值是一个很大的数值,即没有限制。
如果想要修改上述两个参数,可以在配置中直接设置
-XX:MetaspaceSize=N
XX:MaxMetaspaceSize=N
注意点:
-
-XX:MetaspaceSize
:设置初始的元空间大小。对于一个 64位 的服务器端 JVM 来说,其默认的 -XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。-
如果释放的空间不足,那么在不超过MaxMetaspaceSize的情况下,适当提高该值。
-
如果释放空间过多,则适当降低该值。
-
-
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。
4、方法区的内部结构
方法区存储什么?
《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
可以将 域信息 和 方法信息 算作是类型信息中
代码演示:
/**
* @author zyx
* @version 1.0
* @date 2021/11/22 10:18
*/
public class MethodInnerStrucTest extends Object 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 res = 0;
try{
int value = 30;
res = value / cal;
}catch (Exception e){
e.printStackTrace();
}
return res;
}
@Override
public int compareTo(String o) {
return 0;
}
}
将上述代码编译后的字节码指令,查看其反编译结果:
在终端输入命令:
javap -v -p MethodInnerStrucTest.class > test.txt
下面根据类型信息、域信息、方法信息拆解得到的反编译结果:
类型信息
对于每个加载的类型(类class,接口interface,枚举enum,注解annotation),JVM 必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型的直接父类的完整有效名称(对于interface或是java.lang.Object,都没有父类)
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型实现的接口的有序列表
代码中的
public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable
字节码文件中对应的该部分为:
public class com.zoyoxn.heap.MethodInnerStrucTest extends java.lang.Object
implements java.lang.Comparable<java.lang.String>, java.io.Serializable
可以看出,字节码文件将类的完整有效名称记录了下来。而在运行时方法区中,类信息中记录了是哪个加载器(ClassLoader)加载了该类,同时类加载器(ClassLoader)也记录了它都加载了哪些类,彼此相互记录
域(Field)信息
- JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
代码中的:
//属性
public int num = 10;
private static String str = "测试方法区内部结构信息";
字节码文件中对应的该部分为:
public int num;
descriptor: I
flags: ACC_PUBLIC
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
方法(Method)信息
JVM 必须保存所有方法的以下信息,包括方法声明顺序:
- 方法名称
- 方法返回类型(或 void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
- 异常表(abstract和native方法除外)
记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
在代码中我们是没有编写构造器的,但是系统提供了一个默认的空参构造器,因此字节码文件中有空参构造器的相关信息如下:
public com.zoyoxn.heap.MethodInnerStrucTest();
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 10: 0
line 13: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/zoyoxn/heap/MethodInnerStrucTest;
代码中 test1() 方法:
public void test1(){
int count = 20;
System.out.println("count = "+count);
}
对应字节码文件如下部分:
public void test1();
descriptor: ()V
flags: 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 19: 0
line 20: 3
line 21: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/zoyoxn/heap/MethodInnerStrucTest;
3 26 1 count I
descriptor: ()V
:这里的V
表示 void
args_size=1
:这里test1()是空参,却显示参数个数为1,是因为隐含了当前对象的引用this
代码中 test2(int cal) 方法:
public static int test2(int cal){
int res = 0;
try{
int value = 30;
res = value / cal;
}catch (Exception e){
e.printStackTrace();
}
return res;
}
对应字节码文件如下部分:
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 24: 0
line 26: 2
line 27: 5
line 30: 9
line 28: 12
line 29: 13
line 32: 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 res 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 */
args_size=1
:这里的参数也为1,首先方法就是有参数int的,那么当前对象引用 this 呢?由于该方法是静态方法,因此没有隐含的当前对象引用 this
代码中的异常对应的异常表:
Exception table:
from to target type
2 9 12 Class java/lang/Exception
non-final的类变量
- 静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分
- 类变量被类的所有实例共享,即使没有类实例也可以访问类变量
示例代码:
public class MethodAreaTest {
public static void main(String[] args){
Order order = null;
order.hello();
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
可以看到,创建类 Order
实例时,我们将其设置为了null
值,但使用该null
对象实例去调用static 的方法仍然能够执行成功,这就说明了没有类实例也可以访问类变量
被声明为final
的类变量的处理方法则不同,每个全局常量(static final)在编译的时候就会被分配了
比如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
对于number
常量,在编译的时候就已经赋值了
对于count
变量,count
是静态变量,且进行了显式赋值,那么在类加载过程中就对其赋值
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1 //赋值为1
1: putstatic #5 // Field count:I
4: return
LineNumberTable:
line 12: 0
运行时常量池 VS 常量池
- 方法区内部包含了运行时常量池
- 字节码文件内部包含了常量池
字节码文件通过类加载器,加载到运行时数据区. 而字节码文件中的常量池, 就对应到方法区中的运行时常量池. 因此想要了解运行时常量池, 那就要了解清楚字节码文件中的常量池
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外, 还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和类型、域和方法的符号引用
为什么需要常量池?
一个Java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据很大,因此不能直接存到字节码中。换另一种方式,可以存到常量池中,然后这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池
比如下面的代码:
public class SimpleClass {
public static void main(String[] args) {
System.out.println("hello");
}
}
虽然上述代码只占用了574字节,但是里面使用到了String、System、PrintStream及object等结构。这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这里就需要用到常量池了。
如果不使用常量池,就需要将用到的类信息、方法信息等记录在当前的字节码文件中,造成文件臃肿。因此我们将所需用到的结构信息记录在常量池中,并通过引用的方式,来加载、调用所需的结构
小结:
常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型和字面量等信息
运行时常量池
- 运行时常量池是方法区的一部分
- 常量池表是字节码文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。也就是说编译期只是确定了需要哪些类和接口,等运行了才加载进来放到运行时常量池。
- 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池
- JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。就像炒菜,炒菜前已经根据配方知道了该放哪些调料(编译时确定好字节码文件中的符号引用),但是真正等到炒菜的时候才放配方中的调料进去(运行时将需要用到的类加载,不再是符号地址,而是真实地址)
- 运行时常量池,相对于字节码文件常量池的另一重要特征是:
动态性
- 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛
OutOfMemoryError
异常
5、方法区使用举例
示例代码:
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);
}
}
- 字节码执行过程展示:初始状态,此时本地变量表中下标 0 位置存储了main方法中的参数
args
,由于main 方法是静态方法,因此不需要存储当前对象的引用this
sipush 500
将操作数500压入操作数栈中,之所以是sipush
是因为 500 是在short 类型范围内的
istore_1
将操作数 500 从操作数栈中取出,存储到局部变量表中索引为 1 的位置
bipush 100
将操作数100压入操作数栈中,之所以是bipush
是因为 100 是在 byte 类型范围内的
istore_2
操作数 100 从操作数栈中取出,存储到局部变量表中索引为 2 的位置
iload_1
读取局部变量表中下标为 1 的值,压入操作数栈
iload_2
读取局部变量表中下标为 2 的值 ,压入操作数栈
- 两数相除,计算结果放在操作数栈顶,之后执行
istore_3
指令,将计算结果从操作数栈中弹出,存入局部变量表中下标为 3 的位置
bipush 50
将操作数 50 压入操作数栈
istore_4
将操作数 50 从栈顶弹出,保存在局部变量表 4 中
- 获取 System.out 输出流的引用
- 将本地变量表 3 的值取出,压入操作数栈中,准备进行加法运算
- 将本地变量表 4 的值取出,压入操作数栈中,准备进行加法运算
- 执行加法运算后,将计算结果放在操作数栈顶
- 调用静态方法 println( ) ,输出加法结果
- main( ) 方法执行结束
6、方法区的演进细节
Hotspot中方法区的变化:
StringTable 为什么要从永久代调整到堆中?
JDK7中将 StringTable 放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发
这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存
7、方法区的垃圾回收
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
常量池的垃圾收集
- 常量池中通常存放两大类常量:字面量和符号引用
字面量:文本字符串、被声明为final的常量值等
符号引用:- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- Hotspot 虚拟机对常量池的回收策略是很明确的,
只要常量池中的常量没有被任何地方引用,就可以被回收
- 回收废弃常量和回收堆内的对象是类似的
回收不再使用的类型(需要满足的条件很苛刻)
-
判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
-
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,Hotspot虚拟机提供了
-Xnoclassgc
参数进行控制,还可以使用-verbose:class
以及-XX:+TraceClass-Loading
、-XX:+TraceClassUnLoading
查看类加载和卸载信息 -
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及 OSGi 这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力