跟孙哥学java
JVM概述
java开发中的问题
你被JVM伤害过吗?
:::info 你是否也遇到过这些问题?
运行着的线上系统突然卡死,系统无法访问,甚至直接OOM! 想解决线上JVM GC问题,但却无从下手。 新项目上线,对各种JVM参数设置一脸茫然,直接默认吧,然后就JJ了 每次面试之前都要重新背一遍JVM的一些原理概念性的东西,然而面试官却经常问你在实际项目中如何调优JVM参数,如何解决GC、OOM等问题,一脸懵逼。 :::
架构师每天都在思考什么?
- 应该如何让我的系统更快?
- 如何避免系统出现瓶颈?
Java语言以及Java生态
JVM的架构与知识脉络图
能否画出JVM架构图?
这个架构可以分成三层看:
- 最上层:javac编译器将编译好的字节码class文件,通过java 类装载器执行机制,把对象或class文件存放在 jvm划分内存区域。
- 中间层:称为Runtime Data Area,主要是在Java代码运行时用于存放数据的,从左至右为方法区(永久代、元数据区)、堆(共享,GC回收对象区域)、栈、程序计数器、寄存器、本地方法栈(私有)。
- 最下层:解释器、JIT(just in time)编译器和 GC(Garbage Collection,垃圾回收器)
JVM有哪几块知识脉络?
字节码文件概述
字节码文件是跨平台的吗?
是的
Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联。
无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。
想要让一个Java程序正确地运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码。
docs.oracle.com/javase/spec… 所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的,这样一来字节码文件可以在各种JVM上运行。
从Java虚拟机的角度看,通过Class文件,可以让更多的计算机语言支持Java虚拟机平台。因此,Class文件结构不仅仅是Java虚拟机的执行入口,更是Java生态圈的基础和核心。
class文件里是什么?
字节码文件里是什么?
源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码。
随着Java平台的不断发展,在将来,Class文件的内容也一定会做进一步的扩充,但是其基本的格式和结构不会做重大调整。
能介绍下生成class文件的编译器吗?
生成class文件的编译器是指前端编译器即javac
- 从位置上理解
前端编译器 vs 后端编译器
半编译半解释型语言! javac ... java ...
半编译半解释型是指生成 .class文件后,通过解释器进行解释,JIT进行即时编译
- 前端编译器的种类 Java源代码的编译结果是字节码,那么肯定需要有一种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译为字节码的前端编译器。
HotSpot VM并没有强制要求前端编译器只能使用javac来编译字节码,其实只要编译结果符合JVM规范都可以被JVM所识别即可。 在Java的前端编译器领域,除了javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的ECJ (Eclipse Compiler for Java)编译器。和Javac的全量式编译不同,ECJ是一种增量式编译器。
在Eclipse中,当开发人员编写完代码后,使用“Ctrl+S”快捷键时,ECJ编译器所釆取的编译方案是把未编译部分的源码逐行进行编译,而非每次都全量编译。因此ECJ的编译效率会比javac更加迅速和高效,当然编译质量和javac相比大致还是一样的。 ECJ不仅是Eclipse的默认内置前端编译器,在Tomcat中同样也是使用ECJ编译器来编译jsp文件。由于ECJ编译器是釆用GPLv2的开源协议进行源代码公开,所以,大家可以登录eclipse官网下载ECJ编译器的源码进行二次开发。 默认情况下,IntelliJ IDEA 使用 javac 编译器。(还可以自己设置为AspectJ编译器 ajc)
- 前端编译器的任务 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。
javac编译器的编译步骤?
javac编译步骤
javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。
大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图中的各个步骤
目前前端编译器的局限性?
前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的JIT编译器负责。
复习:AOT(静态提前编译器,Ahead Of Time Compiler)
jdk9引入了AOT编译器(静态提前编译器,Ahead Of Time Compiler)
Java 9 引入了实验性 AOT 编译工具jaotc。它借助了 Graal 编译器,将所输入的 Java 类文件转换为机器码,并存放至生成的动态共享库之中。
所谓 AOT 编译,是与即时编译相对立的一个概念。我们知道,即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而 AOT 编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。 .java -> .class -> .so 最大好处:Java虚拟机加载已经预编译成二进制库,可以直接执行。不必等待即时编译器的预热,减少Java应用给人带来“第一次运行慢”的不良体验。
缺点: 破坏了java“一次编译,到处运行”,必须为每个不同硬件、OS编译对应的发行包。 降低了Java链接过程的动态性,加载的代码在编译期就必须全部已知。 还需要继续优化中,最初只支持Linux x64 java base
哪些类型对应右Class的对象?
(1)class: 外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类 (2)interface:接口 (3)[]:数组 (4)enum:枚举 (5)annotation:注解@interface (6)primitive type:基本数据类型 (7)void
@org.junit.Test
public void test() {
Class c1 = Object.class;
Class c2 = Comparable.class;
Class c3 = String[].class;
Class c4 = int[][].class;
Class c5 = ElementType.class;
Class c6 = Override.class;
Class c7 = int.class;
Class c8 = void.class;
Class c9 = Class.class;
int[] a = new int[10];
int[] b = new int[100];
Class c10 = a.getClass();
Class c11 = b.getClass();
// 只要元素类型与维度一样,就是同一个Class
System.out.println(c10 == c11);
}
字节码指令
什么是字节码指令?
什么是字节码指令(byte code)?
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。 比如:
为什么要懂字节码指令?
public class ByteCodeInterview {
//面试题: i++和++i有什么区别?
@Test
public void test1() {
int i = 10;
i++;
//++i;
System.out.println(i);
}
@Test
public void test2() {
int i = 10;
i = i++;
System.out.println(i);//10
}
@Test
public void test3() {
int i = 2;
i *= i++;
System.out.println(i);
}
@Test
public void test4() {
int k = 10;
k = k + (k++) + (++k);
System.out.println(k);
}
//包装类对象的缓存问题
@Test
public void test5() {
// Integer x = 5;
// int y = 5;
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2);
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);
Boolean b1 = true;
Boolean b2 = true;
System.out.println(b1 == b2);
}
}
@Test
public void test2() {
int i = 10;
i = i++;
System.out.println(i);//10
}
test2 字节指令
@Test
public void test3() {
int i = 2;
i *= i++; //i=i*i++=2*2=4;
System.out.println(i);
}
@Test
public void test4() {
int k = 10;
k = k + (k++) + (++k);//k=10+10+12=32;
System.out.println(k);
}
//包装类对象的缓存问题
@Test
public void test5() {
// Integer x = 5;
// int y = 5;
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2);//true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//false
Boolean b1 = true;
Boolean b2 = true;
System.out.println(b1 == b2);//true
}
@Test
public void test6(){
String str = new String("hello") + new String("world");//堆空间
String str1 = "helloworld";//常量池
System.out.println(str == str1);//false
}
String声明的字面量数据都放在字符串常量池中
jdk6中字符串常量池存放在方法区(即永久代中)
jdk7及以后字符串常量池存放在堆空间
@Test
public void test7(){
String str = new String("hello") + new String("world");
str.intern();//加上 str.intern() 在str声明之前
String str1 = "helloworld";
System.out.println(str == str1);//true
}
class Father {
int x = 10;
public Father() {
this.print();
x = 20;
}
public void print() {
System.out.println("Father.x = " + x);
}
}
class Son extends Father {
int x = 30;
public Son() {
this.print();
x = 40;
}
public void print() {
System.out.println("Son.x = " + x);
}
}
public class SonTest {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.x);
}
}
Son.x = 0
Son.x = 30
20
如何解读.class文件?
idea下载插件 jclasslib
Class文件结构细节
官方文档位置: docs.oracle.com/javase/spec…
Class 类的本质 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class 文件是一组以8位字节为基础单位的二进制流。
Class文件格式 Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
“下雨天留客天留我不留” “下雨天,留客天,留我不留?” “下雨天,留客天,留我不?留!” “下雨,天留客?天留,我不留!”
Class 文件格式采用一种类似于 C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表。 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。 由于表没有固定长度,所以通常会在其前面加上个数说明 代码举例
public class Demo {
private int num = 1;
public int add(){
num = num + 2;
return num;
}
}
对应的字节码文件:
换句话说,充分理解了每一个字节码文件的细节,自己也可以反编译出Java源文件来。
class文件结构细节概述
class文件结构概述 Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。 Class文件的总体结构如下: 魔数 Class文件版本 常量池 访问标识(或标志) 类索引,父类索引,接口索引集合 字段表集合 方法表集合 属性表集合
这是一张Java字节码总的结构表,我们按照上面的顺序逐一进行解读就可以了。
这里整体给大家讲解一遍,这里需要注意听。否则大家以后也很难有动力,踏实认真的一个字节一个字节的解读一遍。
面试题
文件结构有几个部分?(百度)
Java 的 .class
文件是 Java 编译器编译 Java 源代码生成的字节码文件,它包含了用于在 Java 虚拟机上执行的二进制格式。.class
文件的结构可以分为以下几个主要部分:
- 魔数(Magic Number):
- 大小:4 个字节
- 描述:
.class
文件的头部始终以一个魔数开头,用于标识这是一个 Java 字节码文件。魔数的值为0xCAFEBABE
。
- 版本信息:
- 大小:4 个字节
- 描述:
.class
文件的次部分包含了两个版本号,分别是主版本号(Major Version)和次版本号(Minor Version)。这两个版本号描述了 Java 编译器和 JVM 的版本。
- 常量池(Constant Pool):
- 大小:不固定
- 描述:常量池存储了类中使用的常量,包括类名、字段名、方法名、字符串字面值等。常量池的索引从1开始,0被用作特殊标记,表示不引用任何常量。
- 访问标志(Access Flags):
- 大小:2 个字节
- 描述:访问标志用于描述类或接口的访问级别,例如
public
、final
、abstract
等。
- 类索引、父类索引和接口索引集合:
- 大小:各占 2 个字节
- 描述:这些索引指向常量池中的类、父类、接口的描述符。类索引指向当前类的全限定名,父类索引指向当前类的父类,接口索引集合指向当前类实现的接口。
- 字段表(Field Table):
- 大小:不固定
- 描述:字段表描述了类或接口中声明的字段信息,包括字段的访问标志、名称、描述符等。
- 方法表(Method Table):
- 大小:不固定
- 描述:方法表存储了类或接口中定义的方法信息,包括方法的访问标志、名称、描述符、方法体等。
- 属性表(Attribute Table):
- 大小:不固定
- 描述:属性表包含了额外的元数据信息,例如代码行号、局部变量表、异常表等。常见的属性有
Code
属性、SourceFile
属性等。
这些部分构成了 Java 字节码文件的基本结构。.class
文件中的具体内容和格式遵循 Java 虚拟机规范,这使得 Java 虚拟机能够正确地加载和执行 Java 字节码。
class文件的魔数是什么?
Magic Number(魔数):class文件的标志 每个 Class 文件开头的4个字节的无符号整数称为魔数(Magic Number)
它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符。
魔数值固定为0xCAFEBABE。不会改变。
如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误: Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 1885430635 in class file StringTest
使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。
如何确保高版本的JVM可执行低版本的class文件?
不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。 (向下兼容) 在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时,特别注意开发编译的JDK版本和生产环境中的JDK版本是否一致。
class文件版本号 紧接着魔数的 4 个字节存储的是 Class 文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。
它们共同构成了class文件的格式版本号。譬如某个 Class 文件的主版本号为 M,副版本号为 m,那么这个Class 文件的格式版本号就确定为 M.m。
版本号和Java编译器的对应关系如下表:
Java 的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1。
虚拟机JDK版本为1.k (k >= 2)时,对应的class文件格式版本号的范围为45.0 - 44+k.0 (含两端)。
常量池:class文件的基石?作用是?
常量池:存放所有常量
- 常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用。
- 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。
- 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。
由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。
为什么需要常量池计数器?
constant_pool_count (常量池计数器)
由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。
常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项。
Demo的值为:
其值为0x0016,掐指一算,也就是22。 需要注意的是,这实际上只有21项常量。索引为范围是1-21。为什么呢? 通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。
常量池表
constant_pool [](常量池)
constant_pool是一种表结构,以 1 ~ constant_pool_count - 1为索引。表明了后面有多少个常量项。
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte (标记字节、标签字节)。
字面量和符号引用
对这些常量解读前,我们需要搞清楚几个概念。
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。如下表:
String str = "atguigu";
final int NUM = 10;
- 全限定名
com/atguigu/test/Demo这个就是类的全限定名,仅仅是把包名的"."替换成"/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。
- 简单名称
简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别是add和num。
- 描述符
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表: (数据类型:基本数据类型 、 引用数据类型)
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如:
方法java.lang.String toString()的描述符为() Ljava/lang/String;,
方法int abc(int[] x, int y)的描述符为([II) I。
谈谈你对符号引用、直接引用的理解?
:::info Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。 这里说明下符号引用和直接引用的区别与关联:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
:::
常量类型和结构
根据上图每个类型的描述我们也可以知道每个类型是用来描述常量池中哪些内容(主要是字面量、符号引用)的。比如:CONSTANT_Integer_info是用来描述常量池中字面量信息的,而且只是整型字面量信息。
标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。
细节说明:
CONSTANT_Class_info 结构用于表示类或接口
CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info结构表示字段、方法和接口方法
CONSTANT_String_info结构用于表示String类型的常量对象
CONSTANT_Integer_info和CONSTANT_Float_info 表示4字节(int和float)的数值常量
CONSTANT_Long_info和CONSTANT_Double_info结构表示8字节(long和double)的数值常量
在class文件的常量池表中,所有的8字节常量均占两个表成员(项)的空间。如果一个CONSTANT_Long_info或CONSTANT_Double_info结构的项在常量池表中的索引位n,则常量池表中下一个可用项的索引位n+2,此时常量池表中索引为n+1的项仍然有效但必须视为不可用的。
CONSTANT_NameAndType_info结构用于表示字段或方法,但是和之前的3个结构不同,CONSTANT_NameAndType_info结构没有指明该字段或方法所属的类或接口。
CONSTANT_Utf8_info用于表示字符常量的值
CONSTANT_MethodHandle_info结构用于表示方法句柄
CONSTANT_MethodType_info结构表示方法类型
CONSTANT_InvokeDynamic_info结构用于表示invokedynamic指令所用到的引导方法(bootstrap method)、引导方法所用到的动态调用名称(dynamic invocation name)、参数和返回类型,并可以给引导方法传入一系列称为静态参数(static argument)的常量。
访问标识访问标识(access_flag、访问标志、访问标记)
在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。各种访问标记如下所示:
类的访问权限通常为 ACC_ 开头的常量。
每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。比如,若是public final的类,则该标记为ACC_PUBLIC | ACC_FINAL。
使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记。
补充说明:
- 带有ACC_INTERFACE标志的class文件表示的是接口而不是类,反之则表示的是类而不是接口。 1)如果一个class文件被设置了 ACC_INTERFACE 标志,那么同时也得设置ACC_ABSTRACT 标志。同时它不能再设置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 标志。 2)如果没有设置ACC_INTERFACE标志,那么这个class文件可以具有上表中除 ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。
- ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志。对于Java SE 8及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class文件的版本号是多少,Java虚拟机都认为每个class文件均设置了ACC_SUPER标志。 1)ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的 ACC_SUPER标志在由JDK 1.0.2之前的编译器所生成的access_flags中是没有确定含义的,如果设置了该标志,那么Oracle的Java虚拟机实现会将其忽略。
- ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。
- 注解类型必须设置ACC_ANNOTATION标志。如果设置了 ACC_ANNOTATION标志, 那么也必须设置ACC_INTERFACE标志。
- ACC_ENUM标志表明该类或其父类为枚举类型。
- 表中没有使用的access_flags标志是为未来扩充而预留的,这些预留的标志在编译器中应该设置为0, Java虚拟机实现也应该忽略它们。
类索引、父类索引、接口索引集合
在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:
这三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名。由于 Java语言不允许多重继承,所以父类索引只有一个,除了3/ 3. java.lang.Object 之外,所有的Java类都有父类,因此除了java.lang.Object 外,所有Java类的父类索引都不为 0。 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中。
1.this_class(类索引) 2字节无符号整数,指向常量池的索引。它提供了类的全限定名,如com/atguigu/java1/Demo。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个class文件所定义的类或接口。
2.super_class (父类索引) 2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是java/lang/Object类。同时,由于Java不支持多继承,所以其父类只有一个。 superclass指向的父类不能是final。
- interfaces 指向常量池索引集合,它提供了一个符号引用到所有已实现的接口 由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class (当然这里就必须是接口,而不是类)。
3.1 interfaces_count (接口计数器) interfaces_count项的值表示当前类或接口的直接超接口数量。
3.2 interfaces interfaces []中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为 interfaces_count。 每个成员 interfaces[i]必须为 CONSTANT_Class_info结构,其中 0 <= i < interfaces_count。在 interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左边的接口。
字段表集合
字段表集合
fields 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。(local variables) 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。
注意事项: 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。 在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
方法表集合
methods:指向常量池索引集合,它完整描述了每个方法的签名。
在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public、private或protected),方法的返回值类型以及方法的参数信息等。 如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法()和实例初始化方法())。
使用注意事项: 在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。 也就是说,尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和Java语法规范相反,字节码文件中却恰恰允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同。
属性表集合
方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该 class 文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。
此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。
属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。
oracle官方的反解析工具:javap
解析字节码的作用
通过反编译生成的字节码文件,我们可以深入的了解java代码的工作机制。但是,自己分析类文件结构太麻烦了!除了使用第三方的jclasslib工具之外,oracle官方也提供了工具:javap。
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息。
通过局部变量表,我们可以查看局部变量的作用域范围、所在槽位等信息,甚至可以看到槽位复用等信息。
javac -g操作
解析字节码文件得到的信息中,有些信息(如局部变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等等)需要在使用javac编译成class文件时,指定参数才能输出。
比如,你直接javac xx.java,就不会在生成对应的局部变量表等信息,如果你使用javac -g xx.java就可以生成所有相关信息了。如果你使用的eclipse或IDEA,则默认情况下,eclipse、IDEA在编译时会帮你生成局部变量表、指令和代码行偏移量映射表等信息的。
javap的用法
javap的用法格式: javap 其中,classes就是你要反编译的class文件。 在命令行中直接输入javap或javap -help可以看到javap的options有如下选项: :::info 一般常用的是-v -l -c三个选项。 javap -l 会输出行号和本地变量表信息。 javap -c 会对当前class字节码进行反编译生成汇编代码。 javap -v classxx 除了包含-c内容外,还会输出行号、局部变量表信息、常量池等信息。
:::
总结
1、通过javap命令可以查看一个java类反汇编得到的Class文件版本号、常量池、访问标识、变量表、指令代码行号表等等信息。不显示类索引、父类索引、接口索引集合、()、()等结构
2、通过对前面例子代码反汇编文件的简单分析,可以发现,一个方法的执行通常会涉及下面几块内存的操作: (1)java栈中:局部变量表、操作数栈。 (2)java堆。通过对象的地址引用去操作。 (3)常量池。 (4)其他如帧数据区、方法区的剩余部分等情况,测试中没有显示出来,这里说明一下。
3、平常,我们比较关注的是java类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行的,可以参考官方文档查看每个指令的含义,很简单: docs.oracle.com/javase/spec…
字节码指令集与解析概述
Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。
Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于 Java 虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。
由于限制了 Java 虚拟机操作码的长度为一个字节(即 0~255),这意味着指令集的操作码总数不可能超过 256 条。
官方文档:docs.oracle.com/javase/spec…
熟悉虚拟机的指令对于动态字节码生成、反编译Class文件、Class文件修补都有着非常重要的价值。因此,阅读字节码作为了解 Java 虚拟机的基础技能,需要熟练掌握常见指令。
字节码与数据类型
在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务: i代表对int类型的数据操作 l代表long类型的数据操作 s代表short类型的数据操作 b代表byte类型的数据操作 c代表char类型的数据操作 f代表float类型的数据操作 d代表double类型的数据操作
也有一些指令的助记符中没有明确地指明操作类型的字母,如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。
还有另外一些指令,如无条件跳转指令goto则是与数据类型无关的。
大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。
byte b1 = 12; short s1 = 10; int i = b1 + s1;
指令分类
由于完全介绍和学习这些指令需要花费大量时间。为了让大家能够更快地熟悉和了解这些基本指令,这里将JVM中的字节码指令集按用途大致分成 9 类。 加载与存储指令 算术指令 类型转换指令 对象的创建与访问指令 方法调用与返回指令 操作数栈管理指令 控制转移指令 异常处理指令 同步控制指令
(说在前面)在做值相关操作时: 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,可能是对象的引用)被压入操作数栈。 一个指令,也可以从操作数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系统调用等等操作。
字节码指令
加载与存储指令
加载和存储指令 1、作用 加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。
2、常用指令 1、【局部变量压栈指令】将一个局部变量加载到操作数栈:xload、xload_(其中x为i、l、f、d、a,n 为 0 到 3) 2、【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_ 3、【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_(其中x为i、l、f、d、a,n 为 0 到 3) 4、扩充局部变量表的访问索引的指令:wide。
上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_)。这些指令助记符实际上代表了一组指令(例如 iload_代表了iload_0、iload_1、iload_2和iload_3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。
比如: iload_0:将局部变量表中索引为0位置上的数据压入操作数栈中。 iload 0:将局部变量表中索引为0位置上的数据压入操作数栈中。 iload 4:将局部变量表中索引为4位置上的数据压入操作数栈中。
除此之外,它们的语义与原生的通用指令完全一致(例如 iload_0的语义与操作数为0时的 iload 指令语义完全一致)。在尖括号之间的字母指定了指令隐含操作数的数据类型,代表非负的整数, 代表是int类型数据, 代表long类型,代表float类型, 代表double类型。
操作byte、char、short和boolean类型数据时,经常用int类型的指令来表示。
算术指令
1、作用: 算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈。 2、分类: 大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点类型数据进行运算的指令。
3、byte、short、char和boolean类型说明 在每一大类中,都有针对Java虚拟机具体数据类型的专用算术指令。但没有直接支持byte、short、char和boolean类型的算术指令,对于这些数据的运算,都使用int类型的指令来处理。此外,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。
4、运算时的溢出 数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数。其实Java虚拟机规范并无明确规定过整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0时会导致虚拟机抛出异常ArithmeticException。
5、运算模式 向最接近数舍入模式:JVM要求在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的; 向零舍入模式:将浮点数转换为整数时,采用该模式,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果;
6、NaN值使用 当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用 NaN值来表示。而且所有使用NaN值作为操作数的算术操作,结果都会返回 NaN;
类型转换指令
1、类型转换指令说明 ① 类型转换指令可以将两种不同的数值类型进行相互转换。 ② 这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
宽化类型转换
宽化类型转换(Widening Numeric Conversions) 1.转换规则: Java虚拟机直接支持以下数值的宽化类型转换(widening numeric conversion,小范围类型向大范围类型的安全转换)。也就是说,并不需要指令执行,包括: 从int类型到long、float或者double类型。对应的指令为:i2l、i2f、i2d 从long类型到float、double类型。对应的指令为:l2f、l2d 从float类型到double类型。对应的指令为:f2d
简化为:int --> long --> float --> double
- 精度损失问题 2.1 宽化类型转换是不会因为超过目标类型最大值而丢失信息的,例如,从int转换到 long,或者从int转换到double,都不会丢失任何信息,转换前后的值是精确相等的。
2.2 从int、long类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失——可能丢失掉几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值。
尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java虚拟机抛出运行时异常。
3.补充说明 从byte、char和short类型到int类型的宽化类型转换实际上是不存在的。对于byte类型转为int,虚拟机并没有做实质性的转化处理,只是简单地通过操作数栈交换了两个数据。而将byte转为long时,使用的是i2l,可以看到在内部byte在这里已经等同于int类型处理,类似的还有short类型,这种处理方式有两个特点: 一方面可以减少实际的数据类型,如果为short和byte都准备一套指令,那么指令的数量就会大增,而虚拟机目前的设计上,只愿意使用一个字节表示指令,因此指令总数不能超过256个,为了节省指令资源,将short和byte当做int处理也在情理之中。 另一方面,由于局部变量表中的槽位固定为32位,无论是byte或者short存入局部变量表,都会占用32位空间。从这个角度说,也没有必要特意区分这几种数据类型。
窄化类型转换窄化类型转换(Narrowing Numeric Conversion)
1.转换规则 Java虚拟机也直接支持以下窄化类型转换: 从int类型至byte、short或者char类型。对应的指令有:i2b、i2s、i2c 从long类型到int类型。对应的指令有:l2i 从float类型到int或者long类型。对应的指令有:f2i、f2l 从double类型到int、long或者float类型。对应的指令有:d2i、d2l、d2f
- 精度损失问题 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度。
尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况,但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常
- 补充说明 3.1 当将一个浮点值窄化转换为整数类型T(T限于int或long类型之一)的时候,将遵循以下转换规则: 如果浮点值是NaN,那转换结果就是int或long类型的0。 如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入模式取整,获得整数值v,如果v在目标类型T(int或long)的表示范围之内,那转换结果就是v。否则,将根据v的符号,转换为T所能表示的最大或者最小正数
3.2 当将一个 double 类型窄化转换为 float 类型时,将遵循以下转换规则: 通过向最接近数舍入模式舍入一个可以使用float类型表示的数字。最后结果根据下面这3条规则判断: 如果转换结果的绝对值太小而无法使用 float来表示,将返回 float类型的正负零。 如果转换结果的绝对值太大而无法使用 float来表示,将返回 float类型的正负无穷大。 对于double 类型的 NaN值将按规定转换为 float类型的 NaN值。
对象的创建与访问指令
Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。
创建指令
一、创建指令 虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令:
- 创建类实例的指令: 创建类实例的指令:new 它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
- 创建数组的指令: 创建数组的指令:newarray、anewarray、multianewarray。 newarray:创建基本类型数组 anewarray:创建引用类型数组 multianewarray:创建多维数组
上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也非常高。
字段访问指令
二、字段访问指令 对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。
-
访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic
-
访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield
举例: 以getstatic指令为例,它含有一个操作数,为指向常量池的Fieldref索引,它的作用就是获取Fieldref指定的对象或者值,并将其压入操作数栈。
public void sayHello() {
System.out.println("hello");
}
对应的字节码指令: 0 getstatic #8 <java/lang/System.out> 3 ldc #9 5 invokevirtual #10 <java/io/PrintStream.println> 8 return
数组操作指令
三、数组操作指令 数组操作指令主要有:xastore和xaload指令。具体为: 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值存储到数组元素中的指令:bastore、 castore、 sastore、iastore、 lastore、fastore、dastore、aastore
即:
取数组长度的指令:arraylength 该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈。
- 说明 指令xaload表示将数组的元素压栈,比如saload、caload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令会弹岀栈顶这两个元素,并将a[i]重新压入栈。 xastore则专门针对数组操作,以iastore为例,它用于给一个int数组的给定索引赋值。在iastore执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore会弹出这3个值,并将值赋给数组中指定索引的位置。
类型检查指令
类型检查指令
检查类实例或数组类型的指令:instanceof、checkcast。 指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常。 指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈。
方法调用与放回指令
1.方法调用指令:invokevirtual、invokeinterface、invokespecial、invokestatic 、invokedynamic
以下5条指令用于方法调用: invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。 invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。 invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。 invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的。 invokedynamic:调用动态绑定的方法,这个是JDK 1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在 java 虚拟机内部,而 invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
2.方法返回指令: 方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。 包括ireturn(当返回值是 boolean、byte、char、short和int 类型时使用)、lreturn、freturn、dreturn和areturn 另外还有一条return 指令供声明为 void的方法、实例初始化方法以及类和接口的类初始化方法使用。
举例: 通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。
如果当前返回的是synchronized方法,那么还会执行一个隐含的monitorexit指令,退出临界区。
最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。
对应的代码:
public int methodReturn(){
int i = 500;
int j = 200;
int k = 50;
return (i + j) / k;
}
操作数栈管理指令
操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。
这类指令包括如下内容: 将一个或两个元素从栈顶弹出,并且直接废弃: pop,pop2; 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶: dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2; 将栈最顶端的两个Slot数值位置交换: swap。Java虚拟机没有提供交换两个64位数据类型(long、double)数值的指令。 指令nop,是一个非常特殊的指令,它的字节码为0x00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等。
这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。
说明: 不带_x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2。dup的系数代表要复制的Slot个数。 dup开头的指令用于复制1个Slot的数据。例如1个int或1个reference类型数据 dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个float类型数据 带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1, dup2_x1, dup_x2, dup2_x2。对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此 dup_x1插入位置:1+1=2,即栈顶2个Slot下面 dup_x2插入位置:1+2=3,即栈顶3个Slot下面 dup2_x1插入位置:2+1=3,即栈顶3个Slot下面 dup2_x2插入位置:2+2=4,即栈顶4个Slot下面 pop:将栈顶的1个Slot数值出栈。例如1个short类型数值 pop2:将栈顶的2个Slot数值出栈。例如1个double类型数值,或者2个int类型数值
控制转移指令
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为 1)比较指令、2)条件跳转指令、3)比较条件跳转指令、4)多条件分支跳转指令、5)无条件跳转指令等。
异常处理指令
1、抛出异常指令:
(1)athrow指令 在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。 除了使用throw语句显示抛出异常情况之外,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在 idiv或 ldiv指令中抛出 ArithmeticException异常。
(2)注意 正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java 虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。
异常及异常的处理: 过程一:异常对象的生成过程 ---> throw (手动 / 自动) ---> 指令:athrow 过程二:异常的处理:抓抛模型。 try-catch-finally ---> 使用异常表
1、处理异常: 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成的。
2、异常表 如果一个方法定义了一个try-catch 或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如: 起始位置 结束位置 程序计数器记录的代码处理的偏移地址 被捕获的异常类在常量池中的索引
当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。
不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标
同步控制指令
组成 java虚拟机支持两种同步结构:方法级的同步 和 方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的。
面试题
int a = 1;JVM如何取得a的值(圆通)
:::info
在 Java 中,变量 a
在运行时的值是存储在栈内存中的。Java 虚拟机(JVM)通过栈帧中的局部变量表来维护局部变量的值。对于基本数据类型(如 int
),局部变量表中直接存储变量的值。
假设有下 Java 代码:
:::
public class Example {
public static void main(String[] args) {
int a = 1;
// 其他代码...
}
}
:::info
在这个代码中,main
方法是程序的入口方法,JVM 在执行该方法时会创建一个栈帧。栈帧包含了局部变量表,而 a
就是一个局部变量。当执行 int a = 1;
时,将会在局部变量表中分配一个位置用于存储 a
的值。
要获取 a
的值,JVM 可以通过以下步骤:
- 访问局部变量表中的位置: JVM 根据
a
在局部变量表中的位置,找到分配给a
的槽位。 - 读取槽位中的值: JVM 从该槽位中读取存储的值,这就是
a
的值。 总体而言,JVM 通过访问局部变量表中的槽位来获取局部变量的值。需要注意的是,这是在运行时进行的操作,因为局部变量表的内容在方法执行期间动态变化。如果在代码的其他部分修改了a
的值,JVM 将会反映这些变化。 :::
Java虚拟机中,数据类型可以分为哪几类?
- Java虚拟机是通过某些数据类型来执行计算的,数据类型可以分为两种:基本类型和引用类型,基本类型的变量持有原始值,而引用类型的变量持有引用值。
- Java语言中的所有基本类型同样也都是Java虚拟机中的基本类型。但是boolean有点特别,虽然Java虚拟机也把boolean看做基本类型,但是指令集对boolean只有很有限的支持,当编译器把Java源代码编译为字节码时,它会用int或者byte来表示boolean。在Java虚拟机中,false是由整数零来表示的,所有非零整数都表示true,涉及boolean值的操作则会使用int。另外,boolean数组是当做byte数组来访问的。
- Java虚拟机还有一个只在内部使用的基本类型:returnAddress,Java程序员不能使用这个类型,这个基本类型被用来实现Java程序中的finally子句。该类型是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作码的指针。returnAddress类型不是简单意义上的数值,不属于任何一种基本类型,并且它的值是不能被运行中的程序所修改的。
- Java虚拟机的引用类型被统称为“引用(reference)”,有三种引用类型:类类型、接口类型、以及数组类型,它们的值都是对动态创建对象的引用。类类型的值是对类实例的引用;数组类型的值是对数组对象的引用,在Java虚拟机中,数组是个真正的对象;而接口类型的值,则是对实现了该接口的某个类实例的引用。还有一种特殊的引用值是null,它表示该引用变量没有引用任何对象。
为什么不把基本类型放堆中呢?
首先是栈、堆的特点不同。(堆比栈要大,但是栈比堆的运算速度要快。) 将复杂数据类型放在堆中的目的是为了不影响栈的效率,而是通过引用的方式去堆中查找。(八大基本类型的大小创建时候已经确立大小。三大引用类型创建时候无法确定大小) 简单数据类型比较稳定,并且它只占据很小的内存,将它放在空间小、运算速度快的栈中,能够提高效率。
Java中有没有指针的概念?
指针具有您可能(或可能不会)通常使用的额外操作;引用缺少这些操作,因为这些操作可能不安全。例如,如果您使用指针来索引数组的第一个元素,如下所示:int squares[] = {1, 4, 9, 16, 25, 36, 49};int* intPointer = squares;您可能想要取消引用指针并获取值“ 1”。还可以继续intPointer++获得值“4”,intPointer++获得值“9”。这是因为++操作将指针在内存中向前移动了一个“单位”。指针将一个地址存储在内存中,并且++操作将适当数量的字节添加到该地址。在许多系统上,int会加四个字节,但是如果指针是char指针,则只能加一个字节。由于指针的基础数据类型是内存中的地址,因此以下内容是合法的(但不建议这样做):char* charPointer = squares;charPointer++;void* voidPointer = squares;voidPointer++。