java虚拟机学习笔记

一、体系结构组成

当编写并运行一个Java程序时,就同时体验了这四种技术。用Java语言编写源代码,编译成Java Class文件,然后再在Java虚拟机上运行class文件。当编写程序时,通过调用类中的方法来访问系统资源。当程序运行的时候,它通过调用class文件中的方法来满足程序的Java API调用。

  1. Java程序设计语言
  2. Java Class文件格式
  3. Java应用编程接口
  4. Java虚拟机

java虚拟机学习笔记_第1张图片

二、Java虚拟机
  1. Java虚拟机的主要任务是装载class文件并执行其中的字节码。JVM包含一个类装载器,它可以从程序和API中装class文件。Java API中只有程序执行时需要的那些类才会被装载。
  2. 当JVM是由主机操作系统上的软件实现的时候,Java程序通过调用本地方法(native方法)和主机交互。Java中有两种方法:
    • Java方法:由Java语言编写的,编译成字节码,存储在class文件中,是平台无关的;
    • 本地方法:由其它语言(C,C++或汇编语言)编写的,编译成和处理器相关的机器代码,保存在动态链接库中,格式是各个平台专有的,是平台相关的;当编写一个平台独立的Java程序时,必须遵守的一条最重要的原则就是:不要直接或间接调用不属于Java API的本地方法;一个平台无关的Java程序如图:
      java虚拟机学习笔记_第2张图片


    • 运行中的程序调用本地方法时,JVM装载包含这个本地方法的动态库,并调用这个方法;
三、类装载器
  1. 一个Java应用程序可以使用两种类装载器:"启动"(bootstrap)类装载器和用户自定义的类装载器。启动类装载器(这是系统中唯一的)是JVM的一部分。
  2. Java应用程序能够在运行时安装用户定义的类装载器,这种类装载器能够使用自定义的方式来装载类,例如从网络下载class文件。
  3. 用户定义的类装载器使得在运行时扩展Java应用程序成为可能。当它运行时,应用程序能够决定它需要哪些额外的类,能够决定是使用一个或是更多的用户定义的类装载器来装载。由于类装载器是使用Java编写的,所以能用任何在Java中可以表述的风格来进行类的装载。这些类可以通过网络下载,可以从某些数据库中获取,甚至可以动态生成。
  4. 每一个类被装载的时候,JVM都监视这个类,看它到底是被启动类装载器还是被用户定义类装载器装载。当被装载的类引用了另外一个类时,JVM就会使用装载第一个类的类装载器装载被引用的类。这样,两个类就动态地建立起了联系。
  5. 由于JVM采取这种方式进行类的装载,所以被装载的类默认情况下只能看到被同一个类装载器装载的别的类。被不同的类装载器装载的类存放在不同的命名空间中,它们不能互相访问,除非应用程序显式地允许这么做。通过这种方法,就能够使用Java类装载器的体系结构来控制任何从不同源文件中装载的代码之间的相互影响,特别是能够阻止恶意代码获取访问和破坏善意代码的权限。
  6. Java类装载器体系结构如图:
    java虚拟机学习笔记_第3张图片


四、Java独有的特性
  1. 在Java中没有通过使用强制转换指针类型或者通过进行指针运算直接访问内存的方法;
  2. Java避免无意间破坏内存的另一个办法是自动垃圾收集;在Java中,只需要停止对一个对象的引用,一段时间后,垃圾收集器会自动回收这个对象所占用的内存;
  3. Java在运行时保护内存完整性的第三个办法是数组边界检查;
  4. 最后一个关于Java确保程序健壮性的例子是对对象引用的检查,每次使用引用的时候,Java都会确保这些引用不为空值;

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所示。

java虚拟机学习笔记_第4张图片

 

图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个或多个操作数。执行引擎执行字节码 时,首先取得一个操作码,如果操作码有操作数,取得它的操作数。它执行操作码和跟随的操作数规定的动作,然后再取得下一个操作码。这个执行字节码的过程在 线程完成前将一直持续。

下面详细说明下具体构成:

1、数据类型

java虚拟机学习笔记_第5张图片

Java语言中所有的基本类型同样也都是Java虚拟机中的基本类型。但boolean有点特别,指令集对boolean只有很有限的支持。当编译器把Java源码编译为字节码的时,它会用intbyte来表示booleanBoolean数组是当byte数组来访问的。

returnAddressJava虚拟机内部使用的基本类型,这个类型被用来实现Java程序中的finally子句。

2、类装载器子系统

负责查找并装载的那部分被称为类装载器子系统。

分为启动类装载器和用户自定义类装载器

由不同的类装载器装载的类将放在虚拟机内部的不同命名空间中。

用户自定义的类装载器以及Class类的实例都放在内存的堆区,而装载的类型信息则都位于方法区。

装载顺序:

1)装载——查找并装载类型的二进制数据

2)连接——执行验证(确保被导入类型的正确性),准备(为类变量分配内存,并将其初始化为默认值),以及解析(把类变量中的符号引用转换为正确的初始值)

3)初始化——把类变量初始化为正确的初始值

3方法区

java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。

所有线程都共享方法区。

类型信息:

这个类型的全限定名

这个类型的直接超类的全限定名

这个类型是类类型还是接口类型

这个类型的访问修饰符

任何直接超接口的全限定名的有序列表

该类型的常量池

字段信息

方法信息

除了常量以外的所有类(静态)变量

一个到类ClassLoader的引用

一个到Class类的引用

其中字段信息包括

字段名

字段类型

字段的修饰符

方法信息包括

方法名

方法的返回类型

方法参数的数量和类型

方法的修饰符

如果方法不是抽象的和本地的还须有

方法的字节码

操作数栈和该方法的栈帧中的局部变量的大小

异常表

Java程序在运行时所创建的所有类实例或数组都放在同一个堆中。

Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能快速的定位对象实例的数据,另外,它必须能通过该对象引用访问相应的类数据,因此对象中通常有一个指向方法区的指针。

一种可能的堆空间设计就是,把堆分为两部分:一个句柄池,一个对象池。

这种设计的好处是有利于堆碎片的整理,缺点是每次访问对象的实例变量都需要经过两次指针传递。

java虚拟机学习笔记_第6张图片

另一种设计方式是使对象直接指向一组数据,而数据包括对象实例数据以及指向方法区类数据的指针。这种设计方式的优点是只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。

java虚拟机学习笔记_第7张图片

堆中其他数据:

1、对象锁,用于协调多个线程访问一个对象时的同步。

2、等待集合

3、与垃圾收集器有关的数据。

4、方法表:加快了调用实例方法时的效率。

方法表指向的实例方法数据包括以下信息:

此方法的操作数栈和局部变量区的大小

此方法的字节码

异常表

这些信息足够虚拟机去调用一个方法了,方法表包含有方法指针——指向类活或超类声明的方法的数据

java虚拟机学习笔记_第8张图片
5、程序计数器


对于一个运行中的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%。当自适应优化的虚拟机判断出某个特定的方法是瓶颈的时候,它启动一个后台线程,把字节码编译成本地代码,非常仔细的优化这些本地代码。

3.1为什么需要安全性

    Java的安全模型是其多个重要结构特点之一,它使Java成为适于网络环境的技术。因为网络提供了一条攻击连人的计算机的潜在途径,因此安全性是非常重要的。Java安全模型侧重于保护终端用户免受从网络下载的、来自不可靠来源的、恶意程序(以及善意程序中的bug)的侵犯。为了达到这个目的,Java提供了一个用户可配置的“沙箱”,在沙箱中可以放置不可靠的Java程序。

例如:原来在版本1.0中的沙箱对很多不可靠Java applet的活动做了限制,包括:

对本地硬盘的读写操作。

进行任何网络连接,但不能连接到提供这个applect的源主机。

创建新的进程。

装载新的动态连接库。

一、基本沙箱
  1. 组成沙箱的基本组件有:
    • 类装载器结构;
    • class文件校验器;
    • 内置于Java虚拟机(及语言)的安全特性;
    • 安全管理器及Java API;
  2. Java的沙箱安全模型,最重要的优点之一就是这些组件中的类装载器和安全管理器是可以由用户定制的;
二、类装载器体系结构
  1. 类装载器体系结构是Java沙箱中的第一道防线;
  2. 类装载器体系结构在三个方面对Java的沙箱起作用:
    • 它防止恶意代码去干涉善意的代码,这是通过为由不同的类装载器装入的类提供不同的命名空间来实现的;在Java虚拟机中,同一个命名空间内的类可以直接进行交互,而不同的命名空间中的类甚至不能察觉彼此的存在,除非显示地提供了允许它们进行交互的机制;如下图所示,类装载器1装载了Class1和Class2,方法区中存放了Class1和Class2的类型数据,但类装载器2装载了Class1和Class3,因此方法区中存放了Class1和Class3的类型数据;此时命名空间1中的Class1与命名空间2中的Class1关联到方法区中的类型数据有两份;
      java虚拟机学习笔记_第9张图片
    • 它守护了信任类库的边界,这是通过分别使用不同的类装载器装载可靠的包和不可靠的包来实现的;在版本1.2中,类装载器请求另一个类装载器来装载类型的过程被形式化,称为"双亲委派模式",如果一个类装载器的双亲类装载器有能力来装载这个类型,则这个类装载器返回这个类型,否则这个类装载器试图自己来装载这个类型。
      类装载器的双亲委派模式如下:
      java虚拟机学习笔记_第10张图片
      1. 启动(Bootstrap)类装载器:启动类装载器是用本地代码实现的类装载器,它负责将 <Java_Runtime_Home>/lib 下面的类库加载到内存中。由于启动类装载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类装载器的引用,所以不允许直接通过引用进行操作。
      2. 标准扩展(Extension)类装载器:标准扩展类装载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类装载器。
      3. 类路径(ClassPath)类装载器:类路径类装载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
      4. 在有双亲委派模式的情况下,启动类装载器可以抢在标准扩展类装载器之前去装载类,而标准扩展类装载器可以抢在类路径类装载器之前去装载那个类,类路径类装载器又可以抢在用户自定义类装载器之前去装载它,用这种方法,类装载器的体系结构就可以防止不可靠的代码用它们自己的版本来替代可信任的类。
    • 它将代码归入某类(称为保护域),该类确定了代码可以进行哪些操作;
三、class文件检验器
  1. 和类装载器一起,class文件检验器保证装载的class文件内容有正确的内部结构,并且这些class文件相互协调一致。
  2. class文件检验器要进行四趟独立的扫描来完成它的操作:
    • class文件的结构检查:在类被装载时进行的,它必须遵从Java的class文件的固定格式,这样它才能被正确编译成在方法区中的内部数据结构;以下三趟扫描均在方法区中数据结构上进行;
    • 类型数据的语义检查:查看方法区中的数据结构中的每个组成部分,确认每个方法描述符都是符合特定语法的、格式正确的字符串;
    • 字节码验证:对字节流进行数据流分析,这些字节流代表的是类的方法,它是由被称为操作码的单字节指令组成的序列,每一个操作码后都跟一个或多个操作数;执行字节码时,依次执行每个操作码,这就在Java虚拟机内构成了执行的线程。字节码验证确保采用任何路径在字节码流中都得到一个确定的操作码,确保操作数栈总是包含正确的数值以及正确的类型;
    • 符号引用的验证:在动态链接的过程中,如果包含在一个class文件中的符号引用被解析时,class文件检验器将进行符号引用的检查;动态链接是一个将符号引用解析为直接引用的过程。当Java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向别一个类的符号引用,那到JVM就必须解析这个符号引用。在解析时,JVM执行两个基本任务:
      1. 查找被引用的类,如果必要的话,装载它;
      2. 将符号引用替换为直接引用;
四、内置于Java虚拟机(及语言)的安全特性

Java虚拟机装载了一个类,并且对它进行了每一到第三趟的class文件检查,这些字节码就可以被运行了。除了对符号引用的检验,Java虚拟机在执行字节码时还进行了其他一些内置的安全机制的操作:

  1. 类型安全的引用转换;
  2. 结构化的内存访问;
  3. 自动垃圾收集;
  4. 数组边界检查;
  5. 空引用检查;
五、安全管理器及API
    1. Java模型的前三个部分(类装载器体系结构、class文件检验器以及Java中内置的安全特性),保持Java虚拟机的实例和它正在运行的应用程序的内部完整性,使得它们不被下载的恶意或有漏洞的代码侵犯;而安全管理器它主要是保护虚拟机的外部资源不被虚拟机内运行的恶意或有漏洞的代码侵犯;
    2. 当Java API进行任何可能不安全的操作时,它都会向安全管理器请求许可,从而强制执行自定义的安全策略;
      一、JVM的生命周期
      1. 当启动一个Java程序时,一个Java虚拟机实例就诞生了;当该程序关闭退出时,这个Java虚拟机也就随之消亡;
      2. JVM实例通过调用某个初始类的main方法来运行一个Java程序;这个main方法必须是public、static的,而且返回值必须是void;任何一个拥有这样的main方法的类都可以作为Java程序运行的起点;
      3. Java程序初始类中的main方法,将作为该程序初始线程的起点,其它任何线程都是由这个初始线程启动的;
      4. 守护线程和非守护线程
        • 守护线程通常是由虚拟机自己使用的,比如执行垃圾收集任务的线程;
        • Java程序可以把它任何创建的线程标记为守护线程;
        • Java初始线程(即开始于main方法的线程)是非守护线程;
        • 只要还有任何非守护线程在运行,那么这个Java程序也在运行,即这个JVM实例还存活着;当JVM中的所有非守护线程都终止时,JVM实例将自动退出;
      二、JVM的体系结构
        1. JVM体系结构如图所示
          java虚拟机学习笔记_第11张图片
          • 方法区和堆是所有线程共享的;
          • 当虚拟机装载一个class文件时,会从二进制数据中解析类型信息,然后把类型信息放到方法区中;
          • 当程序运行时,虚拟机会把所有该程序在运行时创建的对象放在堆中;如下所示:
            java虚拟机学习笔记_第12张图片
          • 当每一个新线程被创建时,将得到自己的PC寄存器和Java栈;
            如果线程正在执行的是一个Java方法,则PC寄存器的值总是指向下一条将被执行的指令;而它的Java栈则总是存储该线程中Java方法的调用状态(局部变量、参数、返回值以及中间结果);
            如果线程正在执行的是本地方法,则是以依赖于具体实现的方式存储在本地方法栈、寄存器或是其它内存区中;
          • Java栈是由许多栈桢组成的,一个栈桢包含一个方法的调用状态;当线程调用一个Java方法时,虚拟机压入一个新的栈桢到该线程的Java栈中;当该方法返回时,这个栈桢将从Java栈中弹出;
        2. 数据类型
          • Java 语言中的所有基本类型同样也都是Java虚拟机中的基本类型,但boolean有点特别,虽然Java虚拟机也把boolean当作基本类型,但是指令集 对boolean,只有很有限的支持:当编译器把Java源码编译为字节码时,它会用int或byte来表示boolean。在Java虚拟机 中,false是由整数零表示的,所有非零整数都表示true。涉及boolean的值的操作则会使用int。另外,boolean数据是当做byte数组来访问的,但是在堆区,它也可以被表示为位域;
          • Java虚拟机中还有一个只在内部使用的基本类型:returnAddress,不能在程序开发时使用这个类型,它被用来实现Java程序中的finally子句;
          • Java虚拟机有三种引用类型:
            • 类类型:对类实例的引用;
            • 接口类型:对实现了该接口的某个类实例的引用;
            • 数组类型:对数组对象的引用;在Java虚拟机中,数组是个真正的对象;
        3. 装载、连接、初始化
          • 装载:查找并装载类型的二进制数据;
          • 连接:执行验证、准备以及解析,解析是可选的;
            • 验证:确保被导入类型的正确性;
            • 准备:为类变量分配内存,并初始化为默认值;
            • 解析:把类型中的符号引用转换为直接引用;
          • 初始化:把类变量初始化为正确的初始值;
        4. 方法区
          • 当虚拟机装载某个类时,它使用类装载器定位相应的class文件,然后读入class文件中的线性二进制流,提取其中的类型信息,并将其存储到方法区;该类型中的类变量、静态变量也存储到方法区中;
          • 所有线程共享方法区,因此它们对方法区数据的访问必须设计成线程安全的;
          • 类型信息
            • 全限定名;
            • 直接超类的全限定名(如果这个类是Object类,则没有超类);
            • 是类类型还是接口类型;
            • 访问修饰符(public、abstract或final的某个子集);
            • 任何直接接口的全限定名的有限列表;
            • 该类型的常量池;
            • 字段信息;
            • 方法信息;
            • 除常量以外的所有类变量、静态变量;
            • 一个到类ClassLoader的引用;
            • 一个到Class类的引用;
          • 常量池:该类型所用常量的一个有序集合,包括直接常量和对其它类型、字段和方法的符号引用;
          • 字段信息:类型的字段,包括声明顺序都要在方法区中保存,如字段名、字段的类型以及字段的修饰符;
          • 方法信息:类型的方法,包括声明顺序都要在方法区中保存,如方法名、返回类型、参数数量,类型,顺序以及方法的修饰符;
            • 如果不是本地方法和抽象方法,还要保存方法的字节码、操作数栈和方法的栈桢中局部变量的大小以及异常表;
          • 类(静态)变量:类变量由所有类实例共享,即使没有任何类实例,它也可以被访问,因此它们总是作为类型信息的一部分保存到方法区中;
          • 指向ClassLoader类的引用:每个类被装载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器,则虚拟机必须在类型信息中保存对该类装载器的引用;
            • 虚拟机在动态连接期间使用这个信息,当某个类引用另一个类型时,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型;
            • 这个动态连接的过程,对于虚拟机分离命名空间也很重要;
          • 指向Class类的引用:虚拟机会为每个被装载的类型创建一个Class类的实例,并把这个实例的引用存储在方法区中;
            • 有两个方法得到类型的Class对象引用: 方法 说明 备注
              Class.forName(String className) 装载并返回该类型的Class对象引用 如果不能装载类型,则抛出ClassNotFoundException
              obj.getClass() 直接通过实例得到类型的Class对象引用  
            • 通过类型的Class对象引用,可以访问方法区中的类型信息: 方法 说明 备注
              getName() 返回类型的全限定名  
              getSuperClass() 得到直接超类 如果是Object类型或是一个接口,则返回null
              isInterface() 判断是否是接口 如果是返回true,否则返回false
              getInterfaces() 得到所有直接接口的数组 如果没有实现任何接口,返回长度为0的数组
              getClassLoader() 得到装载该类型的类装载器 如果类型是由启动类装载器装载的,则返回null
          • 方法表:为了提高访问效率,虚拟机实现中可能还包含其它数据结构来加快对原始数据的访问速度,如方法表;
            • 虚拟机会为每个装载的非抽象类生成一个方法表,并把它作为类型信息的一部分保存在方法区中;
            • 方法表是一个数组,其元素是所有它的实例可能调用的实例方法的直接引用,包括那些从超类继承过来的方法;
          • 程序计数器:每一个线程都有自己的PC寄存器,它是在线程启动时创建的;PC寄存器大小是一个字长,因此即可以持有一个本地指针,也可以持有一个returnAddress;
          • Java栈:每当启动一个新线程,虚拟机都会为它创建一个Java栈;
            • Java栈以桢为单位保存线程的运行状态;
            • 当线程调用一个Java方法时,虚拟机都会在线程的Java栈中压入一个新桢,在执行这个方法时,虚拟机使用这个栈桢存储参数、局部变量和中间运算结果等;
            • Java方法可以以两种方法完成:一种是通过return正常返回;另一种是抛出异常中止;不管是哪种返回,虚拟机都会弹出当前栈桢,丢弃;

              一、class文件内容

              Java class文件是对Java程序二进制文件格式的精确定义。每一个Java class文件都对一个Java类或者Java接口作出了全面描述。一个class文件只 能包含一个类或接口;

              class文件内容按顺序如下(ClassFile表中各项简介如下):
              1. magic(魔数)-u4
                • 0xCAFEBABE,其作用在于可以轻松辨别出Java class文件和非Java class文件;
              2. minor_version-u2和major_version-u2(次、主版本号)
                • 对于虚拟机来说,主次版本号确定了特定的class文件格式,通常只有给定主版本号和一系列次版本号后,虚拟机才能读取该class文件;
                • 如下图所示

                  第5、6字节表示次版本号=0
                  第7、8字节表示主版本号=50(高位在前)
              3. constant_pool_count-u2和constant_pool(常量池)
                • 常量池的数量为constant_pool_count-1,其索引从1开始;
                • 除直接常量外,常量池还包括类和接口的全限定名、字段的名称和描述符以及方法的名称和描述符等符号引用;
                • 每个常量池入口都有一个字节的标志,这个标志指出了列表中该位置的常量类型。虚拟机获取这个标志后,就知道在标志后面的常量类型了;其中每一 个标志都有一个相对应的表结构来描述该常量池;具体类型如下表所示:
                  java虚拟机学习笔记_第13张图片
              4. access_flags(访问标志)-u2
                • 访问标志展示了文件中类或接口的如下信息: 标志名 值 设置后的含义
                  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;
                  正是由于这两种绑定的不同,在子类覆盖超类的方法、并向上转型引用后,才产生了多态以及其他特殊的调用结果。

              5. this_class-u2(当前类)
                • 它是一个对常量池的索引,在this_class位置的常量池入口必须为CONSTANT_Class类型对应的表结构 CONSTANT_Class_info...;
                • 下图显示了常量池的使用方法:
                  java虚拟机学习笔记_第14张图片
                  在CONSTANT_Utf8_info表的bytes里存放的即是当前类的全限定名字符串;
              6. super_class-u2(直接超类)
                • 和this_class表示的内容相同;
                • 除了java.lang.Object类以外,常量池索引super_class对于所有类均有效;
                • 对于java.lang.Object,其super_class为0;
                • 对于接口,其super_class指向的常量池为java.lang.Object;
              7. interfaces_count-u2和interfaces(接口列表)
                • 保存该类直接实现或该接口所扩展的接口数量;
                • 接口按implements子句和extends子句出现的顺序显现;
                • interfaces表中的每一项都是对父接口的常量池的索引,该索引用CONSTANT_Class_info来描述;
                • 如果接口数为0,则没有interfaces表;
              8. fields_count-u2和fields(字段列表)
                • 只有在class文件中由类或接口声明了的字段才在fields列表中列出,不列出从超类或父接口中继承而来的字段;
                • fields列表可能会包含在对应的Java源文件中没有叙述的字段,这是因为Java编译器可能会在编译时向类或者接口添加字段;如对一个内部类的fields列表来说,为了保持对外围类实例的引用,Java编译器会为每个外围类实例添加实例变量 ;
                • 第个field_info表包含以下字段信息:
                  • 字段名字
                  • 字段描述符和修饰符
                  • 如果该字段被声明为final,则还包含其常量值;
              9. methods_count-u2和methods(方法列表)
                • methods_count表示该类或接口中所声明的所有方法的总计数,不包括超类或都父接口中继承来的方法;
                • methods列表中包含以下信息:
                  • 方法名和描述符(方法的返回值类型和参数类型)
                  • 如果方法即不是抽象的,也不是本地的,则还包含方法局部变量所需的栈空间长度、为方法所捕获的异常表、字节码序列以及可选的行数和局部变量 表
                  • 如果方法能够抛出任何已验证异常,则methods列表就会包括一个关于这些已验证异常的列 表
              10. attributes_count-u2和attributes(属性列表)
                • class文件中最后的部分是属性,它给出了该文件中类或接口所定义的属性的基本信息;
                • 每个attribute_info表的第一项是指向常量池中CONSTANT_Utf8_info表的索引,该表给出了属性的名称;
              class文件内容举例
              1. 代码 
                public interface MyInterface {
                    void hello();
                }
              2. 编译后其字节码如下图:
                java虚拟机学习笔记_第15张图片
                • 常量池数"0009"表示后面紧接着有8个常量池项
                • 常量池索引1表示一个CONSTANT_Class_info表(07),它引用索引为7的常量池;
                • 常量池索引1表示一个CONSTANT_Class_info表(07),它引用索引为8的常量池;
                • 常量池索引3表示一个CONSTANT_Utf8_info表(01),其内容为hello(方法名);
                • 常量池索引4表示一个CONSTANT_Utf8_info表(01),其内容为()V(方法参数与返回值);
                • 常量池索引5表示一个CONSTANT_Utf8_info表(01),其内容为SourceFile(某属性值);
                • 常量池索引6表示一个CONSTANT_Utf8_info表(01),其内容为MyInterface.java(某属性值);
                • 常量池索引7表示一个CONSTANT_Utf8_info表(01),其内容为MyInterface,被常量池索引1引用到(当前类);
                • 常量池索引8表示一个CONSTANT_Utf8_info表(01),其内容为java/lang/Object,被常量池索引2引用到 (超类);
                • 访问标志"0601"表示是public(0001)、abstract(0400),且是接口(0200);
                • 当前类索引号"0001"表示指向常量池索引1,指明当前类为MyInterface;
                • 超类索引号"0002"表示指向常量池索引2,指明超类为java/lang/Object;
                • 实现的接口数"0000"表示没有实现任何接口;
                • 字段数"0000"表示该接口没有字段;
                • 方法数"0001"表示接口有一个方法;
                • 剩下的字节码为方法列表及属性列表;

              二、特殊字符串

              全限定名

              常量池指向类或者接口时,给出的是全限定名,形如java/util/Hashtable;

              简单名称

              字段名和方法名以简单名称形式出现在常量池入口中,如上例中的常量池索引3中的"hello";

              描述符
              1. 指向字段和方法的符号引用还包含描述符字符串;字段的描述符给出了字段的类型,方法描述符给出了方法的返回值和方法参数的数量、类型以及顺序;
              2. 描述符使用上下文无关语法:斜体表示非终结符,等宽字体表示终结符;
              3. 如下所示:
                java虚拟机学习笔记_第16张图片
              4. 基本类型终结符 终结符 类型
                B byte
                C char
                D double
                F float
                I int
                J long
                S short
                Z boolean
              5. 描述符举例 描述符 说明
                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)

                一、类型生命周期的开始

                1. 如图所示
                  java虚拟机学习笔记_第17张图片
                2. 初始化时机
                  • 所有Java虚拟机实现必须在每个类或接口首次主动使用时初始化;
                  • 以下几种情形符合主动使用的要求:
                    • 当创建某个类的新实例时(或者通过在字节码中执行new指令,或者通过不明确的创建、反射、克隆和反序列化);
                    • 当调用某个类的静态方法时(即在字节码中执行invokestatic指令);
                    • 当使用某个类或接口的静态字段,或者对该字段赋值时(用final修饰的静态字段除外,它被初始化为一个编译时常量表达式);
                    • 当调用Java API中的某些反射方法;
                    • 当初始化某个类的子类时(子类初始化时,要求父类已经被初始化);
                    • 当虚拟机启动时某个被标明为启动类的类(即含有main方法的那个类);
                  • 无论如何,如果一个类在首次主动使用前还没有被装载和连接的话,那它必须在此时被装载和连接,这样才能初始化;
                3. 默认值和初始值
                  • 在准备阶段,虚拟机把给类变量分配的内存设置为默认值;
                  • 在初始化阶段,为类变量赋予正确的初始值;
                  • 在Java代码中,一个正确的初始值是通过类变量初始化语句或静态初始化语句给出的;如:
                    static int size = 3 * (int) (Math.random() * 5.0);

                    static int size;
                    static {
                        size = 3 * (int) (Math.random() * 5.0);
                    }
                4. <clinit>()方法
                  • 所有的类变量初始化语句和类型的静态初始化器都被Java编译器收集在一起,放在一个特殊的方法中,该方法为"<clinit>()"方法;该方法只能由Java虚拟机调用;
                  • 初始化一个类包含两步:
                    • 如果类存在直接超类的话,且直接超类没有初始化,就先初始化直接超类;第一个被初始化的类永远是Object;
                    • 如果类存在一个类初始化方法,就执行此方法;
                  • 初始化接口不需要初始化它的父接口,只需一步:如果接口存在一个接口初始化方法,则执行此方法;
                  • 以下的类没有<clinit>()方法
                    • 如果类没有声明任何类变量,也没有静态初始化语句;
                    • 如果类声明了类变量,但是没有明确使用类变量初始化语句或静态初始化语句来初始化它们;
                    • 如果类仅包含静态final变量的初始化语句,而且这些类变量初始化语句采用编译时常量表达式;

                二、对象的生命周期

                1. 主动使用和被动使用
                  • 前面提到过,JVM在首次主动使用类型时初始化它们;
                  • 使用一个非常量的静态字段只有当类或者接口的确声明了这个字段才是主动使用;如类中声明的字段可能会被子类引用、接口中声明的字段可能会被子接口或是实现了该接口的类引用;对于子类、子接口和实现了接口的类来说,都是被动使用,它们不会触发初始化;
                2. <init>()方法
                  • Java编译器为它编译的每一个类都至少生成一个实例初始化方法,该方法称为"<init>"方法;
                  • 针 对源代码中的每一个类的构造方法,Java编译器都产生一个<init>()方法,如果类没有明确地声明任何构造方法,编译器默认产生一个无 参数的构造方法,它仅仅调用超类的无参构造方法,同时也创建一个<init>()方法,对应默认构造方法;
                  • 一个<init>()方法中可能包含三种代码:调用另一个<init>()方法、实现对任何实例变量的初始化、构造方法体的代码
                    • 如果构造方法通过明确地调用同一个类中的另一个构造方法(this()),它对应的<init>()方法由由两部分组成
                      1. 一个同类的<init>方法的调用
                      2. 实现了对应构造方法的方法体的字节码
                    • 如果构造方法不是通过一个this()调用开始,而且这个对象不是Object,<init>()方法则由三部分组成
                      1. 一个超类的<init>()方法的调用;
                      2. 任意实例变量初始化方法的字节码;
                      3. 实现了对应构造方法的方法体的字节码
                3. 对象的终结
                  • 如果类声明了一个名为void finalize()方法,垃圾收集器会在释放这个实例的内存前执行这个方法一次;
                  • 垃圾收集器最多只会调用一个对象的终结方法一次;如果终结方法代码执行后,对象重新被引用了(即复活),随后再次变得不被引用,垃圾收集器不会第二次调用终结方法;

                三、类型的卸载

                1. 和对象一样,当类型不再需要时,可以通过卸载来释放内存空间;
                2. 类型的卸载也是通过垃圾回收器完成的;
                一、Java内存组成
                1. 组成图
                  java虚拟机学习笔记_第18张图片
                2. 堆(Heap)
                  1. 运行时数据区域,所有类实例和数组的内存均从此处分配。Java虚拟机启动时创建。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。
                  2. 组成 组成 详解
                    Young Generation 即图中的Eden + From Space + To Space 
                    1.Eden存放新生的对象 
                    2.Survivor Space有两个,存放每次垃圾回收后存活的对象
                    Old Generation Tenured Generation 即图中的Old Space 
                    主要存放应用程序中生命周期长的存活对象
                3. 非堆内存
                  1. JVM具有一个由所有线程共享的方法区。方法区属于非堆内存。它存储每个类结构,如运行时常数池、字段和方法数据,以及方法的代码。它是在Java虚拟机启动时创建的。
                  2. 除了方法区外,Java虚拟机实现可能需要用于内部处理或优化的内存,这种内存也是非堆内存。例如,JIT编译器需要内存来存储从Java虚拟机代码转换而来的本机代码,从而获得高性能。
                  3. 组成 组成 详解
                    Permanent Generation 即图中的Permanent Space 
                    存放JVM自己的反射对象,比如类对象和方法对象
                    native heap JVM内部处理或优化
                二、GC策略
                  1. JVM采用一种分代回收(generational collection)的策略,用较高的频率对年轻的对象(young generation)进行扫描和回收,这种叫做minor collection,而对老对象(old generation)的检查回收频率要低很多,称为major collection。这样就不需要每次GC都将内存中所有对象都检查一遍。
                1. 非堆内存
                  1. GC不会在主程序运行期对PermGen Space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen Space错误。
                三、内存申请、对象衰老过程
                1. 内存申请过程
                  1. JVM会试图为相关Java对象在Eden中初始化一块内存区域;
                  2. 当Eden空间足够时,内存申请结束。否则到下一步;
                  3. JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
                  4. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
                  5. 当OLD区空间不够时,JVM会在OLD区进行major collection;
                  6. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误";
                2. 对象衰老过程
                  1. young generation的内存,由一块Eden和两块Survivor Space构成。新创建的对象的内存都分配自eden。两块Survivor Space总有会一块是空闲的,用作copying collection的目标空间。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。所谓survivor,也就是大部分对象在Eden出生后,根本活不过一次GC。对象在young generation里经历了一定次数的minor collection后,年纪大了,就会被移到old generation中,称为tenuring。
                  2. 剩余内存空间不足会触发GC,如eden空间不够了就要进行minor collection,old generation空间不够要进行major collection,permanent generation空间不足会引发Full GC。
                四、JVM参数
                1. 参数说明 参数 说明 默认值 生产环境约定
                  -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区比例,以达到目标系统规定的最低相应时间或者收集频率等。   此值建议使用并行收集器时,一直打开。
                2. 举例说明
                  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存入位置为78的局部变量中

                24: lconst_1 //常量long类型的1入栈

                25: lstore 9 // 弹出栈顶元素0存入位置为910的局部变量中

                27: lload 7 //从位置为78的局部变量中取出元素long类型的0压入栈

                29: lload 9 //从位置为910的局部变量中取出元素long类型的1压入栈

                31: ladd //从栈顶弹出两个元素然后做加法,把结果压入栈

                32: lstore 11 //弹出栈顶元素1存入位置为1112的局部变量中

                34: aconst_null //null对象引用压入栈

                35: astore 13 //弹出栈顶元素null存入位置为13的局部变量中

                37: return

                 

                }

                1.2常量值在字节码中跟随在操作码之后:

                byteshort类型常量压入栈

                操作码

                操作数

                说明

                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虚拟机通过给定的索引查找相应的常量池入口,决定这些常量的类型和值,并把它们压入栈。

                常量池索引是一个无符号值,ldcldc_w是把一个字长的项压入栈,区别在于:ldc的索引只有一个8位,只能指向常量池中1255范围的位置。ldc_w的索引有16位,可以指向165535范围的位置。

                例如下面代码:

                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存入位置为1011的局部变量中

                47: ldc2_w #20; //long -2147483649l //从常量池索引20的位置取出long类型的-2147483649L压入栈

                50: lstore 12 //弹出栈顶元素-2147483649L存入位置为1213的局部变量中

                52: lload 10 //取出位置为1011的局部变量中的数压入栈

                54: lload 12 //取出位置为1213的局部变量中的数压入栈

                56: ladd //从栈顶弹出两个元素然后做加法,把结果压入栈

                57: lstore 14 //弹出栈顶元素存入位置为1415的局部变量中

                59: return

                 

                2通用栈操作

                操作码

                操作数

                说明

                nop

                (无)

                不做任何操作

                pop

                (无)

                从操作数栈弹出栈顶部的一个字

                pop2

                (无)

                从操作数栈弹出最顶端的两个字

                swap

                (无)

                交换栈顶部的两个字

                dup

                (无)

                复制栈顶部的一个字

                dup2

                (无)

                复制栈顶部的两个字

                dup_x1

                (无)

                复制栈顶部的一个字,并将复制内容及原来弹出的两个字长的内容压入栈

                dup_x2

                (无)

                复制栈顶部的一个字,并将复制内容及原来弹出的三个字长的内容压入栈

                dup2_x1

                (无)

                复制栈顶部的两个字,并将复制内容及原来弹出的三个字长的内容压入栈

                dup2_x2

                (无)

                复制栈顶部的两个字,并将复制内容及原来弹出的四个字长的内容压入栈

                1dup:复制栈顶部的一个字长的内容。

                栈:

                前:......,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

                后:.......

                8pop2:弹出栈顶端两个字长的内容

                :

                前:......,word2,word1

                后:.......

                9swap:交换栈顶端两个字的内容

                :

                前:......,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

                将位置为vindexint类型的局部变量压入栈

                iload_0

                ()

                将位置为0int类型的局部变量压入栈

                iload_1

                ()

                将位置为1int类型的局部变量压入栈

                iload_2

                ()

                将位置为2int类型的局部变量压入栈

                iload_3

                ()

                将位置为3int类型的局部变量压入栈

                fload

                vindex

                将位置为vindexfloat类型的局部变量压入栈

                fload_0

                ()

                将位置为0float类型的局部变量压入栈

                fload_1

                ()

                将位置为1float类型的局部变量压入栈

                fload_2

                ()

                将位置为2float类型的局部变量压入栈

                fload_3

                ()

                将位置为3float类型的局部变量压入栈

                将两个字长的局部变量压入栈

                操作码

                操作数

                说明

                lload

                vindex

                将位置为vindex(vindex+1)long类型的局部变量压入栈

                lload_0

                ()

                将位置为01long类型的局部变量压入栈

                lload_1

                ()

                将位置为12long类型的局部变量压入栈

                lload_2

                ()

                将位置为23long类型的局部变量压入栈

                lload_3

                ()

                将位置为34long类型的局部变量压入栈

                dload

                vindex

                将位置为vindex(vindex+1)double类型的局部变量压入栈

                dload_0

                ()

                将位置为01double类型的局部变量压入栈

                dload_1

                ()

                将位置为12double类型的局部变量压入栈

                dload_2

                ()

                将位置为23double类型的局部变量压入栈

                dload_3

                ()

                将位置为34double类型的局部变量压入栈

                将对象引用局部变量压入栈

                操作码

                操作数

                说明

                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类型值,然后将其存到位置为01的局部变量中

                lstore_1

                ()

                从栈中弹出long类型值,然后将其存到位置为12的局部变量中

                lstore_2

                ()

                从栈中弹出long类型值,然后将其存到位置为23的局部变量中

                lstore_3

                ()

                从栈中弹出long类型值,然后将其存到位置为34的局部变量中

                dstore

                vindex

                从栈中弹出double类型值,然后将其存到位置为vindex(vindex+1)的局部变量中

                dstore_0

                ()

                从栈中弹出double类型值,然后将其存到位置为01的局部变量中

                dstore_1

                ()

                从栈中弹出double类型值,然后将其存到位置为12的局部变量中

                dstore_2

                ()

                从栈中弹出double类型值,然后将其存到位置为23的局部变量中

                dstore_3

                ()

                从栈中弹出double类型值,然后将其存到位置为34的局部变量中

                操作码

                操作数

                说明

                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虚拟机从栈顶端弹出一个值,对它进行转换,然后再把转换结果压入栈。

                intlongfloatdouble 类型之间的相互转换

                操作码

                操作数

                说明

                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数据类型向bytecharshort类型的转换

                操作码

                操作数

                说明

                i2b

                ()

                int类型值转换为byte类型值

                i2c

                ()

                int类型值转换为char类型值

                i2s

                ()

                int类型值转换为short类型值

                i2b指令将弹出的int类型值截取为byte类型,然后再对其进行带符号扩展,恢复成int类型压入栈。

                i2c指令将弹出的int类型值截取为char类型,然后再对其进行零扩展,恢复成int类型压入栈

                i2s将弹出的int类型值截取为short类型,然后再对其进行带符号扩展,恢复成int类型压入栈

                不存在把比int类型值占据更小空间的数据类型转换成int类型的操作码。因为任何bytecharshort类型值在压入栈的时候,就已经有效的转换成int类型了,涉及到bytecharshort类型的运算操作首先都把这些值转化成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虚拟机提供几种进行整数算术运算的操作码,他们执行基于intlong类型的运算。当byteshortchar类型值参与算术运算时,首先会将它们转换为int类型。这些操作码都不会抛出异常,溢出在这里通常可以被忽略。

                整数加法

                操作码

                操作数

                说明

                iadd

                (无)

                从栈中弹出两个int类型数,相加,然后将所得int类型结果压回栈

                ladd

                (无)

                从栈中弹出两个long类型数,相加,然后将所得long类型结果压回栈

                将一个常量与局部变量相加

                操作码

                操作数

                说明

                iinc

                vindex,const

                把常量与一个位于vindex位置的int类型局部变量相加

                wide

                iincindexconst

                把常量与一个位于vindex位置的int类型局部变量相加

                iinc可以用来给局部变量赋值-128127之间的值。这条操作码与用于控制循环执行的变量的加减相比,效率更高。加上wide可以用来把常量的范围增大到-3276832767

                整数减法

                操作码

                操作数

                说明

                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指定)压入栈

                putfieldgetfield这两个操作码只在字段是实例变量的情况下才执行,putstaticgetstatic对静态变量进行存取操作。操作数表示常量池的索引。这个索引所指向的常量池入口包含了该字段的所属类、名字和类型等信息。如果还没有这些信息,虚拟机会解析这个常量池入口。

                例如下面代码:

                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;
                   }
                }
                复制代码
                以上函数的返回结果时什么?
                对于finally,我们通常的认识一般如下:
                不管异常是否发生,它都会执行,但是看上面的例子,哪个return先执行?
                其实际语义具体描述如下:
                try中抛出异常后,如果存在捕获异常的过程,那么这个过程执行完后会执行finally;
                try中没有异常发生,那么 当try执行到结尾后,执行finally.
                注意 “try执行到结尾”是指在return之前
                 
                所以normal()函数实际上返回的是20.
                 
                Java虚拟机规范中谈到,finally子句类似于一种微型子例程,我们可以通过查看class文件的字节码理解。
                 
                新建TryFinallyTest.java进行测试:
                复制代码
                public class TryFinallyTest{ private static int normal(){ try{ return 10;
                    }finally{ return 20;
                    }
                 } public static void main(String args[]){
                    System.out.println(normal());
                 }
                 
                }
                复制代码
                使用javap -c -private TryFinallyTest输出类汇编版本:
                (关于以下的jvm指令的含义,可以参考《Java虚拟机规范》或者《Inside JVM》)
                复制代码
                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
                 
                }
                复制代码
                示例2:Surprise.java
                复制代码
                class Surprise { static boolean surpriseTheProgrammer(boolean bVal) { while (bVal) { try { return true;
                            } finally { break;
                            }
                        } return false;
                    }
                }
                复制代码
                相信经过例子1可以知道答案了。
                对应的类汇编版本:
                复制代码
                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
                
                }
                复制代码
                《Inside JVM》中提到finally子句往往通过jsr指令跳转,但是javap显示的结果并不是这样的,编译器做了一定的优化。
                 
                示例3:
                Finally子句的一个令人困惑的特性:
                复制代码
                    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);
                        }
                    }
                复制代码
                请问 normal的返回值是多少,结果如下
                in try:set a to 1 in finally:set a to 2
                1
                先看看javap的结果吧,
                复制代码
                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
                复制代码
                假设上面方法修改的是对象的引用,结果也是类似的,两者的差别仅在于字节码中的操作码有所不同。
                (一个是iload和istore,一个是astore和aload)
                所以,如果在finally中修改了变量,而又需要返回该值,那么必须在finally块中添加返回语句。finally里面对result再赋值也不会影响栈顶的返回值,只会影响result的值。但如果在finally里面也加了return的话,这时候虚拟机栈又会把result的值copy到return的栈顶,这时候返回的就是finally里面重新赋值之后的result的值了

                  1.对多线程的支持是Java语言的一大优势,Java对线程的控制主要集中在对线程的同步和协作上,Java使用的同步机制是监视器。

                监视器

                java监视器支持两种线程:互斥和协作。
                java虚拟机通过对象锁来实现互斥,允许多个线程在同一个共享数据上独立而不干扰地工作。
                协作则通过object类的wait方法和notify方法来实现,允许多个线程为了同一个目标而共同工作。
                我们将监视器比作一个建筑物,里面有很多房间,房间里面有一些数据,并且同一个时间只能被一个线程占据。一个线程进入房间到离开为止,独占其中全部的数据。
                进入这个数据叫做进入监视器。
                进入房间叫做获得监视器。
                占据房间叫做持有监视器。
                离开房间叫做释放监视器。
                离开建筑叫做退出监视器。

                互斥原理
                对一个监视器来说监视区域是最小的,不可分割的代码块。同一个监视器中,监视区域只会被一个线程执行。
                当一个线程到达了一个监视区域的开始处,它就会放置到监视器的入口区。如果没有其他线程等待,它就会进入监视器并且获得监视器,当执行完监视区域的代码后,就会释放并退出监视器。
                如果当前监视器正在被另外一个线程持有,那么它就会进入一个等待队列(如果有其他线程也在等待进入监视器)。当持有线程退出之后,只能有等待队列中的一个线程进入并持有该监视器,其他的线程仍旧留会在等待队列中。

                协作原理
                当一个线程需要一些特别的数据,而由另一个线程负责改变这些数据的状态时,同步显得特别重要。
                eg,一个读线程会由一个缓冲区中读取数据,而当前这个缓存区是空的,就需要一个写线程写入数据,当写数据完成写入,读线程才能做读取操作。
                java虚拟机使用的这种监视器叫做等待并唤醒监视器。
                这种监视器中,一个已经持有该监视器的线程,可以通过调用等待命令,暂停自身的执行。当这个线程执行等待命令后,就会释放该监视器并进入一个等待区,并会一直在那里持续等待状态,直到一段时间后该监视器内的其他线程调用了唤醒命令。当一个线程调用唤醒命令后,它会持续持有监视器,直到它主动释放监视器。当执行了唤醒命令或者执行完监视区域,释放监视器后,等待线程会苏醒,其中的一个会重新获得监视器,判断状态条件,以便决定是否继续继续进入等待状态或者退出或者执行监视区域。

                    Java虚拟机中的这种监视器模型如图20-1所示,将监视器分成了三个区域。中间的大方框包括一个单独的线程,是监视器的持有者;左边小的方框中时入口区;右边另一个小方框是等待区。活动线程用深灰色圆画出,暂停的线程用灰色圆画出。

                java虚拟机学习笔记_第19张图片

                对象锁

                java虚拟机的一些运行时数据区会被所有线程共享,其他的数据是各个线程私有的。

                因为堆和方法区是被所有线程共享的,java程序需要为两种多线程访问数据进行协调。

                1)保存在堆中的实例变量

                2)保存在方法区中的类变量

                程序不需要协调保存在java栈中的局部变量,因为java栈中的数据是属于拥有该栈的线程私有的。

                在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。对于对象来说,相关联的监视器保护对象的实例变量。对于类来说,监视器保护类的类变量。如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。

                 

                为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁(有时候被称为互斥提(mutex))。一个锁就像一种任何时候只允许一个线程“拥有”的特权。线程访问实例变量或者类变量不需要获取锁。但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)

                类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。

                 

                一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减1,当计数器值为0时,锁就被完全释放了。

                 

                java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。在java程序中,只需要使用同步语句或者同步方法就可以标志一个监视区域。当每次进入一个监视区域时,java虚拟机都会自动锁上对象或者类。



你可能感兴趣的:(java虚拟机学习笔记)