Skip to content

字节码篇

字节码文件是跨平台的吗

符合 JVM 字节码文件规范的字节码文件都可以在 JVM 上运行,不管是什么语言编写的。

java 编译器

java 是半编译半解释型语言,前端编译器与后端编译器

前端编译器

javac 编译器负责将 java 源码编译为字节码文件,默认情况下,idea 使用 javac 编译器。

前端编译器并不会涉及编译优化等方面的技术,而是将这些具体优化细节移交给 HotSpot 的 JIT 编译器负责。

为什么要懂字节码指令

包装类对象的缓存问题

java
Integer i1 = 1;
Integer i2 = 1;
System.out.println(i1 == i2); // true

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); //false

包装类型的缓存问题.png

对字符串及常量池的理解

java
String str = new String("hello") + new String("world");
String str1 = "helloworld";
System.out.println(str == str1); //false

-----------
String str = new String("hello") + new String("world");
str.intern();
String str1 = "helloworld";
System.out.println(str == str1); //true

-----------
String str = new String("hello") + new String("world");
String str1 = "helloworld";
str.intern();
System.out.println(str == str1); //false

class 文件结构细节概述

class文件结构.png

class 文件的魔数是什么?

文件类型标识符,比如 java 的 class 文件的魔数都是 cafebabe

常量池: class 文件的基石

常量池:存放所有常量

  • 常量池是 Class 文件中内容最为丰富的区域之一。常量池对于 Class 文件中的字段和方法解析也有至关重要的作用
  • 常量池可以理解为 Class 文件中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一
  • 常量池表项中,用于存放编译时期生成的各种字面量符号引用,这部分内容将在类加载后进入方法区的 运行时常量池 中存放

字面量和符号引用

字面量和符号引用.png

谈谈符号应用、直接引用的理解

Java 代码在进行 Javac 编译的时候,并不像 C/C++ 那样有 "连接"这一步骤,而是在虚拟机加载 Class 文件的时候进行动态链接。

也就是说,在 Class 文件中,不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址。

当虚拟机运行时,需要从常量池中获取对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

符号引用与直接引用.png

访问标识

访问标识.png

类索引、父类索引、接口索引集合

类索引.png

字段表集合

方法表集合

属性表集合

字节码指令

Java 虚拟机的指令是由一个字节长度的、代表着某种特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作所需参数(操作数)构成。

由于 Java 虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。操作码的个数不超过 256 个,因此由一个字节就可以表示完全。

JVM 中的字节码指令集按用途大致分为 9 类:

  • 加载与存储指令
  • 算术指令
  • 类型转换指令
  • 对象的创建与访问指令
  • 方法调用与返回指令
  • 操作数栈管理指令
  • 控制转移指令
  • 异常处理指令
  • 同步控制指令

方法调用指令

  • invokevirtual : 调用对象的实例方法,根据对象的实际类型进行分派,支持多态
  • invokeinterface : 调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用
  • invokespecial : 调用一些需要特殊处理的实例方法,比如构造方法、私有方法和父类方法。这些方法都是静态绑定的,不会在调用时进行动态派发
  • invokestatic : 调用命名类中的类方法,也就是(static)静态方法
  • invokedynamic : 调用动态绑定方法,用于在运行时动态解析除调用点限定符所引用的方法,比如: 方法引用
java
int a = 1; //问题一:JVM 如何取得 a 的值
//从局部变量表中获取

//问题二: 

Integer x = 128;
int y = 128;

System.out.println(x == y); //true


//字节码 
 0 sipush 128
        3 invokestatic #7 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
        6 astore_1
 7 sipush 128
        10 istore_2
11 getstatic #13 <java/lang/System.out : Ljava/io/PrintStream;>
        14 aload_1
15 invokevirtual #19 <java/lang/Integer.intValue : ()I> //调用 intValue 方法,自动拆箱
        18 iload_2
19 if_icmpne 26 (+7)
        22 iconst_1
23 goto 27 (+4)
        26 iconst_0
27 invokevirtual #23 <java/io/PrintStream.println : (Z)V>
        30 return

java 虚拟机中,数据类型可以分为哪几类?

数据类型.png

Java 语言中的所以基本类型同样也是 Java 虚拟机中的基本类型。但是指令集对 boolean 只有很有限的支持,当编译器把 java 源代码编译为字节码时,它会用 int 或者 byte 表示 boolean 。在 JVM 中,false 由整数零表示;true 由非零整数表示。涉及 boolean 的操作会用 int, boolean 数组会当作 byte 数组访问

JVM 还有一个只在内部使用的基本类型: returnAddress , 这个基本类型用来实现 Java 程序中的 finally 子句。

JVM 的引用类型统称为 "引用(reference)" ,有三种引用类型:类类型、接口类型、数组类型,它们的值都是对动态创建对象的引用。

  • 类类型是对类实例的引用
  • 数组类型的值是对数组对象的引用,在 JVM 中,数组是个真正的对象
  • 接口类型的值是对实现了该接口的某个实例的引用
  • 还有一种特殊的引用值是 null ,它表示该引用变量没有引用任何对象

为什么不把基本类型放堆中呢?

1、首先是栈、堆的特点不同 (堆比栈大,栈比堆的运算速度要快)

2、将复杂数据类型存放在堆中的目的是不影响栈的效率

3、简单数据类型比较稳定(八大基本类型的大小创建时就已经确立大小,三大引用类型创建时候无法确立大小),并且只占用很小的内存,存放在栈中效率更高

Java 中的参数传递是传值还是传引用?

传值

Java 中有没有指针的概念?

java 中没有指针,比如 C 通过指针增加偏移量可以指向内存中的不同位置;而 Java 没有该机制,是通过索引来访问内存的保存数据位置