之前看了一篇关于“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文件结构
编译后的类文件由以下结构组成:
1 ClassFile { 2 u4 magic; 3 u2 minor_version; 4 u2 major_version; 5 u2 constant_pool_count; 6 cp_info contant_pool[constant_pool_count – 1]; 7 u2 access_flags; 8 u2 this_class; 9 u2 super_class; 10 u2 interfaces_count; 11 u2 interfaces[interfaces_count]; 12 u2 fields_count; 13 field_info fields[fields_count]; 14 u2 methods_count; 15 method_info methods[methods_count]; 16 u2 attributes_count; 17 attribute_info attributes[attributes_count]; 18 }
1 package org.jvminternals; 2 3 public class SimpleClass { 4 5 public void sayHello() { 6 System.out.println("Hello"); 7 } 8 9 }
然后,运行javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class,您会得到以下输出:
1 public class org.jvminternals.SimpleClass 2 SourceFile: "SimpleClass.java" 3 minor version: 0 4 major version: 51 5 flags: ACC_PUBLIC, ACC_SUPER 6 Constant pool: 7 #1 = Methodref #6.#17 // java/lang/Object."<init>":()V 8 #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; 9 #3 = String #20 // "Hello" 10 #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V 11 #5 = Class #23 // org/jvminternals/SimpleClass 12 #6 = Class #24 // java/lang/Object 13 #7 = Utf8 <init> 14 #8 = Utf8 ()V 15 #9 = Utf8 Code 16 #10 = Utf8 LineNumberTable 17 #11 = Utf8 LocalVariableTable 18 #12 = Utf8 this 19 #13 = Utf8 Lorg/jvminternals/SimpleClass; 20 #14 = Utf8 sayHello 21 #15 = Utf8 SourceFile 22 #16 = Utf8 SimpleClass.java 23 #17 = NameAndType #7:#8 // "<init>":()V 24 #18 = Class #25 // java/lang/System 25 #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; 26 #20 = Utf8 Hello 27 #21 = Class #28 // java/io/PrintStream 28 #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V 29 #23 = Utf8 org/jvminternals/SimpleClass 30 #24 = Utf8 java/lang/Object 31 #25 = Utf8 java/lang/System 32 #26 = Utf8 out 33 #27 = Utf8 Ljava/io/PrintStream; 34 #28 = Utf8 java/io/PrintStream 35 #29 = Utf8 println 36 #30 = Utf8 (Ljava/lang/String;)V 37 { 38 public org.jvminternals.SimpleClass(); 39 Signature: ()V 40 flags: ACC_PUBLIC 41 Code: 42 stack=1, locals=1, args_size=1 43 0: aload_0 44 1: invokespecial #1 // Method java/lang/Object."<init>":()V 45 4: return 46 LineNumberTable: 47 line 3: 0 48 LocalVariableTable: 49 Start Length Slot Name Signature 50 0 5 0 this Lorg/jvminternals/SimpleClass; 51 52 public void sayHello(); 53 Signature: ()V 54 flags: ACC_PUBLIC 55 Code: 56 stack=2, locals=1, args_size=1 57 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 58 3: ldc #3 // String "Hello" 59 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 60 8: return 61 LineNumberTable: 62 line 6: 0 63 line 7: 8 64 LocalVariableTable: 65 Start Length Slot Name Signature 66 0 9 0 this Lorg/jvminternals/SimpleClass; 67 }
这个类文件显示了三个主要部分:常量池,构造函数和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--方法引用
看下面例子:
1 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行调用实例初始化方法。该操作数还包含对常量池的引用。初始化方法消耗(弹出)操作数池的顶部引用作为方法的参数。最后,有一个对已创建和初始化的新对象的引用。
如果你编译下面的简单类:
1 package org.jvminternals; 2 3 public class SimpleClass { 4 5 public void sayHello() { 6 System.out.println("Hello"); 7 } 8 9 }
生成的类文件中的常量池将如下所示:
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。