目录
什么是字节码?
为什么要了解字节码?
如何查看字节码?
字节码包括哪些内容?
总结
hello读者盆友们,在上一篇文章[Java基础]面向对象-内存解析_小王师傅66的博客-CSDN博客最后,我们通过查看字节码,来印证字符串拼接底层调用StringBuilder.append()方法实现。下面这篇文章,将重点学习Java字节码相关的内容,欢迎大家点评。
什么是字节码?
我们知道,计算机直接使用的程序语言语句是机器指令码,又称机器码/代码。机器码是用于指挥计算机执行操作和操作数地址的一组二进制数,只用0和1表示的数。开始人们用机器码编写程序,就是机器语言。机器码计算机能理解,但和人的语言差异太大,不便于人理解和记忆,使用机器码编程出错率高。后来,人们用助记符代替机器码编程,就是汇编语言。再后来,出现了各种更便于人理解的高级语言,如C,C++,Java等,使人们不用了解计算机的指令系统和具体结构也能编程。虽然语言逐渐向便于人类理解的方向发展,但计算机只能理解机器码,所以在执行时,汇编语言和高级语言都需要解释和翻译成机器码才能在计算机上执行。了解了机器码的发展基础之后,我们再来看字节码。
在前面的文章【Java基础】Java总览_小王师傅66的博客-CSDN博客我们讲到过,Java语言非常重要的特点:Write Once,Run Any Where 一次编译,到处执行。这个特点体现在,SUN公司以及虚拟机供应商发布了许多可以运行在不同平台的虚拟机,这些虚拟机可以载入和执行同一种字节码,而与平台无关。而且不仅Java语言,其他语言编译成字节码文件,也能在Java虚拟机载入和执行,虚拟机并不关心由什么语言编译成的字节码文件。

Java程序经过Javac编译器,编译成字节码文件 ,后缀是.class
为什么要了解字节码?
1. 可以读懂Java代码的执行过程。
通过查看字节码,我们可以真正理解Java代码的执行流程,理解方法调用的过程,理解异常处理的过程等。这有助于我们写出高质量的Java代码。
2. 可以做Java语言层面的优化。
通过分析字节码,我们可以找到Java代码的瓶颈,并对其进行优化。比如可以通过调整方法参数顺序避免不必要的对象创建,可以选择更高效的数据结构和算法等。
3. 可以研究Java虚拟机。
Java字节码是Java虚拟机执行的指令,通过学习字节码,我们可以更好的理解Java虚拟机的工作原理,甚至可以研究虚拟机的优化技术。
4. 可以编写字节码操作框架。
有很多框架需要操作字节码,比如ASM框架,Javassist框架等。如果我们要使用这些框架,就必须要了解Java字节码。
5. 有助于学习其他JVM语言。
很多JVM语言最后也是编译成字节码执行,所以学习Java字节码也有助于我们学习Scala、Kotlin、Groovy等JVM语言。
6. 安全性方面也很重要。
很多Java安全机制都是基于字节码实现的,比如沙箱安全模型等。了解字节码可以帮助我们更好的理解Java的安全机制,写出更安全的代码。
如何查看字节码?
1. javap命令。
javap是Java自带的可以反编译class文件查看字节码的命令。我们可以通过`javap -c ClassName` 或 `javap -v ClassName`来查看类的字节码。
注:- javap -c 只输出每个方法的字节码指令,不包含任何注释和变量表信息。这适用于仅仅分析方法的执行逻辑。- javap -v 除了输出方法的字节码指令外,还会输出方法的注释信息,变量表,异常表等详细信息。使用这个命令可以更全面地分析一个类,理解它的设计和实现。
2. IDE插件。
很多Java IDE都有查看字节码的插件,比如IDEA有jclasslib插件,Eclipse有Bytecode Outline插件。使用这些插件可以直接在IDE中查看字节码。
3. 反编译工具。
比如CFR、Procyon等反编译工具,可以反编译class文件为Java代码,通过查看生成的Java代码可以间接分析字节码。
4. 字节码分析框架。
有些框架可以直接分析字节码,生成报告。比如ASM Bytecode Outline插件,可以将字节码转换为htm报告,方便分析。
5. JVM TI工具。
JVM TI(JVM Tool Interface)工具可以在运行时分析JVM的内部状态,通过这些工具也可以查看和分析字节码。常见的工具有jvisualvm,YourKit等。
注:1,2,4亲测有效,安装文档很多,大家可自行百度适合自己的
字节码包括哪些内容?
我们接着用上一篇文章中的例子进行分析[Java基础]面向对象-内存解析_小王师傅66的博客-CSDN博客这篇文章,将为大家讲解,Java代码执行过程中,内存空间占用的情况,图文并茂,值得一看。https://blog.csdn.net/WKX18330698534/article/details/131127876
代码:
package selfTest;
/**
* 类 名 称:Circle
* 类 描 述:
* 创建时间:2023/6/7 9:25 上午
* 创 建 人:admin
*/
public class Circle {
private CirclePoint o;
private double radius;
public CirclePoint getO() {
return o;
}
public void setO(double i, double j) {
o.setX(i);
o.setY(j);
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
public double getArea() {
return 3.14 * radius * radius;
}
// 无参构造函数
Circle(CirclePoint o, double r) {
this.o = o;
this.radius = r;
}
// 自定义初始值的构造函数
Circle(double r) {
o = new CirclePoint(0, 0);
radius = r;
}
Circle() {
}
}
class CirclePoint {
private double x;
private double y;
CirclePoint() {
}
CirclePoint(double x1, double y1) {
this.x = x1;
this.y = y1;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public void setX(double x) {
this.x = x;
}
public void setY(double y) {
this.y = y;
}
}
class TestCircle {
public static void main(String[] args) {
Circle c1 = new Circle(new CirclePoint(1.0, 2.0), 2.0);
Circle c2 = new Circle(5.0);
System.out.println("c1:(" + c1.getO().getX() + "," + c1.getO().getY() + ")," + c1.getRadius());
System.out.println("c2 area = " + c2.getArea());
double newI = 5d;
double newJ = 6d;
c1.setO(newI, newJ);
c2.setRadius(9.0);
}
}
我们用命令分析一下: javap -v TestCircle.class
Classfile /Users/admin/Projects/testProject/target/classes/selfTest/TestCircle.class
Last modified 2023-6-20; size 1329 bytes
MD5 checksum 058d64231223967f4d87092a0eb77dfe
Compiled from "Circle.java"
class selfTest.TestCircle
minor version: 0 // 次版本号
major version: 52 // 主版本号
flags: ACC_SUPER
Constant pool: // 常量池
#1 = Methodref #34.#54 // java/lang/Object."<init>":()V
#2 = Class #55 // selfTest/Circle
#3 = Class #56 // selfTest/CirclePoint
#4 = Double 2.0d
#6 = Methodref #3.#57 // selfTest/CirclePoint."<init>":(DD)V
#7 = Methodref #2.#58 // selfTest/Circle."<init>":(LselfTest/CirclePoint;D)V
#8 = Double 5.0d
#10 = Methodref #2.#59 // selfTest/Circle."<init>":(D)V
#11 = Fieldref #60.#61 // java/lang/System.out:Ljava/io/PrintStream;
#12 = Class #62 // java/lang/StringBuilder
#13 = Methodref #12.#54 // java/lang/StringBuilder."<init>":()V
#14 = String #63 // c1:(
#15 = Methodref #12.#64 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#16 = Methodref #2.#65 // selfTest/Circle.getO:()LselfTest/CirclePoint;
#17 = Methodref #3.#66 // selfTest/CirclePoint.getX:()D
#18 = Methodref #12.#67 // java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder;
#19 = String #68 // ,
#20 = Methodref #3.#69 // selfTest/CirclePoint.getY:()D
#21 = String #70 // ),
#22 = Methodref #2.#71 // selfTest/Circle.getRadius:()D
#23 = Methodref #12.#72 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#24 = Methodref #73.#74 // java/io/PrintStream.println:(Ljava/lang/String;)V
#25 = String #75 // c2 area =
#26 = Methodref #2.#76 // selfTest/Circle.getArea:()D
#27 = Double 6.0d
#29 = Methodref #2.#77 // selfTest/Circle.setO:(DD)V
#30 = Double 9.0d
#32 = Methodref #2.#78 // selfTest/Circle.setRadius:(D)V
#33 = Class #79 // selfTest/TestCircle
#34 = Class #80 // java/lang/Object
#35 = Utf8 <init>
#36 = Utf8 ()V
#37 = Utf8 Code
#38 = Utf8 LineNumberTable
#39 = Utf8 LocalVariableTable
#40 = Utf8 this
#41 = Utf8 LselfTest/TestCircle;
#42 = Utf8 main
#43 = Utf8 ([Ljava/lang/String;)V
#44 = Utf8 args
#45 = Utf8 [Ljava/lang/String;
#46 = Utf8 c1
#47 = Utf8 LselfTest/Circle;
#48 = Utf8 c2
#49 = Utf8 newI
#50 = Utf8 D
#51 = Utf8 newJ
#52 = Utf8 SourceFile
#53 = Utf8 Circle.java
#54 = NameAndType #35:#36 // "<init>":()V
#55 = Utf8 selfTest/Circle
#56 = Utf8 selfTest/CirclePoint
#57 = NameAndType #35:#81 // "<init>":(DD)V
#58 = NameAndType #35:#82 // "<init>":(LselfTest/CirclePoint;D)V
#59 = NameAndType #35:#83 // "<init>":(D)V
#60 = Class #84 // java/lang/System
#61 = NameAndType #85:#86 // out:Ljava/io/PrintStream;
#62 = Utf8 java/lang/StringBuilder
#63 = Utf8 c1:(
#64 = NameAndType #87:#88 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#65 = NameAndType #89:#90 // getO:()LselfTest/CirclePoint;
#66 = NameAndType #91:#92 // getX:()D
#67 = NameAndType #87:#93 // append:(D)Ljava/lang/StringBuilder;
#68 = Utf8 ,
#69 = NameAndType #94:#92 // getY:()D
#70 = Utf8 ),
#71 = NameAndType #95:#92 // getRadius:()D
#72 = NameAndType #96:#97 // toString:()Ljava/lang/String;
#73 = Class #98 // java/io/PrintStream
#74 = NameAndType #99:#100 // println:(Ljava/lang/String;)V
#75 = Utf8 c2 area =
#76 = NameAndType #101:#92 // getArea:()D
#77 = NameAndType #102:#81 // setO:(DD)V
#78 = NameAndType #103:#83 // setRadius:(D)V
#79 = Utf8 selfTest/TestCircle
#80 = Utf8 java/lang/Object
#81 = Utf8 (DD)V
#82 = Utf8 (LselfTest/CirclePoint;D)V
#83 = Utf8 (D)V
#84 = Utf8 java/lang/System
#85 = Utf8 out
#86 = Utf8 Ljava/io/PrintStream;
#87 = Utf8 append
#88 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#89 = Utf8 getO
#90 = Utf8 ()LselfTest/CirclePoint;
#91 = Utf8 getX
#92 = Utf8 ()D
#93 = Utf8 (D)Ljava/lang/StringBuilder;
#94 = Utf8 getY
#95 = Utf8 getRadius
#96 = Utf8 toString
#97 = Utf8 ()Ljava/lang/String;
#98 = Utf8 java/io/PrintStream
#99 = Utf8 println
#100 = Utf8 (Ljava/lang/String;)V
#101 = Utf8 getArea
#102 = Utf8 setO
#103 = Utf8 setRadius
{
selfTest.TestCircle();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 79: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LselfTest/TestCircle;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=8, locals=7, args_size=1
0: new #2 // class selfTest/Circle ---创建Circle对象
3: dup // ---复制栈顶元素
4: new #3 // class selfTest/CirclePoint ---创建CirclePoint对象
7: dup // ---复制栈顶元素
8: dconst_1 // ---将double类型常量1入栈(将1.0入栈)
9: ldc2_w #4 // double 2.0d ---将常量池中的double类型的项入栈
12: invokespecial #6 // Method selfTest/CirclePoint."<init>":(DD)V ---调用CirclePoint的构造函数
15: ldc2_w #4 // double 2.0d ---将常量池中的double类型的项入栈
18: invokespecial #7 // Method selfTest/Circle."<init>":(LselfTest/CirclePoint;D)V ---调用Circle的构造函数
21: astore_1 // ---将引用类型或returnAddress类型值存入局部变量表1(将c1存入局部变量表)
22: new #2 // class selfTest/Circle
25: dup // ---复制栈顶元素
26: ldc2_w #8 // double 5.0d ---将常量池中的double类型的项入栈
29: invokespecial #10 // Method selfTest/Circle."<init>":(D)V ---调用Circle的构造函数
32: astore_2 // ---将引用类型或returnAddress类型值存入局部变量表2(将c2存入局部变量表)
33: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; ---从类中获取静态字段
36: new #12 // class java/lang/StringBuilder ---创建StringBuilder对象
39: dup // ---复制栈顶元素
40: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V ---调用StringBuilder的构造函数
43: ldc #14 // String c1:( --- 将String型常量"c1:("从运行时常量池push到栈顶
45: invokevirtual #15 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; ---调用StringBuilder的成员函数append()
48: aload_1 // 从局部变量1中装载引用类型值到栈顶(对应第21行局部变量表中的c1)
49: invokevirtual #16 // Method selfTest/Circle.getO:()LselfTest/CirclePoint; --- 调用Circle的成员函数getO()
52: invokevirtual #17 // Method selfTest/CirclePoint.getX:()D --- 调用CirclePoint的成员函数getX()
55: invokevirtual #18 // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder; ---调用StringBuilder的成员函数append()
58: ldc #19 // String , --- 将String型常量","从运行时常量池push到栈顶
60: invokevirtual #15 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; ---调用StringBuilder的成员函数append()
63: aload_1 // 从局部变量1中装载引用类型值到栈顶(对应第21行局部变量表中的c1)
64: invokevirtual #16 // Method selfTest/Circle.getO:()LselfTest/CirclePoint; --- 调用Circle的成员函数getO()
67: invokevirtual #20 // Method selfTest/CirclePoint.getY:()D --- 调用CirclePoint的成员函数getY()
70: invokevirtual #18 // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder; ---调用StringBuilder的成员函数append()
73: ldc #21 // String ), --- 将String型常量"), "从运行时常量池push到栈顶
75: invokevirtual #15 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; ---调用StringBuilder的成员函数append()
78: aload_1 // 从局部变量1中装载引用类型值到栈顶(对应第21行局部变量表中的c1)
79: invokevirtual #22 // Method selfTest/Circle.getRadius:()D --- 调用Circle的成员函数getRadius()
82: invokevirtual #18 // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder; ---调用StringBuilder的成员函数append()
85: invokevirtual #23 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; ---调用StringBuilder的成员函数toString()
88: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V ---调用PrintStream的成员函数println()
91: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
94: new #12 // class java/lang/StringBuilder
97: dup
98: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V
101: ldc #25 // String c2 area =
103: invokevirtual #15 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
106: aload_2
107: invokevirtual #26 // Method selfTest/Circle.getArea:()D
110: invokevirtual #18 // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder;
113: invokevirtual #23 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
116: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
119: ldc2_w #8 // double 5.0d
122: dstore_3
123: ldc2_w #27 // double 6.0d
126: dstore 5
128: aload_1
129: dload_3
130: dload 5
132: invokevirtual #29 // Method selfTest/Circle.setO:(DD)V
135: aload_2
136: ldc2_w #30 // double 9.0d
139: invokevirtual #32 // Method selfTest/Circle.setRadius:(D)V
142: return
LineNumberTable:
line 81: 0
line 82: 22
line 84: 33
line 85: 91
line 86: 119
line 87: 123
line 88: 128
line 89: 135
line 91: 142
LocalVariableTable:
Start Length Slot Name Signature
0 143 0 args [Ljava/lang/String;
22 121 1 c1 LselfTest/Circle;
33 110 2 c2 LselfTest/Circle;
123 20 3 newI D
128 15 5 newJ D
}
SourceFile: "Circle.java"
反编译后的内容主要包括以下几方面的内容:
1. 魔数和版本号。
class文件开头的4个字节是魔数(0xCAFEBABE),用于确定该文件是否是一个能被虚拟机接受的文件。
紧接着是版本号,用于标识这个class文件遵循哪个版本的class文件格式。
2. 常量池。
常量池中存放了两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值、8种基本类型的值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名(Fully Qualified Name),字段的名称和描述符(Descriptor),方法的名称和描述符。
比如上面代码的字符串常量“c1:(” 就是字面量;
类和接口的全限定名:selfTest/CirclePoint;
字段的名称和描述符:c1 c2;
方法的名称和描述符: selfTest/Circle.getArea:();
上面的反编译结果中可以看出,计算机帮我们把103项常量都计算出来了。
3. 访问标志。
表示该类的访问权限(public、private等)以及是否是抽象类或接口等信息。包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
4. 字段表。
包含类变量以及实例变量信息,如名称、类型和访问权限等。
5. 方法表。
包含方法信息,如方法名称、返回值类型、参数类型、访问权限、方法体的字节码指令等。
6. 属性表。
包含类或方法的额外属性,比如Annotation等。
字段表,方法表,属性表用来描述一些不方便使用“固定字节”表达的内容。譬如描述方法的返回值是什么,有几个参数,每个参数的类型等。因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。
7. 接口信息。
对实现的接口进行索引。
8. 字段和方法属性。
包含字段和方法的附加属性,如方法的参数数量和本地变量表等。
9. 方法体。
包含方法的字节码指令,行号信息等。
10. 附加属性。
一些额外的类属性,格式不固定,由属性名和属性值组成。
总结
以上是字节码相关知识的总览,要想完全掌握字节码的知识,我们更需要了解:栈帧,操作数栈,局部变量表,堆等,掌握了代码在执行过程中如何在这几块区域中执行,就更清楚了。这些内容后面我们将讲到。
字节码指令的意义,大家可参考官网,多查几次就记住了,不用死记硬背,
Chapter 6. The Java Virtual Machine Instruction Set