当编写并运行一个Java程序时,就同时体验了这四种技术。用Java语言编写源代码,编译成Java Class文件,然后再在Java虚拟机上运行class文件。当编写程序时,通过调用类中的方法来访问系统资源。当程序运行的时候,它通过调用class文件中的方法来满足程序的Java API调用。
jvm全称是Java Virtual Machine(java虚拟机)。它之所以被称之为是“虚拟”的,就是因为它仅仅是由一个规范来定义的抽象计算机。我们平时经常使用的Sun HotSpot虚拟机只是其中一个具体的实现(另外还有BEA JRockit、IBM J9等等虚拟机)。在实际的计算机上通过软件来实现一个虚拟计算机。与VMWare等类似软件不同,你是看不到jvm的,它存在于内存。
当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果在同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。
一、jvm体系结构
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。根据《Java虚拟机规范(第2版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图1所示。
图1 Java虚拟机的内部体系结构
下面先对图中各部分做个简单的说明:
1.class文件:虚拟机并不关心Class的来源是什么语言,只要它符合Java class文件格式就可以在Java虚拟机中运行。使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的 编译器一样可以把程序代码编译成Class文件。
2.类装载器子系统:负责查找并装载Class 文件到内存,最终形成可以被虚拟机直接使用的Java类型。
3.方法区:在类装载器加载class文件到内存的过程中,虚拟机会提取其中的类型信息,并将这 些信息存储到方法区。方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。由于所有线程都共享方法区,因此它们对方法 区数据的访问必须被设计为是线程安全的。
4.堆:存储Java程序创建的类实例。所有线程共享,因此设计程序时也要考虑到多线程访问对象(堆数据)的同步问题。
5.Java栈:Java栈是线程私有的。每当启动一个新线程时,Java虚拟机都会为它分配一 个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈或出栈。当线程调用java方法时, 虚拟机压入一个新的栈帧到该线程的java栈中。当方法返回时,这个栈帧被从java栈中弹出并抛弃。一个栈帧包含一个java方法的调用状态,它存储有 局部变量表、操作栈、动态链接、方法出口等信息。
6.程序计数器:一个运行中的Java程序,每当启动一个新线程时,都会为这个新线程创建一个 自己的PC(程序计数器)寄存器。程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取 下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个 计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
7.本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态链接并直接调用指定的本地方法。如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。
8.执行引擎: 负责执行字节码。方法的字节码是由Java虚拟机的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。执行引擎执行字节码 时,首先取得一个操作码,如果操作码有操作数,取得它的操作数。它执行操作码和跟随的操作数规定的动作,然后再取得下一个操作码。这个执行字节码的过程在 线程完成前将一直持续。
下面详细说明下具体构成:
Java语言中所有的基本类型同样也都是Java虚拟机中的基本类型。但boolean有点特别,指令集对boolean只有很有限的支持。当编译器把Java源码编译为字节码的时,它会用int或byte来表示boolean。Boolean数组是当byte数组来访问的。
returnAddress是Java虚拟机内部使用的基本类型,这个类型被用来实现Java程序中的finally子句。
2、类装载器子系统
负责查找并装载的那部分被称为类装载器子系统。
分为启动类装载器和用户自定义类装载器
由不同的类装载器装载的类将放在虚拟机内部的不同命名空间中。
用户自定义的类装载器以及Class类的实例都放在内存的堆区,而装载的类型信息则都位于方法区。
装载顺序:
1)装载——查找并装载类型的二进制数据
2)连接——执行验证(确保被导入类型的正确性),准备(为类变量分配内存,并将其初始化为默认值),以及解析(把类变量中的符号引用转换为正确的初始值)
3)初始化——把类变量初始化为正确的初始值
3方法区
在java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。
所有线程都共享方法区。
类型信息:
这个类型的全限定名
这个类型的直接超类的全限定名
这个类型是类类型还是接口类型
这个类型的访问修饰符
任何直接超接口的全限定名的有序列表
该类型的常量池
字段信息
方法信息
除了常量以外的所有类(静态)变量
一个到类ClassLoader的引用
一个到Class类的引用
其中字段信息包括
字段名
字段类型
字段的修饰符
方法信息包括
方法名
方法的返回类型
方法参数的数量和类型
方法的修饰符
如果方法不是抽象的和本地的还须有
方法的字节码
操作数栈和该方法的栈帧中的局部变量的大小
异常表
4 堆
Java程序在运行时所创建的所有类实例或数组都放在同一个堆中。
Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能快速的定位对象实例的数据,另外,它必须能通过该对象引用访问相应的类数据,因此对象中通常有一个指向方法区的指针。
一种可能的堆空间设计就是,把堆分为两部分:一个句柄池,一个对象池。
这种设计的好处是有利于堆碎片的整理,缺点是每次访问对象的实例变量都需要经过两次指针传递。
堆中其他数据:
1、对象锁,用于协调多个线程访问一个对象时的同步。
2、等待集合
3、与垃圾收集器有关的数据。
4、方法表:加快了调用实例方法时的效率。
方法表指向的实例方法数据包括以下信息:
此方法的操作数栈和局部变量区的大小
此方法的字节码
异常表
这些信息足够虚拟机去调用一个方法了,方法表包含有方法指针——指向类活或超类声明的方法的数据
对于一个运行中的Java程序而言,其中的每一个线程都有它自己的PC(程序计数器),在线程启动时创建。大小是一个字长。当线程执行某个Java方法时,PC的内容总是下一条将被指向指令的“地址”。如果该线程正在执行一个本地方法,那么此时PC的值为”undefined”。
6、Java栈
每当启动一个线程时,Java虚拟机都会为它分配一个Java栈,Java栈以帧为单位保存线程的运行状态,虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈。
某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池,在线程执行一个方法时,它会跟踪当前类和当前常量池。
每当线程调用一个方法时,虚拟机都会在该线程的Java栈中压入一个新帧,而这个新栈自然就成为当前帧。在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等数据。
Java栈上的所有数据都是数据都是此线程私有的。
7、栈帧
栈帧由三部分组成:局部变量区、操作数栈和帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,编译器在编译器时就确定的确定了这些值并放在class文件中。帧数据区的大小依赖于具体的实现。
当虚拟机调用一个方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。
局部变量区:Java栈帧的局部变量区被组织为以一个字长为单位、从0开始计数的数组。字节码指令通过从0开始的索引来使用其中的数据。
局部变量区对应方法的参数和局部变量。编译器首先按声明的顺序把这些参数放入局部变量数组。
在java中,所有的对象都按引用传递,并且都存储在堆中,永远都不会在局部变量区或操作数栈中发现对象的拷贝,只会有对象的引用。
操作数栈:操作数栈也是被组织为一个字长为单位的数组。但它不是通过索引来访问,而是通过标准的栈操作——压栈和出栈来访问的。
帧数据区:支持解析常量池解析、正常方法返回以及异常派发机制。每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。常量池中对类型、字段和方法的引用在开始时都是符号。当虚拟机在常量池中搜索时,如果遇到类、接口、字段或者方法的入口,假若它们仍然是符号,虚拟机那时候才会进行解析。
8、执行引擎
指令集:方法的字节码流是由Java虚拟机的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。操作码本身就已经规定了它是否需要跟随操作数,以及如果有操作数它是什么形式的。当虚拟机执行一条指令的时候,可能使用当前常量池中的项、当前帧的局部变量中的值,或者当前帧操作数栈顶端的值。
执行技术:解释、即时编译、字适应优化、芯片级直接执行。
Hotspot虚拟机就采用了自适应优化。自适应优化虚拟机开始的时候对所有的代码都是解释执行,但是它会监视代码的执行情况。大多数程序花费80%-90%的时间来执行10%-20%的代码。虚拟机可以意识到那些方法是程序的热区——就是那10%-20%的代码,他们占整个执行时间的80%-90%。当自适应优化的虚拟机判断出某个特定的方法是瓶颈的时候,它启动一个后台线程,把字节码编译成本地代码,非常仔细的优化这些本地代码。
Java的安全模型是其多个重要结构特点之一,它使Java成为适于网络环境的技术。因为网络提供了一条攻击连人的计算机的潜在途径,因此安全性是非常重要的。Java安全模型侧重于保护终端用户免受从网络下载的、来自不可靠来源的、恶意程序(以及善意程序中的bug)的侵犯。为了达到这个目的,Java提供了一个用户可配置的“沙箱”,在沙箱中可以放置不可靠的Java程序。
例如:原来在版本1.0中的沙箱对很多不可靠Java applet的活动做了限制,包括:
对本地硬盘的读写操作。
进行任何网络连接,但不能连接到提供这个applect的源主机。
创建新的进程。
装载新的动态连接库。
Java虚拟机装载了一个类,并且对它进行了每一到第三趟的class文件检查,这些字节码就可以被运行了。除了对符号引用的检验,Java虚拟机在执行字节码时还进行了其他一些内置的安全机制的操作:
Class.forName(String className) | 装载并返回该类型的Class对象引用 | 如果不能装载类型,则抛出ClassNotFoundException |
obj.getClass() | 直接通过实例得到类型的Class对象引用 |
getName() | 返回类型的全限定名 | |
getSuperClass() | 得到直接超类 | 如果是Object类型或是一个接口,则返回null |
isInterface() | 判断是否是接口 | 如果是返回true,否则返回false |
getInterfaces() | 得到所有直接接口的数组 | 如果没有实现任何接口,返回长度为0的数组 |
getClassLoader() | 得到装载该类型的类装载器 | 如果类型是由启动类装载器装载的,则返回null |
一、class文件内容
Java class文件是对Java程序二进制文件格式的精确定义。每一个Java class文件都对一个Java类或者Java接口作出了全面描述。一个class文件只 能包含一个类或接口;
ACC_PUBLIC | 0x0001 | public类型 |
ACC_FINAL | 0x0010 | 类为final类型 |
ACC_SUPER | 0x0020 | 使用新型的invokespecial语义 |
ACC_INTERFACE | 0x0200 | 接口类型,不是类类型 |
ACC_ABSTRACT | 0x0400 | abstract类型 |
附:invokespecial和invokevirtual
invokespecial指静态绑定后,由JVM产生调用的方法。如super(),以及super.someMethod(),都属于 invokespecial;
invokevirtual指动态绑定后,由JVM产生调用的方法。如obj.someMethod(),属于invokevirtual;
正是由于这两种绑定的不同,在子类覆盖超类的方法、并向上转型引用后,才产生了多态以及其他特殊的调用结果。
常量池指向类或者接口时,给出的是全限定名,形如java/util/Hashtable;
字段名和方法名以简单名称形式出现在常量池入口中,如上例中的常量池索引3中的"hello";
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
I | int i; |
[Ljava/lang/Object; | java.lang.Object[] obj; |
([BII)Ljava/lang/String; | String method(byte[] b, int i, int j) |
ZILjava/lang/String;II()Z | boolean method(boolean b, int i, String s, int j, int k) |
Young Generation | 即图中的Eden + From Space + To Space 1.Eden存放新生的对象 2.Survivor Space有两个,存放每次垃圾回收后存活的对象 |
Old Generation | Tenured Generation 即图中的Old Space 主要存放应用程序中生命周期长的存活对象 |
Permanent Generation | 即图中的Permanent Space 存放JVM自己的反射对象,比如类对象和方法对象 |
native heap | JVM内部处理或优化 |
-Xms/-Xmx | 定义YOUNG+OLD段的总尺寸,ms为JVM启动时YOUNG+OLD的内存大小;mx为最大可占用的YOUNG+OLD内存大小。 | 默认是物理内存的1/64但小于1G。 | 在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。 |
-XX:NewSize/-XX:MaxNewSize | 定义YOUNG段的尺寸,NewSize为JVM启动时YOUNG的内存大小;MaxNewSize为最大可占用的YOUNG内存大小。 | 默认是物理内存的1/4但小于1G。 | 在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。 |
-Xmn | 设置young generation的内存大小。 | 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 | |
-XX:PermSize/-XX:MaxPermSize | 定义Perm段的尺寸,PermSize为JVM启动时Perm的内存大小;MaxPermSize为最大可占用的Perm内存大小。 | 在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。 | |
-XX:NewRaito | 设置YOUNG与OLD段的比值。 | ||
-XX:SurvivorRaito | 设置YOUNG段中Eden区与Survivor区的比值,如此值为4,则Eden为4/6,两个Survivor分别为1/6。 | ||
-XX:MaxTenuringThreshold | 设置垃圾最大年龄。 | 如果设置为0的话,则新生对象不经过Survivor区,直接进入OLD段。对于OLD对象比较多的应用,可以提高效率。如果将此值设置为一个较大值,则 新生对象会在Survivor区进行多次复制,这样可以增加对象的存活时间,增加在minor collection即被回收的概率。 | |
-Xss | 设置栈的大小。 | JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。 | 在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 |
-XX:+UseParallelGC | 选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。 | ||
-XX:ParallelGCThreads | 配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。 | 此值最好配置与处理器数目相等。 | |
-XX:+UseParallelOldGC | 配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。 | ||
-XX:MaxGCPauseMillis | 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。 | ||
-XX:+UseAdaptiveSizePolicy | 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等。 | 此值建议使用并行收集器时,一直打开。 |
MEM_ARGS="-Xms512m -Xmx512m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=128m -XX:MaxPermSize=128m -XX:SurvivorRatio=6"
上例中,
YOUNG+OLD: 512M
YOUNG: 256M
Perm: 128M
Eden: YOUNG*6/(6+1+1)=192M
单个Survivor: YOUNG/(6+1+1)=32M
Java栈和局部变量操作
Java虚拟机是基于栈的机器,几乎所有Java虚拟机的指令都与操作数栈相关。栈操作包括把常量压入操作数栈、执行通用的栈操作、在操作数栈和局部变量之间往返传输值。
1常量入栈操作:
操作码在执行常量入栈操作之前,使用三种方式指明常量的值:常量值隐含包含在操作码内部、常量值在字节码中如同操作数一样跟随在操作码之后,或者从常量池中取出常量。
1.1常量值隐含包含在操作码内部:
将一个字长的常量压入栈
操作码 |
操作数 |
说明 |
---|---|---|
iconst_m1 |
(无) |
将int类型值-1压入栈 |
iconst_0 |
(无) |
将int类型值0压入栈 |
iconst_1 |
(无) |
将int类型值1压入栈 |
iconst_2 |
(无) |
将int类型值2压入栈 |
iconst_3 |
(无) |
将int类型值3压入栈 |
iconst_4 |
(无) |
将int类型值4压入栈 |
iconst_5 |
(无) |
将int类型值5压入栈 |
fconst_0 |
(无) |
将float类型值0压入栈 |
fconst_1 |
(无) |
将float类型值1压入栈 |
fconst_2 |
(无) |
将float类型值2压入栈 |
将两个字长的常量压入栈
操作码 |
操作数 |
说明 |
---|---|---|
lconst_0 |
(无) |
将long类型值0压入栈 |
lconst_1 |
(无) |
将long类型值1压入栈 |
dconst_0 |
(无) |
将double类型值0压入栈 |
dconst_1 |
(无) |
将double类型值1压入栈 |
给一个对象引用赋空值时会用到aconst_null指令
将空(null)对象引用压入栈
操作码 |
操作数 |
说明 |
---|---|---|
aconst_null |
(无) |
将空(null)对象引用压入栈 |
例如下面代码:
public class StackTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
int i = 0;
int j = 4;
int k;
k = i + j;
float a = 0;
float b = 1;
float c = a + b;
long x = 0;
long y = 1;
long z = x + y;
String string = null;
}
}
用javap工具查看其字节码为:
Compiled from "StackTest.java"
public class StackTest extends java.lang.Object{
public StackTest();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0 //常量int类型的0入栈
1: istore_1 //弹出栈顶元素0存入位置1的局部变量中
2: iconst_4 //常量int类型的4入栈
3: istore_2 //弹出栈顶元素4存入位置2的局部变量中
4: iload_1 //从位置为1的局部变量中取出元素int类型的0压入栈
5: iload_2 //从位置为2的局部变量中取出元素int类型的4压入栈
6: iadd //从栈顶弹出两个元素然后做加法,把结果压入栈
7: istore_3 //弹出栈顶元素4存入位置为3的局部变量中
8: fconst_0 //常量float类型的0入栈
9: fstore 4 //弹出栈顶元素0存入位置为4的局部变量中
11: fconst_1 //常量float类型的1入栈
12: fstore 5 //弹出栈顶元素1存入位置为5的局部变量中
14: fload 4 //从位置为4的局部变量中取出元素float类型的0压入栈
16: fload 5 //从位置为5的局部变量中取出元素float类型的1压入栈
18: fadd //从栈顶弹出两个元素然后做加法,把结果压入栈
19: fstore 6 //弹出栈顶元素1存入位置为3的局部变量中
21: lconst_0 //常量long类型的0入栈
22: lstore 7 // 弹出栈顶元素0存入位置为7和8的局部变量中
24: lconst_1 //常量long类型的1入栈
25: lstore 9 // 弹出栈顶元素0存入位置为9和10的局部变量中
27: lload 7 //从位置为7和8的局部变量中取出元素long类型的0压入栈
29: lload 9 //从位置为9和10的局部变量中取出元素long类型的1压入栈
31: ladd //从栈顶弹出两个元素然后做加法,把结果压入栈
32: lstore 11 //弹出栈顶元素1存入位置为11和12的局部变量中
34: aconst_null //将null对象引用压入栈
35: astore 13 //弹出栈顶元素null存入位置为13的局部变量中
37: return
}
1.2常量值在字节码中跟随在操作码之后:
将byte和short类型常量压入栈
操作码 |
操作数 |
说明 |
---|---|---|
bipush |
一个byte类型的数 |
将byte类型的数转换为int类型的数,然后压入栈 |
sipush |
一个short类型的数 |
将short类型的数转换为int类型的数,然后压入栈 |
1.3从常量池中取出常量
操作码 |
操作数 |
说明 |
---|---|---|
ldc |
无符号8位数indexbyte |
从由indexbyte指向的常量池入口中取出一个字长的值,然后将其压入栈 |
ldc_w |
无符号16位数indexshort |
从由indexshort指向的常量池入口中取出一个字长的值,然后将其压入栈 |
ldc2_w |
无符号16位数indexshort |
从由indexshort指向的常量池入口中取出两个字长的值,然后将其压入栈 |
这三个操作码是从常量池中取出常量,然后将其压入栈,这些操作码的操作码表示常量池索引,Java虚拟机通过给定的索引查找相应的常量池入口,决定这些常量的类型和值,并把它们压入栈。
常量池索引是一个无符号值,ldc和ldc_w是把一个字长的项压入栈,区别在于:ldc的索引只有一个8位,只能指向常量池中1~255范围的位置。ldc_w的索引有16位,可以指向1~65535范围的位置。
例如下面代码:
public class StackTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
byte i = 125;
byte j = -128;
int k = i + j;
short a = 32767;
short b = - 32768;
int c = a + b;
int x = 2147483647;
int y = -2147483648;
int z = x + y;
long I = 2147483648L;
long J = -2147483649L;
long K = I + J;
}
}
用javap工具查看其字节码为:
Compiled from "StackTest.java"
public class StackTest extends java.lang.Object{
public StackTest();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 125 //将byte类型的255转换成int类型压入栈
2: istore_1 //弹出栈顶元素255存入位置为1的局部变量中
3: bipush -128 //将byte类型的-128转换成int类型压入栈
5: istore_2 //弹出栈顶元素-128存入位置为2的局部变量中
6: iload_1 //取出位置为1的局部变量中的数压入栈
7: iload_2 //取出位置为2的局部变量中的数压入栈
8: iadd //从栈顶弹出两个元素然后做加法,把结果压入栈
9: istore_3 //弹出栈顶元素存入位置为3的局部变量中
10: sipush 32767 //将short类型的32767转换成int类型压入栈
13: istore 4 //弹出栈顶元素32767存入位置为4的局部变量中
15: sipush -32768 /将short类型的-32768转换成int类型压入栈
18: istore 5 //弹出栈顶元素-32768存入位置为5的局部变量中
20: iload 4 //取出位置为4的局部变量中的数压入栈
22: iload 5 //取出位置为5的局部变量中的数压入栈
24: iadd //从栈顶弹出两个元素然后做加法,把结果压入栈
25: istore 6 /弹出栈顶元素存入位置为6的局部变量中
27: ldc #16; //int 2147483647 //从常量池索引16的位置取出2147483647压入栈
29: istore 7 //弹出栈顶元素2147483647存入位置为4的局部变量中
31: ldc #17; //int -2147483648 //从常量池索引17的位置取出-2147483648压入栈
33: istore 8 //弹出栈顶元素-2147483648存入位置为8的局部变量中
35: iload 7 //取出位置为7的局部变量中的数压入栈
37: iload 8 //取出位置为8的局部变量中的数压入栈
39: iadd //从栈顶弹出两个元素然后做加法,把结果压入栈
40: istore 9 //弹出栈顶元素存入位置为9的局部变量中
42: ldc2_w #18; //long 2147483648l //从常量池索引18的位置取出long类型的2147483648L压入栈
45: lstore 10 //弹出栈顶元素2147483648L存入位置为10和11的局部变量中
47: ldc2_w #20; //long -2147483649l //从常量池索引20的位置取出long类型的-2147483649L压入栈
50: lstore 12 //弹出栈顶元素-2147483649L存入位置为12和13的局部变量中
52: lload 10 //取出位置为10和11的局部变量中的数压入栈
54: lload 12 //取出位置为12和13的局部变量中的数压入栈
56: ladd //从栈顶弹出两个元素然后做加法,把结果压入栈
57: lstore 14 //弹出栈顶元素存入位置为14和15的局部变量中
59: return
}
2通用栈操作
操作码 |
操作数 |
说明 |
---|---|---|
nop |
(无) |
不做任何操作 |
pop |
(无) |
从操作数栈弹出栈顶部的一个字 |
pop2 |
(无) |
从操作数栈弹出最顶端的两个字 |
swap |
(无) |
交换栈顶部的两个字 |
dup |
(无) |
复制栈顶部的一个字 |
dup2 |
(无) |
复制栈顶部的两个字 |
dup_x1 |
(无) |
复制栈顶部的一个字,并将复制内容及原来弹出的两个字长的内容压入栈 |
dup_x2 |
(无) |
复制栈顶部的一个字,并将复制内容及原来弹出的三个字长的内容压入栈 |
dup2_x1 |
(无) |
复制栈顶部的两个字,并将复制内容及原来弹出的三个字长的内容压入栈 |
dup2_x2 |
(无) |
复制栈顶部的两个字,并将复制内容及原来弹出的四个字长的内容压入栈 |
1,dup:复制栈顶部的一个字长的内容。
栈:
前:......,word
后:......,word,word
2,dup_x1:复制栈顶部一个字长的内容,然后将复制内容及原来弹出的两个字长的内容压入栈
栈:
前:......,word2,word1
后:......,word1,word2,word1
3,dup_x2:复制栈顶部一个字长的内容,然后将复制内容及原来弹出的三个字长的内容压入栈
栈:
前:.......,word3,word2,word1
后:.......,word1,word3,word2,word1
4,dup2:复制栈顶部长度为两个字长的内容
栈:
前:......,word2,word1
后:......,word2,word1,word2,word1
5,dup2_x1:复制栈顶部两个字长的内容,然后将复制内容及原来弹出的三个字长的内容压入栈
栈:
前:......,word3,word2,word1
后:.......,word2,word1,word3,word2,word1
6,dup2_x2:复制栈顶部两个字长的内容,然后将复制内容及原来弹出的四个字长的内容压入栈
栈:
前:......,word4,word3,word2,word1
后:.......,word2,word1,word4,word3,word2,word1
7,pop:弹出栈顶端一个字长的内容
栈:
前:......,word
后:.......
8,pop2:弹出栈顶端两个字长的内容
栈:
前:......,word2,word1
后:.......
9,swap:交换栈顶端两个字的内容
栈:
前:......,word2,word1
后:.......,word1,word2
例如如下代码:
public class StackTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
String a;
String b;
a = new String("aaa");
b = new String("aaa");
}
}
用javap工具查看其字节码为:
Compiled from "StackTest.java"
public class StackTest extends java.lang.Object{
public StackTest();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16; //class java/lang/String
3: dup
4: ldc #18; //String aaa
6: invokespecial #20; //Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #16; //class java/lang/String
13: dup
14: ldc #18; //String aaa
16: invokespecial #20; //Method java/lang/String."<init>":(Ljava/lang/String;)V
19: astore_2
20: return
}
3,把局部变量压入栈
将一个字长的局部变量压入栈
操作码 |
操作数 |
说明 |
---|---|---|
iload |
vindex |
将位置为vindex的int类型的局部变量压入栈 |
iload_0 |
(无) |
将位置为0的int类型的局部变量压入栈 |
iload_1 |
(无) |
将位置为1的int类型的局部变量压入栈 |
iload_2 |
(无) |
将位置为2的int类型的局部变量压入栈 |
iload_3 |
(无) |
将位置为3的int类型的局部变量压入栈 |
fload |
vindex |
将位置为vindex的float类型的局部变量压入栈 |
fload_0 |
(无) |
将位置为0的float类型的局部变量压入栈 |
fload_1 |
(无) |
将位置为1的float类型的局部变量压入栈 |
fload_2 |
(无) |
将位置为2的float类型的局部变量压入栈 |
fload_3 |
(无) |
将位置为3的float类型的局部变量压入栈 |
将两个字长的局部变量压入栈
操作码 |
操作数 |
说明 |
---|---|---|
lload |
vindex |
将位置为vindex和(vindex+1)的long类型的局部变量压入栈 |
lload_0 |
(无) |
将位置为0和1的long类型的局部变量压入栈 |
lload_1 |
(无) |
将位置为1和2的long类型的局部变量压入栈 |
lload_2 |
(无) |
将位置为2和3的long类型的局部变量压入栈 |
lload_3 |
(无) |
将位置为3和4的long类型的局部变量压入栈 |
dload |
vindex |
将位置为vindex和(vindex+1)的double类型的局部变量压入栈 |
dload_0 |
(无) |
将位置为0和1的double类型的局部变量压入栈 |
dload_1 |
(无) |
将位置为1和2的double类型的局部变量压入栈 |
dload_2 |
(无) |
将位置为2和3double类型的局部变量压入栈 |
dload_3 |
(无) |
将位置为3和4double类型的局部变量压入栈 |
将对象引用局部变量压入栈
操作码 |
操作数 |
说明 |
---|---|---|
aload |
vindex |
将位置为vindex的对象引用局部变量压入栈 |
aload_0 |
(无) |
将位置为0的对象引用局部变量压入栈 |
aload_1 |
(无) |
将位置为1的对象引用局部变量压入栈 |
aload_2 |
(无) |
将位置为2的对象引用局部变量压入栈 |
aload_3 |
(无) |
将位置为3的对象引用局部变量压入栈 |
4,弹出栈顶元素,将其赋给局部变量
弹出一个字长的值,将其赋给局部变量
操作码 |
操作数 |
说明 |
---|---|---|
istore |
vindex |
从栈中弹出int类型值,然后将其存到位置为vindex的局部变量中 |
istore_0 |
(无) |
从栈中弹出int类型值,然后将其存到位置为0的局部变量中 |
istore_1 |
(无) |
从栈中弹出int类型值,然后将其存到位置为1的局部变量中 |
istore_2 |
(无) |
从栈中弹出int类型值,然后将其存到位置为2的局部变量中 |
istore_3 |
(无) |
从栈中弹出int类型值,然后将其存到位置为3的局部变量中 |
fstore |
vindex |
从栈中弹出float类型值,然后将其存到位置为vindex的局部变量中 |
fstore_0 |
(无) |
从栈中弹出float类型值,然后将其存到位置为0的局部变量中 |
fstore_1 |
(无) |
从栈中弹出float类型值,然后将其存到位置为1的局部变量中 |
fstore_2 |
(无) |
从栈中弹出float类型值,然后将其存到位置为2的局部变量中 |
fstore_3 |
(无) |
从栈中弹出float类型值,然后将其存到位置为3的局部变量中 |
弹出对象引用,并将其赋值给局部变量
操作码 |
操作数 |
说明 |
---|---|---|
lstore |
vindex |
从栈中弹出long类型值,然后将其存到位置为vindex和(vindex+1)的局部变量中 |
lstore_0 |
(无) |
从栈中弹出long类型值,然后将其存到位置为0和1的局部变量中 |
lstore_1 |
(无) |
从栈中弹出long类型值,然后将其存到位置为1和2的局部变量中 |
lstore_2 |
(无) |
从栈中弹出long类型值,然后将其存到位置为2和3的局部变量中 |
lstore_3 |
(无) |
从栈中弹出long类型值,然后将其存到位置为3和4的局部变量中 |
dstore |
vindex |
从栈中弹出double类型值,然后将其存到位置为vindex和(vindex+1)的局部变量中 |
dstore_0 |
(无) |
从栈中弹出double类型值,然后将其存到位置为0和1的局部变量中 |
dstore_1 |
(无) |
从栈中弹出double类型值,然后将其存到位置为1和2的局部变量中 |
dstore_2 |
(无) |
从栈中弹出double类型值,然后将其存到位置为2和3的局部变量中 |
dstore_3 |
(无) |
从栈中弹出double类型值,然后将其存到位置为3和4的局部变量中 |
操作码 |
操作数 |
说明 |
---|---|---|
astore |
vindex |
从栈中弹出对象引用,然后将其存到位置为vindex的局部变量中 |
astore_0 |
(无) |
从栈中弹出对象引用,然后将其存到位置为0的局部变量中 |
astore_1 |
(无) |
从栈中弹出对象引用,然后将其存到位置为1的局部变量中 |
astore_2 |
(无) |
从栈中弹出对象引用,然后将其存到位置为2的局部变量中 |
astore_3 |
(无) |
从栈中弹出对象引用,然后将其存到位置为3的局部变量中 |
5,wide指令
无符号8位局部变量索引,把方法中局部变量数的限制在256以下。一条单独的wide指令可以将8位的索引再扩展8位,就可以把局部变量数的限制扩展到65536.
操作码 |
操作数 |
说明 |
---|---|---|
wide |
iload,index |
从局部变量位置为index的地方取出int类型值,并将其压入栈 |
wide |
lload ,index |
从局部变量位置为index的地方取出long类型值,并将其压入栈 |
wide |
fload,index |
从局部变量位置为index的地方取出float类型值,并将其压入栈 |
wide |
dload,index |
从局部变量位置为index的地方取出double类型值,并将其压入栈 |
wide |
aload,index |
从局部变量位置为index的地方取出对象引用,并将其压入栈 |
wide |
istore,index |
从栈中弹出int类型值,将其存入位置为index的局部变量中 |
wide |
lstore,index |
从栈中弹出long类型值,将其存入位置为index的局部变量中 |
wide |
fstore,index |
从栈中弹出float类型值,将其存入位置为index的局部变量中 |
wide |
dstore,index |
从栈中弹出double类型值,将其存入位置为index的局部变量中 |
wide |
astore,index |
从栈中弹出对象引用,将其存入位置为index的局部变量中 |
跳转指令并不允许直接跳转到被wide指令修改过的操作码。
Java虚拟机包括许多进行基本类型转换工作的操作码,这些执行转换工作的操作码后面没有操作数,转换的值从栈顶断获得。Java虚拟机从栈顶端弹出一个值,对它进行转换,然后再把转换结果压入栈。
int、long、float、double 类型之间的相互转换
操作码 |
操作数 |
说明 |
i2l |
(无) |
将int类型的值转换为long类类型 |
i2f |
(无) |
将int类型的值转换为float类类型 |
i2d |
(无) |
将int类型的值转换为double类类型 |
l2i |
(无) |
将long类型的值转换为int类类型 |
l2f |
(无) |
将long类型的值转换为float类类型 |
l2d |
(无) |
将long类型的值转换为double类类型 |
f2i |
(无) |
将float类型的值转换为int类类型 |
f2l |
(无) |
将float类型的值转换为long类类型 |
f2d |
(无) |
将float类型的值转换为double类类型 |
d2i |
(无) |
将double类型的值转换为int类类型 |
d2l |
(无) |
将double类型的值转换为long类类型 |
d2f |
(无) |
将double类型的值转换为float类类型 |
int数据类型向byte、char、short类型的转换
操作码 |
操作数 |
说明 |
i2b |
(无) |
将int类型值转换为byte类型值 |
i2c |
(无) |
将int类型值转换为char类型值 |
i2s |
(无) |
将int类型值转换为short类型值 |
i2b指令将弹出的int类型值截取为byte类型,然后再对其进行带符号扩展,恢复成int类型压入栈。
i2c指令将弹出的int类型值截取为char类型,然后再对其进行零扩展,恢复成int类型压入栈
i2s将弹出的int类型值截取为short类型,然后再对其进行带符号扩展,恢复成int类型压入栈
不存在把比int类型值占据更小空间的数据类型转换成int类型的操作码。因为任何byte、char、short类型值在压入栈的时候,就已经有效的转换成int类型了,涉及到byte、char、short类型的运算操作首先都把这些值转化成int类型,然后对int类型值进行运算,最后得到int类型的结果。
例如如下代码:
public class ConvertTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
byte a = 1;
byte b = 1;
byte c = (byte)(a + b);
}
}
用javap工具查看其字节码指令:
Compiled from "ConvertTest.java"
public class ConvertTest extends java.lang.Object{
public ConvertTest();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1 //常量1入栈
1: istore_1 //弹出栈顶元素存入位置为1的局部变量
2: iconst_1 //常量1入栈
3: istore_2 //弹出栈顶元素存入位置为2的局部变量
4: iload_1 //取出位置1的局部变量的值入栈
5: iload_2 //取出位置2的局部变量的值入栈
6: iadd //弹出栈顶两个元素做加法,然后结果入栈
7: i2b //转化成byte类型
8: istore_3 //弹出栈顶元素存入位置为3的局部变量
9: return
}
Java虚拟机提供几种进行整数算术运算的操作码,他们执行基于int和long类型的运算。当byte、short和char类型值参与算术运算时,首先会将它们转换为int类型。这些操作码都不会抛出异常,溢出在这里通常可以被忽略。
整数加法
操作码 |
操作数 |
说明 |
iadd |
(无) |
从栈中弹出两个int类型数,相加,然后将所得int类型结果压回栈 |
ladd |
(无) |
从栈中弹出两个long类型数,相加,然后将所得long类型结果压回栈 |
将一个常量与局部变量相加
操作码 |
操作数 |
说明 |
iinc |
vindex,const |
把常量与一个位于vindex位置的int类型局部变量相加 |
wide |
iinc,index,const |
把常量与一个位于vindex位置的int类型局部变量相加 |
iinc可以用来给局部变量赋值-128~127之间的值。这条操作码与用于控制循环执行的变量的加减相比,效率更高。加上wide可以用来把常量的范围增大到-32768~32767。
整数减法
操作码 |
操作数 |
说明 |
isub |
(无) |
从栈中弹出两个int类型数,相减,然后将所得int类型结果压回栈 |
lsub |
(无) |
从栈中弹出两个long类型数,相减,然后将所得long类型结果压回栈 |
从栈顶弹出两个数,顶端的充当减数,次顶端的充当被减数。
整数乘法
操作码 |
操作数 |
说明 |
imul |
(无) |
从栈中弹出两个int类型数,相乘,然后将所得int类型结果压回栈 |
lmul |
(无) |
从栈中弹出两个long类型数,相乘,然后将所得long类型结果压回栈 |
整数除法
操作码 |
操作数 |
说明 |
idiv |
(无) |
从栈中弹出两个int类型数,相除,然后将所得int类型结果压回栈 |
ldiv |
(无) |
从栈中弹出两个long类型数,相除,然后将所得long类型结果压回栈 |
从栈顶弹出两个数,次顶端的数除以顶端的数(首先被压入栈的数作为被除数或者分子,其次被压入的数作为除数或者分母)。如果整数被0除,会抛出ArithmeticException异常。
整数取余
操作码 |
操作数 |
说明 |
irem |
(无) |
从栈中弹出两个int类型数,相减,然后将所得int类型余数压回栈 |
lrem |
(无) |
从栈中弹出两个long类型数,相减,然后将所得long类型余数压回栈 |
如果整数被0除,会抛出ArithmeticException异常。
操作码 |
操作数 |
说明 |
ineg |
(无) |
从栈中弹出两个int类型数,取反,然后将所得int类型结果压回栈 |
lneg |
(无) |
从栈中弹出两个long类型数,取反,然后将所得long类型结果压回栈 |
1、针对对象的操作码
实例化一个新对象需要通过new操作码来实现。
对象的创建
操作码 |
操作数 |
说明 |
new |
index |
在堆中创建一个新的对象,将其引用压入栈 |
new操作码后面紧跟一个无符号16位数,表示常量池中的一个索引。在特定偏移量位置处的常量池入口给出了新对象所属类的信息。如果还没有这些信息,那么虚拟机会解析这个常量池入口。它会为这个堆中的对象建立一个新的实例,用默认初始化对象实例变量,然后把新对象的引用压入栈。
存取实例变量
操作码 |
操作数 |
说明 |
putfield |
index |
设置对象字段(由index指定)的值,值value和对象引用objectref均从栈中获得 |
getfield |
index |
将对象字段(由index指定)压入栈,对象引用objectref栈中取得 |
存取类变量
操作码 |
操作数 |
说明 |
putstatic |
index |
设置静态字段(由index指定)的值,值value从栈中获得 |
getstatic |
index |
将静态字段(由index指定)压入栈 |
putfield和getfield这两个操作码只在字段是实例变量的情况下才执行,putstatic和getstatic对静态变量进行存取操作。操作数表示常量池的索引。这个索引所指向的常量池入口包含了该字段的所属类、名字和类型等信息。如果还没有这些信息,虚拟机会解析这个常量池入口。
例如下面代码:
public class TestA {
int x;
int y;
}
public class TestMain {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
TestA testA = new TestA();
testA.x = 3;
testA.y = 4;
}
}
用javap工具查看其字节码指令为:
Compiled from "TestMain.java"
public class TestMain extends java.lang.Object{
public TestMain();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16; //class TestA 新建 TestA对象
3: dup //
4: invokespecial #18; //Method TestA."<init>":()V 调用构造方法
7: astore_1 //存入位置为1的局部变量
8: aload_1 //取出位置为1的局部变量压入栈
9: iconst_3 //常量3入栈
10: putfield #19; //Field TestA.x:I 赋值
13: aload_1
14: iconst_4
15: putfield #23; //Field TestA.y:I
18: return
}
2、针对数组的操作码
创建数组
操作码 |
操作数 |
说明 |
newarray |
atype |
从栈中弹出数组长度,使用atype所指定的基本数据类型分配新数组,将数组的对象引用压入栈 |
anewarray |
index |
从栈中弹出数组长度,是哟index所指定的类分配新对象数组,将新数组的对象引用压入栈 |
multianewarray |
index,dimensions |
从栈中弹出数组的维数,使用由index所指定的类分配新多维数组,将新数组的对象引用压入栈 |
atype的值
数组类型 |
atype |
T_BOOLEAN |
4 |
T_CHAR |
5 |
T_FLOAT |
6 |
T_DOUBLE |
7 |
T_BYTE |
8 |
T_SHORT |
9 |
T_INT |
10 |
T_LONG |
11 |
需要注意的是,当数组类型显示声明为boolean类型时,Java虚拟机中创建数组的指令会以位为单位进行操作。无论虚拟机对于boolean数组使用哪一种内部实现,都会使用存取byte数组元素的操作码访问boolean数组的元素。
获取数组长度
操作码 |
操作数 |
说明 |
arraylength |
(无) |
从栈中弹出一个数组的对象引用,将数组长度压入栈 |
arraylength指令从栈顶端弹出一个数组引用,然后把这个数组的长度压入栈。
获取数组元素,虚拟机从栈中弹出数组的索引和数组引用,再将位于给定数组的指定索引位置压入栈。
获取数组元素
操作码 |
操作数 |
说明 |
baload |
(无) |
将byte类型或者boolean类型的数组的索引index和数组引用arrayref弹出栈,将arrayref[index]压入栈 |
caload |
(无) |
将char类型的数组的索引index和数组引用arrayref弹出栈,将arrayref[index]压入栈 |
saload |
(无) |
将short类型的数组的索引index和数组引用arrayref弹出栈,将arrayref[index]压入栈 |
iaload |
(无) |
将int类型的数组的索引index和数组引用arrayref弹出栈,将arrayref[index]压入栈 |
laload |
(无) |
将long类型的数组的索引index和数组引用arrayref弹出栈,将arrayref[index]压入栈 |
faload |
(无) |
将float类型的数组的索引index和数组引用arrayref弹出栈,将arrayref[index]压入栈 |
daload |
(无) |
将double类型的数组的索引index和数组引用arrayref弹出栈,将arrayref[index]压入栈 |
aaload |
(无) |
将对象引用类型的数组的索引index和数组引用arrayref弹出栈,将arrayref[index]压入栈 |
操作码 |
操作数 |
说明 |
bastore |
(无) |
将byte类型或者boolean类型的值、数组的索引index和数组引用arrayref弹出栈,赋值为arrayref[index] = value |
castore |
(无) |
将char类型的值value、数组的索引index和数组引用arrayref弹出栈,赋值为arrayref[index] = value |
sastore |
(无) |
将short类型的值value、数组的索引index和数组引用arrayref弹出栈,赋值为arrayref[index] = value |
iastore |
(无) |
将int类型的数组的值value、索引index和数组引用arrayref弹出栈,赋值为arrayref[index] = value |
lastore |
(无) |
将long类型的值value、数组的索引index和数组引用arrayref弹出栈,赋值为arrayref[index] = value |
fastore |
(无) |
将float类型的值value、数组的索引index和数组引用arrayref弹出栈,赋值为arrayref[index] = value |
dastore |
(无) |
将double类型的值value、数组的索引index和数组引用arrayref弹出栈,赋值为arrayref[index] = value |
aastore |
(无) |
将对象引用类型的值value、数组的索引index和数组引用arrayref弹出栈,,赋值为arrayref[index] = value |
例如如下代码:
public class TestMain {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
int a[] = new int[3];
for (int i = 0 ; i < 3;i++){
a[i] = i;
}
}
}
用javap工具查看其字节码为:
Compiled from "TestMain.java"
public class TestMain extends java.lang.Object{
public TestMain();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_3
1: newarray int
3: astore_1
4: iconst_0
5: istore_2
6: goto 16
9: aload_1
10: iload_2
11: iload_2
12: iastore
13: iinc 2, 1
16: iload_2
17: iconst_3
18: if_icmplt 9
21: return
}
本章主要介绍字节码实现的finally子句。包括相关指令以及这些指令的使用方式。此外,本章还介绍了Java源代码中finally子句所展示的一些令人惊讶的特性,并从字节码角度对这些特征进行了解释。
1、微型子例程
字节码中的finally子句表现的很像“微型子例程”。Java虚拟机在每个try语句块和与其相关的catch子句的结尾处都会“调用”finally子句的子例程。finally子句结束后(这里的结束指的是finally子句中最后一条语句正常执行完毕,不包括抛出异常,或执行return、continue、break等情况),隶属于这个finally子句的微型子例程执行“返回”操作。程序在第一次调用微型子例程的地方继续执行后面的语句。
Java方法与微型子例程使用不同的指令集。跳转到微型子例程的指令是jsr或者jsr_w,将返回地址压入栈。执行完毕后调用ret指令。ret指令并不会从栈中弹出返回地址,而是在子例程开始的时候将返回地址从栈顶取出存储在局部变量,ret指令从局部变量中取出。这是因为finally子句本身会抛出异常或者含有return、break、continue等语句。finally确保会执行到,即使try或者catch中有return等语句。
先看看下面的一道面试题:
int normal(){ try{ return 10; }finally{ return 20; } }
public class TryFinallyTest{ private static int normal(){ try{ return 10; }finally{ return 20; } } public static void main(String args[]){ System.out.println(normal()); } }
Compiled from "TryFinallyTest.java" public class TryFinallyTest extends java.lang.Object{ public TryFinallyTest(); Code: 0:aload_0 1:invokespecial#1; //Method java/lang/Object."<init>":()V 4:return private static int normal(); Code: 0:bipush10 2:istore_1 3:bipush20 5:ireturn 6:astore_2 7:bipush20 9:ireturn Exception table: from to target type 0 3 6 any 6 7 6 any //0-2 对应try子句,“3-5”及"7-9"表示finally子例程,而6表示编译器生成的catch处理过程的开端(这可以通过加粗的异常表看到)。 //注意我们没有看到关于return 10的任何信息,该语句已经被忽略了。 public static void main(java.lang.String[]); Code: 0:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream; 3:invokestatic#3; //Method normal:()I 6:invokevirtual#4; //Method java/io/PrintStream.println:(I)V 9:return }
class Surprise { static boolean surpriseTheProgrammer(boolean bVal) { while (bVal) { try { return true; } finally { break; } } return false; } }
Compiled from "Surprise.java" class Surprise extends java.lang.Object{ Surprise(); Code: 0: aload_0 1: invokespecial #8; //Method java/lang/Object."<init>":()V 4: return static boolean surpriseTheProgrammer(boolean); Code: 0: iload_0 1: ifeq 8 4: goto 8 7: pop 8: iconst_0 9: ireturn Exception table: from to target type 4 7 7 any }
private static int normal() { int a; try { a = 1; System.out.println("in try:set a to " + a); return a; } finally { a = 2; System.out.println("in finally:set a to " + a); } }
in try:set a to 1 in finally:set a to 2 1
private static int normal(); Code: 0:iconst_1 1:istore_0 //对应a=1(现在栈中压入常量1,再弹出存储到局部变量_0即a中) 2:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream; 5:new#3; //class java/lang/StringBuilder 8:dup 9:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V 12:ldc#5; //String in try:set a to 14:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17:iload_0 18:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 21:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V //9-24对应try块中的打印语句 27:iload_0 28:istore_1 //令人困惑的原因在这里-----编译器在try块中把变量a的值存在了标号为1的局部变量中,再跳到语句56我们就明白了,try中返回的是标号1的局部变量的值,而不是修改过的值。 29:iconst_2 30:istore_0 31:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream; 34:new#3; //class java/lang/StringBuilder 37:dup 38:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V 41:ldc#10; //String in finally:set a to 43:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 46:iload_0 47:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 50:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 53:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V //29-53对应finally块 56:iload_1 57:ireturn //56-57对应try中的return a. 58:astore_2 59:iconst_2 60:istore_0 //59-60对应finally中的a=2; 61:getstatic#2; //Field java/lang/System.out:Ljava/io/PrintStream; 64:new#3; //class java/lang/StringBuilder 67:dup 68:invokespecial#4; //Method java/lang/StringBuilder."<init>":()V 71:ldc#10; //String in finally:set a to 73:invokevirtual#6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 76:iload_0 77:invokevirtual#7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 80:invokevirtual#8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 83:invokevirtual#9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 86:aload_2 87:athrow Exception table: from to target type 0 29 58 any 58 59 58 any
1.对多线程的支持是Java语言的一大优势,Java对线程的控制主要集中在对线程的同步和协作上,Java使用的同步机制是监视器。
监视器
java监视器支持两种线程:互斥和协作。
java虚拟机通过对象锁来实现互斥,允许多个线程在同一个共享数据上独立而不干扰地工作。
协作则通过object类的wait方法和notify方法来实现,允许多个线程为了同一个目标而共同工作。
我们将监视器比作一个建筑物,里面有很多房间,房间里面有一些数据,并且同一个时间只能被一个线程占据。一个线程进入房间到离开为止,独占其中全部的数据。
进入这个数据叫做进入监视器。
进入房间叫做获得监视器。
占据房间叫做持有监视器。
离开房间叫做释放监视器。
离开建筑叫做退出监视器。
互斥原理
对一个监视器来说监视区域是最小的,不可分割的代码块。同一个监视器中,监视区域只会被一个线程执行。
当一个线程到达了一个监视区域的开始处,它就会放置到监视器的入口区。如果没有其他线程等待,它就会进入监视器并且获得监视器,当执行完监视区域的代码后,就会释放并退出监视器。
如果当前监视器正在被另外一个线程持有,那么它就会进入一个等待队列(如果有其他线程也在等待进入监视器)。当持有线程退出之后,只能有等待队列中的一个线程进入并持有该监视器,其他的线程仍旧留会在等待队列中。
协作原理
当一个线程需要一些特别的数据,而由另一个线程负责改变这些数据的状态时,同步显得特别重要。
eg,一个读线程会由一个缓冲区中读取数据,而当前这个缓存区是空的,就需要一个写线程写入数据,当写数据完成写入,读线程才能做读取操作。
java虚拟机使用的这种监视器叫做等待并唤醒监视器。
这种监视器中,一个已经持有该监视器的线程,可以通过调用等待命令,暂停自身的执行。当这个线程执行等待命令后,就会释放该监视器并进入一个等待区,并会一直在那里持续等待状态,直到一段时间后该监视器内的其他线程调用了唤醒命令。当一个线程调用唤醒命令后,它会持续持有监视器,直到它主动释放监视器。当执行了唤醒命令或者执行完监视区域,释放监视器后,等待线程会苏醒,其中的一个会重新获得监视器,判断状态条件,以便决定是否继续继续进入等待状态或者退出或者执行监视区域。
Java虚拟机中的这种监视器模型如图20-1所示,将监视器分成了三个区域。中间的大方框包括一个单独的线程,是监视器的持有者;左边小的方框中时入口区;右边另一个小方框是等待区。活动线程用深灰色圆画出,暂停的线程用灰色圆画出。
对象锁
java虚拟机的一些运行时数据区会被所有线程共享,其他的数据是各个线程私有的。
因为堆和方法区是被所有线程共享的,java程序需要为两种多线程访问数据进行协调。
1)保存在堆中的实例变量
2)保存在方法区中的类变量
程序不需要协调保存在java栈中的局部变量,因为java栈中的数据是属于拥有该栈的线程私有的。
在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。对于对象来说,相关联的监视器保护对象的实例变量。对于类来说,监视器保护类的类变量。如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。
为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁(有时候被称为互斥提(mutex))。一个锁就像一种任何时候只允许一个线程“拥有”的特权。线程访问实例变量或者类变量不需要获取锁。但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)
类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。
一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减1,当计数器值为0时,锁就被完全释放了。
java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。在java程序中,只需要使用同步语句或者同步方法就可以标志一个监视区域。当每次进入一个监视区域时,java虚拟机都会自动锁上对象或者类。