写点什么

深入理解 JVM 内存管理 - 方法区

用户头像
NORTH
关注
发布于: 2020 年 06 月 02 日
深入理解JVM内存管理 - 方法区

得益于JVM的自动内存管理机制,开发者在写代码时,很少再去关注内存分配与释放,一般情况下,也不会出现内存泄露和溢出问题。不过,由于开发者把内存的控制权交给了JVM,一旦出现内存泄露和溢出问题,如果不了解JVM是怎样使用内存的,将很难排查和修正错误。本文从概念上介绍JVM内存的各个区域及其作用。



JVM在执行Java程序时会把其所管理的内存划分成多个不同的数据区域,每个区域的创建时间、销毁时间以及用途都各不相同。比如有的内存区域是所有线程共享的,而有的内存区域是线程隔离的。线程隔离的区域就会随着线程的启动和结束而创建和销毁。JVM所管理的内存将会包含以下几个运行时数据区域,如下图的上半部分所示。



一、方法区(Mehod Area)



方法区是所有线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、JIT编译后的代码缓存等数据。在Java虚拟机规范中,方法区属于堆的一个逻辑部分,但很多情况下,都把方法区与堆区分开来说。而平时开发中通过反射获取到的类名、方法名、字段名称、访问修饰符等信息都是从这块区域获取的。



在JDK8以前,使用HotSpot虚拟机的同学都喜欢将方法区称为永久代(Permanent Generation),但实际上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队是用永久代来实现方法区而已,这样使得垃圾收集器能够像管理堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但对于其他的虚拟机(JRockit、J9)来说,并不存在永久代这一概念的。



但现在看来,使用永久代来实现方法区并不是一个好注意,由于方法区会存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,在某些场景下非常容易出现永久代内存溢出。如Spring、Hibernate等框架在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。



举个简单的例子来测试,下面的代码通过CGLIB的Enhancer来指定要代理的目标对象以及对代理对象增强的逻辑,最终通过调用create()方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept(),因此,我们在这里可以加入各种增强逻辑,比如打印日志和安全检查等。而通过调用proxy.invokeSuper()会把调用转发给代理对象。



// -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
// 可以使用CGLIB库,如果是Spring项目,可以不用依赖任何库
public class CGlibProxyDemo {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
// 设置需要代理的目标对象
enhancer.setSuperclass(ProxyObject.class);
// 不要缓存
enhancer.setUseCache(false);
// 这里指定需要在原代理对象上增强的逻辑
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o,
Method method,
Object[] os,
MethodProxy proxy) throws Throwable {
System.out.println("I am proxy");
return proxy.invokeSuper(o,os);
}
});
ProxyObject proxy = (ProxyObject) enhancer.create();
proxy.greet();
}
}
/**
* 被代理对象
*/
static class ProxyObject {
public String greet() {
return "Thanks for you";
}
}
}



运行时设置VM参数控制Metaspace大小(JDK1.8以上),运行过程中,可以使用VisualVM来监控Metaspace的空间大小变化情况,大致如下图所示。



监控时,先启动VisualVM,然后debug运行程序,接着在VisualVM找到对应的进程,切换到监控选项卡后,最后放开断点,观察监控情况。





而运行结果则是抛出内存溢出错误:



Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:416)
at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:563)
......



Metaspace不使用堆内存,而是Native Memory,其大小受本机内存大小限制,默认情况下,一般不会出现内存溢出错误,但在使用Hibernate或者Spring AOP时,最好可以关注一下Metaspace的使用情况。



虽然很少会有人再使用JDK1.6了,但这里还是提一句。在JDK1.6中,方法区虽然被称为永久代,但并不意味着这些对象真的能够永久存在,JVM的内存回收机制,仍然会对这一块区域进行扫描,只不过回收这部分内存的条件相当苛刻罢了。

1.1、运行时常量池 (Runtime Constant Pool)



回过头来看下图1的下半部分,方法区主要包含:



  1. 运行时常量池(Runtime Constant Pool)

  2. 类信息(Class & Field & Method data)

  3. 编译器编译后的代码(Code)等等



后面两项都比较好理解,但运行时常量池有何作用,其意义何在?抛开运行时3个字,首先了解下何为常量池。



Java源文件经编译后得到存储字节码的Class文件,Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中。也就是说,哪个字节代表什么含义,长度多少,先后顺序如何都是被严格限定的,是不允许改变的。比如:开头的4个字节存放魔数,用于确定这个文件是否能够被JVM接受,接下来的4个字节用于存放版本号,再接着存放的就是常量池。常量池的长度是不固定的,所以,在常量池的入口存放着常量池容量的计数值。



常量池主要用于存放两大类常量:字面量和符号引用量,字面量相当于Java语言层面常量的概念,比如:字符串常量、声明为final的常量等等。符号引用是用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。



说到这儿,首先简单介绍下如何查看分析字节码,其实这部分内容最好是到B站找视频看,这样会更直观一些,但这里也作一些简单介绍。与其他文章不同,这里会分析一段稍显复杂的代码:



public class BytecodeDemo {
public static final String mstring = "hicsc.com";
public static int mint;
static {
mint = 1;
}
public BytecodeDemo() {
System.out.println("创建bytecodeDemo对象");
}
public synchronized void syncMethod(String param) {
System.out.println("这是一个同步方法:" + param);
}
public void syncBlockMethod() {
synchronized (this) {
System.out.println("这是同步代码块");
}
mint = 2;
}
public void exceptionMethod() {
try {
InputStreamReader reader = new InputStreamReader(new FileInputStream(new File("test.txt")));
BufferedReader bufferedReader = new BufferedReader(reader);
String line = null;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
reader.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("执行finally方法");
}
}



代码非常简单,但已经包含一些理解字节码非常基础的操作,比如静态代码库、同步代码库、同步方法、异常处理,如果你能很好的理解这段代码产生的字节码,那么你也可以非常轻松的分析其他字节码,接下来我会分块介绍这段代码生成的字节码,主要是因为太长了。

1.1.1 魔数与Class文件的版本



public class com.hicsc.proxy.BytecodeDemo
// 次版本号
minor version: 0
// 主版本号
major version: 52
// 类的访问控制符
// 这个类是public,所以其访问控制符是ACC_PUBLIC
// ACC_SUPER用于兼容以前的JDK版本,具体原因可自行搜索
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
// 当前类名,#26表示常量池下标,代表常量池中的第26个常量
this_class: #26 // com/hicsc/proxy/BytecodeDemo
// 父类名称,#2同上
super_class: #2 // java/lang/Object
// 接口数、字段数、方法数、
// 接口数和字段数上面数数就清楚,但方法数为什么是5,代码中明明只有4个?接着看下面
interfaces: 0, fields: 2, methods: 5, attributes: 1



每个Class文件的头4个字节被称为魔数,它的唯一作用是确定这个文件是否为一个可被虚拟机接受的Class文件,其值为0xCAFEBABE,在上面的字节码中并未体现。

1.1.2 常量池
// 总共包含109个常量,其中#数字表示索引
// 等号后面表示常量的类型,具体的类型信息参考代码后面的表格
// 紧接着是常量代表的内容
// 注意:下面的代码中省略了部分注释
Constant pool:
// 类中方法的符号引用,其值是 #2.#3
// 其中 #2 = #4 = java/lang/Object
// #3 = #5:#6 = <init>:()V
// 合起来就是:java/lang/Object."<init>":()V,即跟后面的注释是一样的
// 即:Object类的构造方法,这也是前面为什么方法数是5的原因,出了代码中给出的方法
// 还包含父类和当前类的构造方法
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // 创建bytecodeDemo对象
#14 = Utf8 创建bytecodeDemo对象
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // java/lang/StringBuilder
#22 = Utf8 java/lang/StringBuilder
#23 = Methodref #21.#3 // java/lang/StringBuilder."<init>":()V
#24 = String #25 // 这是一个同步方法:
#25 = Utf8 这是一个同步方法:
#26 = Methodref #21.#27 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#27 = NameAndType #28:#29 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#28 = Utf8 append
#29 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#30 = Methodref #21.#31 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#31 = NameAndType #32:#33 // toString:()Ljava/lang/String;
#32 = Utf8 toString
#33 = Utf8 ()Ljava/lang/String;
#34 = String #35 // 这是同步代码块
#35 = Utf8 这是同步代码块
#36 = Fieldref #37.#38 // com/hicsc/proxy/BytecodeDemo.mint:I
#37 = Class #39 // com/hicsc/proxy/BytecodeDemo
#38 = NameAndType #40:#41 // mint:I
#39 = Utf8 com/hicsc/proxy/BytecodeDemo
#40 = Utf8 mint
#41 = Utf8 I
#42 = Class #43 // java/io/InputStreamReader
#43 = Utf8 java/io/InputStreamReader
#44 = Class #45 // java/io/FileInputStream
#45 = Utf8 java/io/FileInputStream
#46 = Class #47 // java/io/File
#47 = Utf8 java/io/File
#48 = String #49 // test.txt
#49 = Utf8 test.txt
#50 = Methodref #46.#51 // java/io/File."<init>":(Ljava/lang/String;)V
#51 = NameAndType #5:#20 // "<init>":(Ljava/lang/String;)V
#52 = Methodref #44.#53 // java/io/FileInputStream."<init>":(Ljava/io/File;)V
#53 = NameAndType #5:#54 // "<init>":(Ljava/io/File;)V
#54 = Utf8 (Ljava/io/File;)V
#55 = Methodref #42.#56 // java/io/InputStreamReader."<init>":(Ljava/io/InputStream;)V
#56 = NameAndType #5:#57 // "<init>":(Ljava/io/InputStream;)V
#57 = Utf8 (Ljava/io/InputStream;)V
#58 = Class #59 // java/io/BufferedReader
#59 = Utf8 java/io/BufferedReader
#60 = Methodref #58.#61 // java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
#61 = NameAndType #5:#62 // "<init>":(Ljava/io/Reader;)V
#62 = Utf8 (Ljava/io/Reader;)V
#63 = Methodref #58.#64 // java/io/BufferedReader.readLine:()Ljava/lang/String;
#64 = NameAndType #65:#33 // readLine:()Ljava/lang/String;
#65 = Utf8 readLine
#66 = Methodref #58.#67 // java/io/BufferedReader.close:()V
#67 = NameAndType #68:#6 // close:()V
#68 = Utf8 close
#69 = Methodref #42.#67 // java/io/InputStreamReader.close:()V
#70 = String #71 // 执行finally方法
#71 = Utf8 执行finally方法
#72 = Class #73 // java/io/FileNotFoundException
#73 = Utf8 java/io/FileNotFoundException
#74 = Methodref #72.#75 // java/io/FileNotFoundException.printStackTrace:()V
#75 = NameAndType #76:#6 // printStackTrace:()V
#76 = Utf8 printStackTrace
#77 = Class #78 // java/io/IOException
#78 = Utf8 java/io/IOException
#79 = Methodref #77.#75 // java/io/IOException.printStackTrace:()V
#80 = Utf8 mstring
#81 = Utf8 Ljava/lang/String;
#82 = Utf8 ConstantValue
#83 = String #84 // hicsc.com
#84 = Utf8 hicsc.com
#85 = Utf8 Code
#86 = Utf8 LineNumberTable
#87 = Utf8 LocalVariableTable
#88 = Utf8 this
#89 = Utf8 Lcom/hicsc/proxy/BytecodeDemo;
#90 = Utf8 syncMethod
#91 = Utf8 param
#92 = Utf8 syncBlockMethod
#93 = Utf8 StackMapTable
#94 = Class #95 // java/lang/Throwable
#95 = Utf8 java/lang/Throwable
#96 = Utf8 exceptionMethod
#97 = Utf8 reader
#98 = Utf8 Ljava/io/InputStreamReader;
#99 = Utf8 bufferedReader
#100 = Utf8 Ljava/io/BufferedReader;
#101 = Utf8 line
#102 = Utf8 e
#103 = Utf8 Ljava/io/FileNotFoundException;
#104 = Utf8 Ljava/io/IOException;
#105 = Class #106 // java/lang/String
#106 = Utf8 java/lang/String
#107 = Utf8 <clinit>
#108 = Utf8 SourceFile
#109 = Utf8 BytecodeDemo.java



常量池中每个项目其类型如下

  • Methodref:类中方法的符号引用

  • Class:类或接口的符号引用

  • NameAndType:字段或方法的部分符号引用

  • Utf8:UTF-8编码的字符串

  • Fieldref:字段的符号引用

  • String:字符串类型的字面量



常量池被喻为Class文件的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一。

1.1.3 常量与变量
public static final java.lang.String mstring;
// 常量的修饰符,即类型是字符串,L表示字符串
descriptor: Ljava/lang/String;
// 常量访问修饰符:public static final
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
// 常量值
ConstantValue: String hicsc.com
public static int mint;
// 变量类型是I,int
descriptor: I
// 访问修饰符:public static
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
1.1.4 构造方法
public com.hicsc.proxy.BytecodeDemo();
// 构造方法返回值是空
descriptor: ()V
// 访问修饰符
flags: (0x0001) ACC_PUBLIC
// 代码
Code:
// 堆栈深度为2,局部变量1个,方法的参数1个
// 每个方法出了给出的参数以外,
// 还包含一个隐藏的this参数,这样就可以在方法内部使用this.变量来访问当前对象的变量
//
stack=2, locals=1, args_size=1
// 将第0个变量槽中为reference类型的本地变量推送到操作数栈顶
0: aload_0
// 以栈顶的reference类型的数据所指向的对象作为方法接收者
// 调用此对象的实例构造器方法、private方法或者它的父类的方法
// #1 常量池中的第1号常量,即Object的构造方法
// 这句指令即表示调用父类构造方法
1: invokespecial #1 // Method java/lang/Object."<init>":()V
// 从类中获取一个静态变量 #7,即系统输出流
4: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
// 将 #13 常量值推送到栈顶
7: ldc #13 // String 创建bytecodeDemo对象
// 用于调用对象的实例方法
9: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
// 行号表
LineNumberTable:
// 上面 aload_0指令对应代码的22行
line 22: 0
// getstatic指令对应代码的23行
line 23: 4
// return指令对应代码的24行
line 24: 12
// 用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,
// 它不是运行时必需的属性,但默认会生成到Class文件之中
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/hicsc/proxy/BytecodeDemo;



LocalVariableTable属性可以在Javac中使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,譬如IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值,在接下来会有更自观的感受。

1.1.5 同步方法
public synchronized void syncMethod(java.lang.String);
// 字符串类型参数,返回值为Void
descriptor: (Ljava/lang/String;)V
// 访问控制符 public synchronized
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
// 堆栈深度3,局部变量2个,方法参数2个
stack=3, locals=2, args_size=2
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #21 // class java/lang/StringBuilder
6: dup
7: invokespecial #23 // Method java/lang/StringBuilder."<init>":()V
10: ldc #24 // String 这是一个同步方法:
12: invokevirtual #26 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #26 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #30 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 27: 0
line 28: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 this Lcom/hicsc/proxy/BytecodeDemo;
0 26 1 param Ljava/lang/String;



关于具体的指令这里不再作过多的说明,每条指令代表的具体含义,大家可以参考The Java Virtual Machine Instruction Set。这个方法的字节码中有亮点需要注意:



  • 可能你曾经多少听说过,synchronized关键字实现的原理是在字节码中生成 monitorentermonitorexit指令来实现的,但这里的synchronized方法并没有生成这两条指令,而是用 ACC_SYNCHRONIZED 来描述,其实原理是一样的

  • 这里对LocalVariableTable的理解会更直观一些,里就是描述了这个方法的两个参数名,一个是this,一个是param

1.1.6 同步代码块
public void syncBlockMethod();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #34 // String 这是同步代码块
9: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
// 正常结束
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
// 如果同步代码块中抛出异常,仍然会执行monitorexit
19: monitorexit
20: aload_2
21: athrow
22: iconst_2
23: putstatic #36 // Field mint:I
26: return
// 异常表,在下文介绍异常方法时具体介绍
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 31: 0
line 32: 4
line 33: 12
line 34: 22
line 35: 26
LocalVariableTable:
Start Length Slot Name Signature
0 27 0 this Lcom/hicsc/proxy/BytecodeDemo;
// 记录方法中操作数栈与局部变量区的类型在一些特定位置的状态
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/hicsc/proxy/BytecodeDemo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4



StackMapTable属性在JDK 6增加到Class文件规范之中,它是一个相当复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。



可能大家对类型推导的感知不强,毕竟大家所写的Java代码对每个变量都定义了类型,但实际上JVM在加载字节码时需要验证每个变量与其实际定义的类型是否一致,比如,如果一个变量被自定为字符串类型,那么我们就不能给它赋值为数字,这属于验证。而如果你关注最新的JDK特性的话,Java在很早以前就引入了var关键字,比如:



var mint = 1;
var mstring = "hicsc.com";



如果你对这部分内容感兴趣的话,可以参考:能介绍一下StackMapTable属性的运作原理吗?

1.1.7 异常处理
public void exceptionMethod();
// 方法返回Void
descriptor: ()V
// 访问修饰符 public
flags: (0x0001) ACC_PUBLIC
Code:
stack=7, locals=5, args_size=1
// 创建InputStreamReader
0: new #42 // class java/io/InputStreamReader
// 复制操作数堆栈上的顶部值,并将复制的值推送到操作数堆栈上
// 具体作用请参考代码下的文字说明
3: dup
4: new #44 // class java/io/FileInputStream
7: dup
8: new #46 // class java/io/File
11: dup
// 将字符串 test.txt 从常量池中推送至栈顶
12: ldc #48 // String test.txt
14: invokespecial #50 // Method java/io/File."<init>":(Ljava/lang/String;)V
17: invokespecial #52 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
20: invokespecial #55 // Method java/io/InputStreamReader."<init>":(Ljava/io/InputStream;)V
// 将创建的InputStreamReader对象放到局部变量中
23: astore_1
24: new #58 // class java/io/BufferedReader
27: dup
// 将InputStreamReader对象从局部变量中捞出来
28: aload_1
29: invokespecial #60 // Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
// 将创建的BufferReader对象放到局部变量中去
32: astore_2
// 定义局部变量 line = null;
33: aconst_null
// 将line放到操作数栈顶
34: astore_3
// 捞出BufferReader对象放到栈顶
35: aload_2
// 调用BufferReader的readLine方法,读取数据
36: invokevirtual #63 // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
// invokevirtual消耗掉栈顶的BufferReader对象,现在栈顶是line=null
// 先复制一份line=null,因为astore_3会把刚才readline的结果复制给line
39: dup
// line=readLine放到局部变量中去,栈顶还是line=null,继续后面的循环
40: astore_3
// 如果为空,goto到54,即是 aload_2,捞出BufferReader并调用close方法
41: ifnull 54
// 如果不为空,调用println方法打印
44: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
47: aload_3
48: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 打印完成,goto到35,继续readLine读取下一行
51: goto 35
// 从局部变量中把BufferedReader对象捞出来放到栈顶
54: aload_2
// 调用BufferedReaer对象的close方法
55: invokevirtual #66 // Method java/io/BufferedReader.close:()V
// 从局部变量中把InputStreamReader对象捞出来放到栈顶
58: aload_1
// 调用InputStreamReader对象的close方法
59: invokevirtual #69 // Method java/io/InputStreamReader.close:()V
// 接着是finally中的println语句
62: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
65: ldc #70 // String 执行finally方法
67: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// goto到118,即reture,方法执行结束
70: goto 118
// 73-83行是执行 FileNotFoundException.printStackTrace:()打印异常的堆栈信息
73: astore_1
74: aload_1
75: invokevirtual #74 // Method java/io/FileNotFoundException.printStackTrace:()V
78: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
81: ldc #70 // String 执行finally方法
83: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
86: goto 118
// 89-99行是执行 IOException.printStackTrace:()打印异常的堆栈信息
89: astore_1
90: aload_1
91: invokevirtual #79 // Method java/io/IOException.printStackTrace:()V
94: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
97: ldc #70 // String 执行finally方法
99: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
102: goto 118
// 又是执行finally方法,前面是指正常执行完代码走到finally,
// 这里是抛出异常后,做了相关处理后,再到finally
105: astore 4
107: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
110: ldc #70 // String 执行finally方法
112: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
115: aload 4
// 如果到这个位置还有未处理的异常,那么直接抛出去
117: athrow
118: return
Exception table:
from to target type
0 62 73 Class java/io/FileNotFoundException
0 62 89 Class java/io/IOException
0 62 105 any
73 78 105 any
89 94 105 any
105 107 105 any
LineNumberTable:
// ......
LocalVariableTable:
// ......
StackMapTable: number_of_entries = 6
// ......
① dup与astore指令

其实每个new指令之后,都会跟一个dup指令,而紧接着就是invokespecial指令来调用构造方法,比如:

Object obj = new Object();

其字节码大致是:

new Object
dup
invokespecial Object.<init>()V



而创建一个对象的大致分为3个步骤:



  1. 创建并初始化Object类型的对象

  2. 调用Object的构造方法

  3. 将obj指向新创建的对象



而new指令的作用是创建指定类型的对象实例,对其进行默认初始化(注意跟调用构造函数的初始化不同,在类加载部分已经详细讲解,如有疑问,可翻看前面几篇文章),并将指向该实例的一个引用压入操作数栈顶。



然后invokespecial指令会消耗掉栈顶的引用,因为构造方法有一个默认的参数this,jvm会把操作数栈顶的对象应用拿出来作为构造方法的this参数。如果我们希望在invokespecial调用后,在操作数栈顶还有一个指向新建对象的应用,就得在调用这个指令之前先复制一份该对象的引用。



这就是dup指令的作用。



在此基础上,如果我们把这个引用保存到局部变量中去,就会有对应的astore指令,它会把操作数栈顶的那个引用消耗掉,保存到指定的局部变量去。就比如:

20: invokespecial #55 // Method InputStreamReader."<init>":(Ljava/io/InputStream;)V
23: astore_1
24: new #58 // class java/io/BufferedReader
27: dup
28: aload_1



在调用完InputStreamReader的构造方法后,会把InputStreamReader对象保存到局部变量中,因为代码是这样写的:

InputStreamReader reader = new InputStreamReader(new FileInputStream(new File("test.txt")));
BufferedReader bufferedReader = new BufferedReader(reader);



保存到局部变量后,在创建BufferedReader对象,注意在dup指令后,有一个aload指令,这是因为在构造BufferedReader对象时需要InputStreamReader对象,所以需要首先把InputStreamReader对象从局部变量中给捞出来。

② 异常表

字节码是代码编译过后得到的二进制文件,是静态的,简单的理解也就是把源码翻译了一下,但异常在哪儿抛出来,抛出来以后又往什么地方走,这些都是在运行过程中才知道的。因此,在上面的字节码中可以看到很多的goto语句。而到底异常在哪儿抛出来,抛出来后又改如何处理?这就是异常表,JVM会在出现异常的方法中,查找异常表,看看能否找到合适的处理者来处理,异常表的具体内容如下:



Exception table:
from to target type
0 62 73 Class java/io/FileNotFoundException
0 62 89 Class java/io/IOException
0 62 105 any
73 78 105 any
89 94 105 any
105 107 105 any



其中 from 表示可能发生异常的起始索引,to表示可能发生异常的结束索引,target表示抛出对应类型异常后开始处理异常的字节码索引,连起来也就是说:如果从第0-62行中,抛出了FileNotFoundException异常,那么程序就跳转到第73行。回过去,看看第73行指令是什么:



// 73-83行是执行 FileNotFoundException.printStackTrace:()打印异常的堆栈信息
73: astore_1
74: aload_1
75: invokevirtual #74 // Method java/io/FileNotFoundException.printStackTrace:()V
78: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
81: ldc #70 // String 执行finally方法
83: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V



如果某个异常,既不属于FileNotFoundException,也不属于IOException,JVM会继续查找异常表中的剩余条目,比如这里的any,表示可以处理任何异常,即无论发生任何异常都跳转到105行,开始处理异常。



异常表的后面3个条目,表示catch或者finally中出现异常的话,仍然跳转的105行继续处理。

1.1.8 静态代码块
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #36 // Field mint:I
4: return
LineNumberTable:
line 19: 0
line 20: 4



静态代码块,表示给静态变量mint赋值为1,指令iconst_n表示将int类型数字n推送至栈顶,n取值0~5。

1.1.9 字节码小结

到这,基本上把日常开发中最基础最常用的字节码都介绍完了,如果你能够很好的理解上面的内容,在结合Oracle官方的指令说明文档,基本上能够很好的阅读理解复杂的字节码。当然,如果你的代码本身使用了很多新的特性以及很复杂的逻辑,生成的字节码肯定会很复杂,这时候你想理解这些字节码仍然不易,一个简单的方法是另写一个类,保留主要逻辑,去掉一些判断、循环、异常处理等细枝末节再看看,也许会简单一些。



如果你对上面内容的理解仍有困难,建议去B站上面看看,有相当多的视频教你一个字节一个字节的读,对你理解最基础的内容应该有帮助。

1.1.10 运行时常量池小结

常量池是字节码中的内容,是存储在Class文件中的,而Class文件中存储的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。运行时常量池就可以理解为常量池被加载到内存之后的版本,但并非只有Class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也可能产生新的常量,它们也可以放入运行时常量池中。



很多同学在学习这部分内容的时候,对「直接引用」和「符号引用」这两个概念不是很清楚,建议大家看看:JVM里的符号引用如何存储 ,应当对你有帮助。



深入理解JVM系列的第4篇,从目录阅读请移步:深入理解JVM系列文章目录

参考资料

周志明著;深入理解Java虚拟机(第三版);机械工业出版社;2019-12

圣思园张龙视频教程;深入理解JVM

关于JVM字节码中dup指令的问题?

The Java Virtual Machine Instruction Set

能介绍一下StackMapTable属性的运作原理吗?

JVM里的符号引用如何存储

发布于: 2020 年 06 月 02 日阅读数: 128
用户头像

NORTH

关注

Because, I love. 2017.10.16 加入

这里本来应该有简介的,但我还没想好 ( 另外,所有文章会同步更新到公众号:时光虚度指南,欢迎关注 ) 。

评论

发布
暂无评论
深入理解JVM内存管理 - 方法区