类字节码详解

类字节码详解
salt-fish1. 关于本笔记
本文件介绍的是类字节码的难点知识
2. 目录
多语言编译为字节码在JVM运行
计算机是不能直接运行java代码的,必须要先运行java虚拟机,再由java虚拟机运行编译后的java代码。这个编译后的java代码,就是本文要介绍的java字节码。
为什么jvm不能直接运行java代码呢,这是因为在cpu层面看来计算机中所有的操作都是一个个指令的运行汇集而成的,java是高级语言,只有人类才能理解其逻辑,计算机是无法识别的,所以java代码必须要先编译成字节码文件,jvm才能正确识别代码转换后的指令并将其运行。
- Java代码间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。
- JVM也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等。
Java字节码文件
class文件本质上是一个以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中。jvm根据其特定的规则解析该二进制数据,从而得到相关信息。
Class文件采用一种伪结构来存储数据,它有两种类型:无符号数和表。
Class文件的结构属性
在理解之前先从整体看下java字节码文件包含了哪些类型的数据:
例子
下面以一个简单的例子来逐步讲解字节码。
1 | //Main.java |
通过以下命令, 可以在当前所在路径下生成一个Main.class文件。
1 | javac Main.java |
以文本的形式打开生成的class文件,内容如下:
1 | cafe babe 0000 0034 0013 0a00 0400 0f09 |
- 文件开头的4个字节(“cafe babe”)称之为
魔数,唯有以”cafe babe”开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。 - 0000是编译器jdk版本的次版本号0,0034转化为十进制是52,是主版本号,java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为1.8.0。
通过java -version命令稍加验证, 可得结果。
1 | Java(TM) SE Runtime Environment (build 1.8.0_131-b11) |
继续往下是常量池… 知道是这么分析的就可以了,然后我们通过工具反编译字节码文件继续去看。
字节码详细信息
使用到java内置的一个反编译工具javap可以反编译字节码文件, 用法: javap <options> <classes>
其中 <options>选项包括:
1 | -help --help -? 输出此用法消息 |
输入命令 javap -verbose -p Main.class查看输出内容:
1 | Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class |
大致可以归纳为:
文件信息
- class文件位置, 最近修改时间, 文件大小
- MD5信息
- 类全限定名, JDK版本信息
- 访问标志
常量池
- 字面量
* - 符号引用
- 字面量
方法表集合
- 类内部各成员的详细属性
对类内部的方法描述,在字节码中以表的集合形式表现,暂且不管字节码文件的16进制文件内容如何,我们直接看反编译后的内容。
1
2
3private int m;
descriptor: I
flags: ACC_PRIVATE此处声明了一个私有变量m,类型为int,返回值为int
1
2
3
4
5
6
7
8
9
10
11
12
13public com.rhythm7.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rhythm7/Main;这里是构造方法:Main(),返回值为void, 公开方法。
code内的主要属性为:
- stack: 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1
- locals: 局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。
- args_size: 方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this
- attribute_info: 方法体内容,0,1,4为字节码”行号”,该段代码的意思是将第一个引用类型本地变量推送至栈顶,然后执行该类型的实例方法,也就是常量池存放的第一个变量,也就是注释里的
java/lang/Object."":()V, 然后执行返回语句,结束方法。 - LineNumberTable: 该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。可以使用 -g:none 或-g:lines选项来取消或要求生成这项信息,如果选择不生成LineNumberTable,当程序运行异常时将无法获取到发生异常的源码行号,也无法按照源码的行数来调试程序。
- LocalVariableTable: 该 属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或 -g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。 start 表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。
同理可以分析Main类中的另一个方法”inc()”:
方法体内的内容是:将this入栈,获取字段#2并置于栈顶, 将int类型的1入栈,将栈内顶部的两个数值相加,返回一个int类型的值。







