Java finally语句到底是在return之前还是之后执行(JVM字节码分析及内部体系结构)?
之前看了一篇关于“Java finally语句到底是在return之前还是之后执行?”这样的博客,看到兴致处,突然博客里的一个测试用例让我产生了疑惑。
测试用例如下:
public class FinallyTest {
public static void main(String[] args) {
System.out.println(getMap().get("key"));
}
public static Map<String,String> getMap(){
Map<String,String> map=new HashMap<String,String>();
map.put("key","init");
try {
map.put("key", "try");
return map;
}catch (Exception e){
map.put("key","catch");
}finally {
map.put("key","finally");
map=null;
}
return map;
}
}
返回结果:
finally
对于这个结果,我想大部分人也会觉得非常疑惑,我当时也是非常疑惑的,于是我用java自带的工具看了class字节码。
编译字节码命令:
javap -v -p -s -sysinfo -constants FinallyTest.class
字节码文件如下
Classfile /xxx/FinallyTest.class
Last modified 2016-12-13; size 1424 bytes
MD5 checksum fabe826fc077132d6f7b49ca1f630d6c
Compiled from "FinallyTest.java"
public class xxx.FinallyTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #17.#45 // java/lang/Object."<init>":()V
#2 = Fieldref #46.#47 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #16.#48 // com/it/blabla/test1/FinallyTest.getMap:()Ljava/util/Map;
#4 = String #49 // key
#5 = InterfaceMethodref #50.#51 // java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
#6 = Class #52 // java/lang/String
#7 = Methodref #53.#54 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #55 // java/util/HashMap
#9 = Methodref #8.#45 // java/util/HashMap."<init>":()V
#10 = String #56 // init
#11 = InterfaceMethodref #50.#57 // java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
#12 = String #58 // try
#13 = String #59 // finally
#14 = Class #60 // java/lang/Exception
#15 = String #61 // catch
#16 = Class #62 // com/it/blabla/test1/FinallyTest
#17 = Class #63 // java/lang/Object
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/it/blabla/test1/FinallyTest;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 getMap
#30 = Utf8 ()Ljava/util/Map;
#31 = Utf8 e
#32 = Utf8 Ljava/lang/Exception;
#33 = Utf8 map
#34 = Utf8 Ljava/util/Map;
#35 = Utf8 LocalVariableTypeTable
#36 = Utf8 Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;
#37 = Utf8 StackMapTable
#38 = Class #64 // java/util/Map
#39 = Class #60 // java/lang/Exception
#40 = Class #65 // java/lang/Throwable
#41 = Utf8 Signature
#42 = Utf8 ()Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;
#43 = Utf8 SourceFile
#44 = Utf8 FinallyTest.java
#45 = NameAndType #18:#19 // "<init>":()V
#46 = Class #66 // java/lang/System
#47 = NameAndType #67:#68 // out:Ljava/io/PrintStream;
#48 = NameAndType #29:#30 // getMap:()Ljava/util/Map;
#49 = Utf8 key
#50 = Class #64 // java/util/Map
#51 = NameAndType #69:#70 // get:(Ljava/lang/Object;)Ljava/lang/Object;
#52 = Utf8 java/lang/String
#53 = Class #71 // java/io/PrintStream
#54 = NameAndType #72:#73 // println:(Ljava/lang/String;)V
#55 = Utf8 java/util/HashMap
#56 = Utf8 init
#57 = NameAndType #74:#75 // put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
#58 = Utf8 try
#59 = Utf8 finally
#60 = Utf8 java/lang/Exception
#61 = Utf8 catch
#62 = Utf8 com/it/blabla/test1/FinallyTest
#63 = Utf8 java/lang/Object
#64 = Utf8 java/util/Map
#65 = Utf8 java/lang/Throwable
#66 = Utf8 java/lang/System
#67 = Utf8 out
#68 = Utf8 Ljava/io/PrintStream;
#69 = Utf8 get
#70 = Utf8 (Ljava/lang/Object;)Ljava/lang/Object;
#71 = Utf8 java/io/PrintStream
#72 = Utf8 println
#73 = Utf8 (Ljava/lang/String;)V
#74 = Utf8 put
#75 = Utf8 (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
{
public com.it.blabla.test1.FinallyTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/it/blabla/test1/FinallyTest; public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: invokestatic #3 // Method getMap:()Ljava/util/Map;
6: ldc #4 // String key
8: invokeinterface #5, 2 // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
13: checkcast #6 // class java/lang/String
16: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: return
LineNumberTable:
line 13: 0
line 14: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 args [Ljava/lang/String; public static java.util.Map<java.lang.String, java.lang.String> getMap();
descriptor: ()Ljava/util/Map;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=0
0: new #8 // class java/util/HashMap
3: dup
4: invokespecial #9 // Method java/util/HashMap."<init>":()V
7: astore_0
8: aload_0
9: ldc #4 // String key
11: ldc #10 // String init
13: invokeinterface #11, 3 // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
18: pop
19: aload_0
20: ldc #4 // String key
22: ldc #12 // String try
24: invokeinterface #11, 3 // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
29: pop
30: aload_0
31: astore_1
32: aload_0
33: ldc #4 // String key
35: ldc #13 // String finally
37: invokeinterface #11, 3 // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
42: pop
43: aconst_null
44: astore_0
45: aload_1
46: areturn
47: astore_1
48: aload_0
49: ldc #4 // String key
51: ldc #15 // String catch
53: invokeinterface #11, 3 // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
58: pop
59: aload_0
60: ldc #4 // String key
62: ldc #13 // String finally
64: invokeinterface #11, 3 // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
69: pop
70: aconst_null
71: astore_0
72: goto 91
75: astore_2
76: aload_0
77: ldc #4 // String key
79: ldc #13 // String finally
81: invokeinterface #11, 3 // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
86: pop
87: aconst_null
88: astore_0
89: aload_2
90: athrow
91: aload_0
92: areturn
Exception table:
from to target type
19 32 47 Class java/lang/Exception
19 32 75 any
47 59 75 any
LineNumberTable:
line 16: 0
line 17: 8
line 19: 19
line 20: 30
line 24: 32
line 25: 43
line 21: 47
line 22: 48
line 24: 59
line 25: 70
line 26: 72
line 24: 75
line 25: 87
line 27: 91
LocalVariableTable:
Start Length Slot Name Signature
48 11 1 e Ljava/lang/Exception;
8 85 0 map Ljava/util/Map;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 85 0 map Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;
StackMapTable: number_of_entries = 3
frame_type = 255 /* full_frame */
offset_delta = 47
locals = [ class java/util/Map ]
stack = [ class java/lang/Exception ]
frame_type = 91 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 15 /* same */
Signature: #42 // ()Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;
}
SourceFile: "FinallyTest.java"
通过查阅资料,我发现这个里面涉及的东西挺多,包括java虚拟机(JVM)的内部体系,索性一次性全部总结,有啥理解不正确的地方或不全的地方请各位看官指出。
该图上所示的部件在下面分两部分进行解释。 第一部分涵盖为每个线程创建的组件,第二部分涵盖独立于线程创建的组件。
JVM里的线程概念
线程是程序中的执行线程。 JVM允许应用程序具有并发运行的多个执行线程。 在HotSpot JVM中,在Java Thread和本机操作系统Thread之间有一个直接映射。 在准备了诸如线程本地存储,分配缓冲区,同步对象,堆栈和程序计数器的Java线程的所有状态之后,创建本地线程。 一旦Java线程终止,本地线程就被回收。 因此,操作系统负责调度所有线程并将它们分派给任何可用的CPU。 一旦本地线程初始化,它调用Java线程中的run()方法。 当run()方法返回时,处理未捕获的异常,然后本机线程确认JVM是否需要作为线程终止的结果而被终止(即,它是最后一个非deamon线程)。 当线程终止时,本地和Java线程的所有资源都被释放。
JVM系统线程
如果你使用jconsole或任何调试器,可以看到有许多线程在后台运行。 这些后台线程除了作为调用public static void main(String [])的一部分而创建的主线程以及由主线程创建的任何线程之外。 HotSpot JVM中的主要后台系统线程是:
1. VM thread
此线程等待出现需要JVM到达安全点的操作。 这些操作必须在单独的线程上发生的原因是因为它们都要求JVM处于不会发生对堆的修改的安全点。 该线程执行的操作类型是“停止世界”垃圾回收,线程堆栈转储,线程挂起和偏置锁定撤销。
2.Periodic task thread
该线程负责用于调度周期性操作的执行的定时器事件(即中断)
3.GC threads
这些线程支持在JVM中发生的不同类型的垃圾回收活动
4.Compiler threads
这些线程在运行时将字节代码编译为本地代码
5.Signal dispatcher thread
此线程接收发送到JVM进程的信号,并通过调用相应的JVM方法在JVM中处理它们。
每个执行线程都有如下组件:
1.Program Counter (PC)--即程序计数器
当前指令(或操作码)的地址,除非它是本地的。 如果当前方法是native,那么PC是未定义的。 所有CPU都有一个PC,通常PC在每个指令之后递增,因此保存要执行的下一条指令的地址。 JVM使用PC来跟踪其执行指令的位置,PC实际上将指向方法区域中的存储器地址。
2.Stack--即堆栈
每个线程都有自己的堆栈,它为在该线程上执行的每个方法分配一个帧。 堆栈是一个后进先出(LIFO)数据结构,因此当前执行的方法位于堆栈的顶部。 为每个方法调用创建一个新帧并将其添加(推送)到堆栈顶部。 当方法正常返回或如果在方法调用期间抛出未捕获的异常时,帧将被删除(弹出)。 栈不是直接操作的,除了push和pop帧对象,因此帧对象可以在Heap中分配,并且内存不需要是连续的。
3.Native Stack--即本地栈
不是所有的JVM都支持本地方法,但是,通常创建一个每线程本地方法栈。 如果已经使用用于Java本地调用(JNI)的C链接模型来实现JVM,则本地栈将是C栈。 在这种情况下,参数和返回值的顺序在本地堆栈中将与典型的C程序相同。 本地方法通常(取决于JVM实现)回调到JVM并调用Java方法。 这样的本地到Java调用将发生在堆栈(正常的Java堆栈); 线程将离开本地栈并在栈上创建一个新的帧(正常的Java栈)。
4.Stack Restrictions--即堆栈限制
堆栈可以是动态或固定大小。 如果一个线程需要一个比允许的栈更大的空间将抛出stackOverflowError。 如果一个线程需要一个新的帧,没有足够的内存来分配它,那么抛出一个OutOfMemoryError。
5.Frame--帧
为每个方法调用创建一个新帧并将其添加(推送)到堆栈顶部。 当方法正常返回或如果在方法调用期间抛出未捕获的异常时,帧将被删除(弹出)
每个帧包括
- Local variable array--即局部变量数组
- Return value--即返回值
- Operand stack--即操作数栈
- Reference to runtime constant pool for class of the current method--即当前方法的类的运行时常量池的引用
Local Variables Array
局部变量数组包含执行方法期间使用的所有变量,包括当前对调用该方法类实例的引用,所有方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(slot)为最小单位,每个 slot 保证能放下 32 位内的数据类型 ,虚拟机通过索引定位的方式使用局部变量表,索引值从 0 开始。值得注意的是,对于实例方法,局部变量表中第 0 位索引的槽(slot)默认是 this 引用;静态方法则不是。而且为了节约内存,slot 是可以重用的。
局部变量可以是boolean byte char long short int float double reference(引用) returnAddress(所有类型都在局部变量数组中使用一个槽,除了long和double,它们都需要两个连续的槽,因为这些类型是双宽度(64位而不是32位))
Operand Stack
在执行字节代码指令期间以类似于在本地CPU中使用通用寄存器的方式使用操作数堆栈。 大多数JVM字节码花费时间通过推,弹出,复制,交换或执行产生或消耗值的操作来操作操作数堆栈。 因此,在字节代码中非常频繁地在局部变量数组和操作数堆栈之间移动值的指令。
例如,简单的变量初始化导致与操作数栈交互的两个字节代码。
int i;
获取编译成以下字节码:
0: iconst_0 // Push 0 to top of the operand stack 把常量0放入操作数栈
1: istore_1 // Pop value from top of operand stack and store as local variable 1 把栈顶的元素出栈,存到局部变量表索引为1的位置
0 iconst_0 //把常量0放入栈 1 istore_1 //把栈顶的元素出栈,存到局部变量表索引为1的位置
+--------+--------+ +--------+--------+
| local | stack | | local | stack |
+-----------------+ +-----------------+
| | 0 | | 0 | |
+-----------------+ +-----------------+
| | | | | |
+--------+--------+ +--------+--------+
Dynamic Linking--即动态链接
每个帧包含对运行时常量池的引用。 引用指向正在为该帧执行的方法的类的常量池。 此引用有助于支持动态链接。
C / C ++代码通常编译为对象文件,然后将多个对象文件链接在一起,以产生可用的工件,如可执行文件或dll。 在链接阶段期间,每个目标文件中的符号引用被相对于最终可执行文件的实际存储器地址替换。 在Java中,这个链接阶段在运行时动态完成。
当编译Java类时,对变量和方法的所有引用都作为符号引用存储在类的常量池中。符号引用是逻辑引用,而不是实际指向物理内存位置的引用。 JVM实现可以选择何时解析符号引用,这可能发生在类文件被验证时,加载后,称为eager或静态解析,而这可能发生在第一次使用符号引用时称为延迟或延迟解析。然而,JVM必须表现得好像解析发生在每次引用被首次使用时,并且在这一点上抛出任何解析错误。绑定是由直接引用替代的符号引用标识的字段,方法或类的过程,这只发生一次,因为符号引用被完全替换。如果符号引用指的是一个尚未解析的类,那么这个类将被加载。每个直接引用被存储为对与变量或方法的运行时位置相关联的存储结构的偏移。
Shared Between Threads--即线程之间共享
Heap--即堆
Heap用于在运行时分配类实例和数组。 数组和对象永远不能存储在堆栈中,因为帧被设计的是不能在创建后改变大小。 该帧只存储指向堆上对象或数组的引用。 与局部变量数组中的基本变量和引用(在每个帧中)不同,对象总是存储在堆上,所以当方法结束时它们不会被移除。 相反,对象只能被垃圾回收器删除。
为了支持垃圾回收,堆分为三个部分:
Young Generation--即年轻代(通常被分为Eden 和 Survivor)
Old Generation--即老年代 (也被称为Tenured Generation)
Permanent Generation--即是指内存的永久保存区域(主要存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域. 它和和存放Instance的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。)
Memory Management--即内存管理
对象和数组永远不会显式地取消分配,而是垃圾回收器自动回收它们。
通常这个流程如下:
1.新对象和数组创建到年轻代(Young)
2.Minor garbage collection(小型垃圾收集)将在年轻代中运行。 仍然存在的对象将从eden空间移动到Survivor空间。
3.Major garbage collection(主要的垃圾收集),通常导致应用程序线程暂停,将移动对象在年轻代与老年代之间。 对象,仍然活着,将从年轻代移动到老年(终身)代。
4.每次收集老年代(old)时也会去收集永久代(Permanent Generation)。 他们会被收集当任意其中一个变满了。
Non-Heap Memory--即非堆内存
这些对象在逻辑上被认为是JVM机制的一部分但是不是在堆上创建的。
Non-Heap Memory包含如下内容:
1.Permanent Generation包括
the method area--即方法区
interned strings
2.Code Cache--用于编译和存储已由JIT编译器编译为本地代码的方法
Just In Time (JIT) Compilation--即时编译器技术
Java字节码被解释,但是这不像在JVM的主机CPU上直接执行本地代码那么快。 为了提高性能,Oracle Hotspot VM会查找定期执行的字节代码的“热”区域,并将其编译为本地代码。 然后,本地代码存储在非堆内存中的代码高速缓存中。 这样,Hotspot VM尝试选择最合适的方式来折衷编译代码所需的额外时间,以及执行解释代码所需的额外时间。
Method Area--方法区
方法区域存储每个类的信息,例如:
1.Classloader Reference--类加载器引用
2.Run Time Constant Pool--运行时常量池(包括 Numeric constants--数字常量 Field references--字段引用 Method References--方法引用 Attributes--属性)
3.Field data--字段数据(Per field--每个字段里有这些东西(name--名称 type--类型 Modifiers--修饰符 Attributes--属性))
4.Method data--方法数据(Per method--每个方法里有这些东西(name--名称 return type--返回类型 Parameter Types (in order)--参数类型(按顺序) Modifiers--修饰符 Attributes--属性))
5.Method code--方法代码
每个方法代码包含如下东西:
Bytecodes--字节码
Operand stack size--操作数栈大小
Local variable size--局部变量大小
Local variable table--局部变量表
Exception table--异常表
每个异常处理程序又包含如下:
Start point--起点
End point--终点
PC offset for handler code--处理程序代码的PC(程序计数器)偏移量
Constant pool index for exception class being caught--捕获的异常类的常量池索引
所有线程共享相同的方法区域,因此访问方法区域数据和动态链接的过程必须是线程安全的。 如果两个线程尝试访问尚未加载的类的字段或方法,则它只能加载一次,并且两个线程都必须在加载之前不继续执行。
class文件结构
编译后的类文件由以下结构组成:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_pool[constant_pool_count – 1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
package org.jvminternals; public class SimpleClass { public void sayHello() {
System.out.println("Hello");
} }
然后,运行javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class,您会得到以下输出:
public class org.jvminternals.SimpleClass
SourceFile: "SimpleClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // "Hello"
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // org/jvminternals/SimpleClass
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/jvminternals/SimpleClass;
#14 = Utf8 sayHello
#15 = Utf8 SourceFile
#16 = Utf8 SimpleClass.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 Hello
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 org/jvminternals/SimpleClass
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
{
public org.jvminternals.SimpleClass();
Signature: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/jvminternals/SimpleClass; public void sayHello();
Signature: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String "Hello"
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/jvminternals/SimpleClass;
}
这个类文件显示了三个主要部分:常量池,构造函数和sayHello方法。
Constant Pool--这提供了符号表通常提供的相同信息
Methods--每个包含四个区域:
signature and access flags--签名和访问标志
byte code--字节码
LineNumberTable--这向调试器提供信息以指示哪一行对应于哪个字节代码指令,例如Java代码中的行6对应于sayHello方法中的字节代码0,行7对应于字节代码8。
LocalVariableTable--这列出了在帧中提供的所有局部变量,在这两个例子中,唯一的局部变量是this。
在此类文件中使用以下字节代码操作数
aload_0 --- 此操作码是格式为aload_ <n>的一组操作码中的一个。 它们都将对象引用加载到操作数堆栈中。 <n>是指被访问的局部变量数组中的位置,但只能是0,1,2或3.还有其他类似的操作码用于装载不是对象引用的值iload_ <n>,lload_ < n>,float_ <n>和dload_ <n>其中i为int,l为long,f为float,d为double。 索引高于3的局部变量可以使用iload,lload,float,dload和aload加载。 这些操作码都采用单个操作数,指定要加载的局部变量的索引。
ldc --- 此操作码用于将常量从运行时常量池推入操作数堆栈。
getstatic --- 此操作码用于将静态值从运行时常量池中列出的静态字段推入操作数堆栈。
invokespecial, invokevirtual --- 这些操作码是在一组操作码中调用invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual的方法。 在这个类文件中,invokespecial和invokevirutal都被使用,它们之间的区别是invokevirutal调用基于对象类的方法。 invokespecial指令用于调用实例初始化方法以及当前类的超类的私有方法和方法。
return --- 这个操作码在一组操作码ireturn,lreturn,freturn,dreturn,areturn和返回。 这些操作码中的每一个都是返回不同类型的返回语句,其中i是用于int,l是用于long,f是用于float,d用于double,a用于对象引用。 没有前导类型字母return的操作码只返回void。
在任何典型的字节代码中,大多数操作数与局部变量,操作数栈和运行时常数池交互如下。
构造函数有两个指令,首先将this推到操作数堆栈上,接下来调用超类的构造函数,消耗掉this值,因此将其从操作数堆栈中弹出。
sayHello()方法更复杂,因为它必须使用运行时常数池来解析对实际引用的符号引用,如上面更详细解释的。 第一个操作数getstatic用于将对静态字段的引用从系统类推送到操作数堆栈。 下一个操作数ldc将字符串“Hello”推送到操作数堆栈。 最后一个操作数invokevirtual调用System.out的println方法,它将操作数堆栈中的“Hello”作为参数,并为当前线程创建一个新的帧。
Classloader--即类加载器
JVM通过使用bootstrap classloader加载初始类来启动。 然后在调用public static void main(String [])之前,链接和初始化类。 该方法的执行将依次驱动其他类和接口的加载,链接和初始化。
Loading--即加载--加载是找到类文件的过程,它表示具有特定名称的类或接口类型,并将其读入字节数组。 接下来,字节被解析以确认它们表示一个Class对象并具有正确的major and minor versions。 任何被命名为直接超类的类或接口也被加载。 一旦this被创建(创建在栈内存里面),加载器就会从二进制码加载一个对象(或者接口)。
Linking--即链接--链接是采取类或接口验证和准备类型及其直接超类和超级接口的过程。 链接由验证,准备和可选解决的三个步骤组成。
Verifying--即验证--验证是确认类或接口表示在结构上正确并遵守Java编程语言和JVM的语义要求的过程,例如执行以下检查:
1.consistent and correctly formatted symbol table--一致和正确格式的符号表
2.final methods / classes not overridden--最终方法/类不被覆盖
3.methods respect access control keywords--方法遵守访问控制关键字
4.methods have correct number and type of parameters--方法具有正确的数量和类型的参数
5.bytecode doesn't manipulate stack incorrectly--字节码不能正确处理堆栈
6.variables are initialized before being read--变量在被读取之前被初始化
7.variables are a value of the correct type--变量是正确类型的值
在验证阶段执行这些检查意味着不需要在运行时执行这些检查。 链接期间的验证减慢类加载,但是它避免了在执行字节码时需要执行这些检查。
Preparing--即准备--准备涉及为静态存储和JVM使用的任何数据结构(例如方法表)分配内存。 创建静态字段并将其初始化为其默认值,但是,在初始化阶段不会执行初始化程序或代码。
Resolving--Resolving是一个可选的阶段,它涉及通过加载引用的类或接口来检查符号引用,并检查引用是否正确。 如果这在此时不发生,则符号引用的分辨率可以推迟到它们由字节代码指令使用之前。
类或接口的初始化包括执行类或接口初始化方法<clinit>
在JVM中有多个具有不同角色的类装入器。 每个类加载器委托给它的父类加载器(去加载它),除了bootstrap classloader,它是顶级类加载器。
Bootstrap Classloader--Bootstrap类加载器通常实现为本地代码,因为它在JVM加载的早期实例化。 Bootstrap类加载器负责加载基本的Java API(java的核心类,在Sun的JVM中,在执行java的命令中使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定附加的类),包括例如rt.jar。 它只加载在启动类路径(boot classpath)上找到的具有较高信任级别的类; 因此它跳过了对普通类进行的大部分验证。
Extension Classloader--Extension Classloader从标准Java扩展API(如安全扩展功能)加载类(它负债加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包。)。
System Classloader--System Classloader是默认的应用程序类加载器,它从类路径加载应用程序类(-classpath或者java.class.path系统属性或者CLASSPATH*作系统属性所指定的JAR类包和类路径)。
User Defined Classloaders--用户定义的类加载器也可以用于加载应用程序类。 用户定义的类加载器用于许多特殊原因,包括类的运行时重新加载或通常由诸如Tomcat之类的web服务器所需的不同加载类之间的分隔。
Faster Class Loading
从5.0版本的HotSpot JMV中引入了一个称为类数据共享(CDS)的功能。 在JVM的安装过程中,安装程序将一组关键JVM类(如rt.jar)加载到内存映射共享存档中。 CDS减少加载这些类所需的时间,从而提高JVM启动速度,并允许这些类在JVM的不同实例之间共享,从而减少内存占用。
Where Is The Method Area
Java虚拟机规范Java SE 7版清楚地说明:“虽然方法区域在逻辑上是堆的一部分,但是简单的实现可能选择不是垃圾收集或压缩它。”与此Jconsole的矛盾,Oracle JVM显示了方法 区域(和代码高速缓存)作为非堆。 OpenJDK代码显示CodeCache是VM到ObjectHeap的单独字段。
Classloader Reference
所有加载的类都包含对加载它们的类加载器的引用。 反过来,类加载器还包含对它加载的所有类的引用。
Run Time Constant Pool
JVM维护每个类型常量池,即类似于符号表的运行时数据结构,尽管它包含更多数据。 Java中的字节代码需要数据,通常此数据太大,无法直接存储在字节代码中,而是存储在常量池中,字节代码包含对常量池的引用。 如上所述,运行时常量池用于动态链接
几种类型的数据存储在常量池中
- numeric literals--
- string literals--
- class references--类引用
- field references--字段引用
- method references--方法引用
看下面例子:
Object foo = new Object();
将按如下字节码写入:
0: new #2 // Class java/lang/Object
1: dup
2: invokespecial #3 // Method java/ lang/Object "<init>"( ) V
new操作码(操作数代码)后面是#2操作数。该操作数是到常量池的索引,因此引用常量池中的第二个条目。第二个条目是类引用,该条目又将包含类名称的常量池中的另一个条目引用为值为// Class java / lang / Object的常量UTF8字符串。然后,可以使用此符号链接来查找java.lang.Object的类。new操作码创建一个类实例并初始化它的变量。然后将对新类实例的引用添加到操作数堆栈。 dup操作码然后在操作数栈上创建顶部引用的额外副本,并将其添加到操作数堆栈的顶部。最后,通过invokespecial在第2行调用实例初始化方法。该操作数还包含对常量池的引用。初始化方法消耗(弹出)操作数池的顶部引用作为方法的参数。最后,有一个对已创建和初始化的新对象的引用。
如果你编译下面的简单类:
package org.jvminternals; public class SimpleClass { public void sayHello() {
System.out.println("Hello");
} }
生成的类文件中的常量池将如下所示:
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // "Hello"
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // org/jvminternals/SimpleClass
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/jvminternals/SimpleClass;
#14 = Utf8 sayHello
#15 = Utf8 SourceFile
#16 = Utf8 SimpleClass.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 Hello
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 org/jvminternals/SimpleClass
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
常量池包含以下类型:
Exception Table--异常表
异常表存储每个异常处理程序信息,例如:
- Start point--起点
- End point--终点
- PC offset for handler code--处理程序代码的PC(程序计数器)偏移量
- Constant pool index for exception class being caught--捕获的异常类的常量池索引
如果一个方法定义了一个try-catch或一个try-finally异常处理程序,那么将创建一个异常表。 这包含每个异常处理程序或finally块的信息,包括处理程序应用的范围,正在处理的异常类型以及处理程序代码所在的位置。
当抛出异常时,JVM在当前方法中查找匹配的处理程序,如果没有找到匹配的处理程序,那么该方法会突然弹出当前堆栈帧,并在调用方法(新的当前帧)中重新抛出异常。 如果在所有帧都被弹出之前没有找到异常处理程序,则该线程被终止。 这也可能导致JVM本身终止,如果异常被抛出在最后一个非守护线程,例如如果线程是主线程。
最后异常处理程序匹配所有类型的异常,因此总是在抛出异常时执行。 在没有抛出异常的情况下,finally块仍然在方法结束时执行,这是通过在执行return语句之前跳转到finally处理程序代码来实现的。
Symbol Table--符号表
除了每个类型的运行时常量池之外,Hotspot JVM还有一个在永久代(permanent generation)中保存的符号表。 符号表是指向符号的Hashtable映射符号指针(如Hashtable <Symbol *,Symbol>),并且包括指向包括在每个类中的运行时常量池中保存的所有符号的指针。
引用计数用于控制符号何时从符号表中删除。 例如,当类被卸载时,保持在其运行时常数池中的所有符号的引用计数递减。 当符号表中的符号的引用计数变为零时,符号表知道该符号不再被引用,并且符号从符号表中卸载。 对于符号表和字符串表(见下文),所有条目都以规范化形式保存,以提高效率,并确保每个条目只出现一次。
Interned Strings (String Table)--内部字符串(字符串表)
Java语言规范要求包含相同的Unicode代码点序列的相同字符串字面必须引用同一个String实例。 此外,如果在String的实例上调用String.intern(),那么必须返回一个引用,如果该字符串是文字,则该引用将与引用return相同。 因此,以下内容成立:
("j" + "v" + "m").intern() == "jvm"
在Hotspot JVM中,interned string保存在字符串表中,这是一个Hashtable映射对象指向符号的指针(即Hashtable <oop,Symbol>),并且保存在永久代(permanent generation)中。 对于符号表(见上文)和字符串表,所有条目都以规范化形式保存,以提高效率,并确保每个条目只出现一次。
String literals由编译器自动实现,并在加载类时添加到符号表中。 此外,String类的实例可以通过调用String.intern()显式地实现。 当调用String.intern()时,如果符号表已经包含字符串,那么将返回对该字符串的引用,如果没有,则将字符串添加到字符串表中,并返回其引用。
JVM内部体系到这里就已经完了。。。
下面我们来分析下上面的反编译字节码:
首先是FinallyTest的构造器,aload_0是将局部变量数组里的0位置的元素入栈,由下面的LocalVariableTable可以看出0位置的元素是this,然后调用invokespecial,我们知道invokespecial指令用于调用实例初始化方法以及当前类的超类的私有方法和方法。
接着咋们来分析getMap()这个静态方法,首先就是new一个HashMap,dup这个指令是用于栈复制的,在这个地方的作用就是将new的结果复制到栈顶中,主要是为了下面使用invokespecial指令时能知道是调用谁的方法,由后面的invokespecial可知是调用刚才new出来的那个HashMap的构造方法,astore_0并将这个map对象存储到局部变量表的0位置,aload_0将局部变量表中0位置的元素压入操作数栈顶部,接下来的2个ldc就是分别将#4和#10对应的常量值压入操作数栈中,invokeinterface即调用map对象的put方法,pop将操作数栈清空,19-29的操作和先前的一样,只是将常量try压入操作数栈,而不是init,看30和31,先aload_0把局部变量表的0位置的元素map压入栈顶,astore_1把栈顶元素弹出,并存入局部变量数组位置为1的地方,然后再将把局部变量表的0位置的元素map压入栈顶,调用map的put方法将finally存入key对应的value中,清空操作数栈,将null的对象压入栈顶,并存入局部变量数组0的位置,然后再将局部变量数组为1的位置的元素map压入栈顶,返回给其调用者,其中的Exception table里面对应的from和to分别是说从19到32如果抛出异常则调到47.后面以此类推。。。
从上面的分析,我们可以看出,在finally中先把局部变量数组为0的元素复制了一份放在为1的地方,然后null的引用存入局部变量数组为0的地方,最后返回的是局部变量数组为1的元素,由于先前局部变量数组中0和1位置的引用指向同一个内存地址,把finally存入key对应的value是有效的,故最后主线程中打印的值为finally。
Java finally语句到底是在return之前还是之后执行(JVM字节码分析及内部体系结构)?的更多相关文章
- 【转】Java finally语句到底是在return之前还是之后执行?
网上有很多人探讨Java中异常捕获机制try...catch...finally块中的finally语句是不是一定会被执行?很多人都说不是,当然他们的回答是正确的,经过试验,至少有两种情况下final ...
- Java finally语句到底是在return之前还是之后执行
看过网上关于Java中异常捕获机制try-catch-finally块中的finally语句是不是一定会被执行的讨论也有很多. 首先明确一点,下面两种情况finally肯定是不执行的: 1). ret ...
- Java finally语句到底是在return之前还是之后执行?
网上有很多人探讨Java中异常捕获机制try...catch...finally块中的finally语句是不是一定会被执行?很多人都说不是,当然他们的回答是正确的,经过我试验,至少有两种情况下fina ...
- 通过字节码分析java中的switch语句
在一次做题中遇到了switch的问题,由于对switch执行顺序的不了解,在这里简单的通过字节码的方式理解一下switch执行顺序(题目如下): public class Ag{ static pub ...
- Java字节码分析
目录 Java字节码分析 查看字节码详细内容 javap 实例分析 Java字节码分析 对于源码的效率,但从源码来看有时无法分析出准确的结果,因为不同的编译器版本可能会将相同的源码编译成不同的字节码, ...
- JVM源码分析之一个Java进程究竟能创建多少线程
JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...
- Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)
前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析. 一.问题引出 先看一段代码 packa ...
- JVM 字节码指令手册 - 查看 Java 字节码
JVM 字节码指令手册 - 查看 Java 字节码 jdk 进行的编译生成的 .class 是 16 进制数据文件,不利于学习分析.通过下命令 javap -c Demo.class > Dem ...
- JVM源码分析之Java对象头实现
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十一篇. 今天呢!灯塔君跟大家讲: JVM源码分析之Java对象头实现 HotSpot虚拟机中,对象在内存中的布局分为三 ...
随机推荐
- HDu -2844 Coins多重背包
这道题是典型的多重背包的题目,也是最基础的多重背包的题目 题目大意:给定n和m, 其中n为有多少中钱币, m为背包的容量,让你求出在1 - m 之间有多少种价钱的组合,由于这道题价值和重量相等,所以就 ...
- linux中内核的一个不错的参数somaxconn
导读:在linux中,/proc/sys/net/core/somaxconn这个参数,linux中内核的一个不错的参数somaxconn 看下其解析: 对于一个TCP连接,Server与Client ...
- javascript高级特性(面向对象)
javascript高级特性(面向对象): * 面向对象: * 面向对象和面向过程的区别: * 面向对象:人就是对象,年龄\性别就是属性,出生\上学\结婚就是方法. * 面向过程:人出生.上学.工作. ...
- Android Http请求失败解决方法
1.MainActivity.java 文件中的onCreate方法改成如下: @SuppressLint("NewApi") @Override protected void o ...
- Android开发手记(11) 滑动条SeekBar
安卓滑动条的操作特别简单,通过getProgress()可以获得SeekBar的位置,通过setProgress(int progress)可以设置SeekBar的位置.要想动态获取用户对SeekBa ...
- PHP Cookies
PHP Cookies cookie 常用于识别用户. Cookie 是什么? cookie 常用于识别用户.cookie 是一种服务器留在用户计算机上的小文件.每当同一台计算机通过浏览器请求页面时, ...
- JQuery 解析xml
JQuery 可以通过 $.get() 或 $.post() 方法来加载 xml. JQuery 解析 XML 与解析 DOM 一样, 可以使用 find(), children() 等函数来 ...
- C++采用模板实现栈的方法
今天又看了遍<effective C++>,手动实现了一下条款42中的栈,贴出来当博客的处女贴. 首先栈的声明如下,采用了模板传入类型,而栈的底层采用是个链表. // stack.h // ...
- display:inline,display:inline-block,display:block 区别
之前一直迷惑于display:inline/inline-block/block的异同,在度娘谷哥的帮助下,突然有了一点思路. 按照网上的介绍,inline将对象转化为内联元素,block将对象转化为 ...
- .net概述1
1.什么是.net 首先我先说说这个词的读音,很多外行朋友读作"点net"甚至许多圈内朋友也这样读,其实它正确读法应该是读作"dot net",音译即为&quo ...