深度详解JVM类字节码

敢说敢作敢为, 无怨无恨无悔。这篇文章主要讲述深度详解JVM类字节码相关的知识,希望能为你提供帮助。
@TOC
JVM 基础 - 类字节码详解源代码通过编译器编译为字节码,再通过类加载子系统进行加载到JVM中运行。
多语言编译为字节码在JVM运行计算机是不能直接运行java代码的,必须要先运行java虚拟机,再由java虚拟机运行编译后的java代码。这个编译后的java代码,就是本文要介绍的java字节码。
为什么jvm不能直接运行java代码呢,这是因为在cpu层面看来计算机中所有的操作都是一个个指令的运行汇集而成的,java是高级语言,只有人类才能理解其逻辑,计算机是无法识别的,所以java代码必须要先编译成字节码文件,jvm才能正确识别代码转换后的指令并将其运行。

  • Java代码间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。
  • JVM也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等。
深度详解JVM类字节码

文章图片

Java字节码文件class文件本质上是一个以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中。jvm根据其特定的规则解析该二进制数据,从而得到相关信息。
Class文件采用一种伪结构来存储数据,它有两种类型:无符号数和表。这里暂不详细的讲。
本文将通过简单的java例子编译后的文件来理解。
Class文件的结构属性
在理解之前先从整体看下java字节码文件包含了哪些类型的数据:
深度详解JVM类字节码

文章图片

从一个例子开始
【深度详解JVM类字节码】下面以一个简单的例子来逐步讲解字节码。
//Main.java public class Main private int m; public int inc() return m + 1;

通过以下命令, 可以在当前所在路径下生成一个Main.class文件。
javac Main.java

以文本的形式打开生成的class文件,内容如下:
cafe babe 0000 0034 0013 0a00 0400 0f09 0003 0010 0700 1107 0012 0100 016d 0100 0149 0100 063c 696e 6974 3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 0100 0369 6e63 0100 0328 2949 0100 0a53 6f75 7263 6546 696c 6501 0009 4d61 696e 2e6a 6176 610c 0007 0008 0c00 0500 0601 0010 636f 6d2f 7268 7974 686d 372f 4d61 696e 0100 106a 6176 612f 6c61 6e67 2f4f 626a 6563 7400 2100 0300 0400 0000 0100 0200 0500 0600 0000 0200 0100 0700 0800 0100 0900 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003 0001 000b 000c 0001 0009 0000 001f 0002 0001 0000 0007 2ab4 0002 0460 ac00 0000 0100 0a00 0000 0600 0100 0000 0800 0100 0d00 0000 0200 0e

  • 文件开头的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命令稍加验证, 可得结果。
Java(TM) SE Runtime Environment (build 1.8.0_131-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

继续往下是常量池... 知道是这么分析的就可以了,然后我们通过工具反编译字节码文件继续去看。
反编译字节码文件
其中& lt; options& gt; 选项包括:
-help--help-?输出此用法消息 -version版本信息 -v-verbose输出附加信息 -l输出行号和本地变量表 -public仅显示公共类和成员 -protected显示受保护的/公共类和成员 -package显示程序包/受保护的/公共类 和成员 (默认) -p-private显示所有类和成员 -c对代码进行反汇编 -s输出内部类型签名 -sysinfo显示正在处理的类的 系统信息 (路径, 大小, 日期, MD5 散列) -constants显示最终常量 -classpath < path> 指定查找用户类文件的位置 -cp < path> 指定查找用户类文件的位置 -bootclasspath < path> 覆盖引导类文件的位置

输入命令javap -verbose -p Main.class查看输出内容:
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class Last modified 2018-4-7; size 362 bytes MD5 checksum 4aed8540b098992663b7ba08c65312de Compiled from "Main.java" public class com.rhythm7.Main minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref#4.#18// java/lang/Object."< init> ":()V #2 = Fieldref#3.#19// com/rhythm7/Main.m:I #3 = Class#20// com/rhythm7/Main #4 = Class#21// java/lang/Object #5 = Utf8m #6 = Utf8I #7 = Utf8< init> #8 = Utf8()V #9 = Utf8Code #10 = Utf8LineNumberTable #11 = Utf8LocalVariableTable #12 = Utf8this #13 = Utf8Lcom/rhythm7/Main; #14 = Utf8inc #15 = Utf8()I #16 = Utf8SourceFile #17 = Utf8Main.java #18 = NameAndType#7:#8// "< init> ":()V #19 = NameAndType#5:#6// m:I #20 = Utf8com/rhythm7/Main #21 = Utf8java/lang/Objectprivate int m; descriptor: I flags: ACC_PRIVATEpublic 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: StartLengthSlotNameSignature 050thisLcom/rhythm7/Main; public int inc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield#2// Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 8: 0 LocalVariableTable: StartLengthSlotNameSignature 070thisLcom/rhythm7/Main; SourceFile: "Main.java"

字节码文件信息
开头的7行信息包括:Class文件当前所在位置,最后修改时间,文件大小,MD5值,编译自哪个文件,类的全限定名,jdk次版本号,主版本号。
然后紧接着的是该类的访问标志:ACC_PUBLIC, ACC_SUPER,访问标志的含义如下:
标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为Public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义.
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举
常量池
Constant pool意为常量池。
常量池可以理解成Class文件中的资源仓库。主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念,如文本字符串,final常量等,而符号引用则属于编译原理方面的概念,包括以下三种:
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符号(Descriptor)
  • 方法的名称和描述符
不同于C/C++, JVM是在加载Class文件的时候才进行的动态链接,也就是说这些字段和方法符号引用只有在运行期转换后才能获得真正的内存入口地址。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中。 直接通过反编译文件来查看字节码内容:
#1 = Methodref#4.#18// java/lang/Object."< init> ":()V #4 = Class#21// java/lang/Object #7 = Utf8< init> #8 = Utf8()V #18 = NameAndType#7:#8// "< init> ":()V #21 = Utf8java/lang/Object

第一个常量是一个方法定义,指向了第4和第18个常量。以此类推查看第4和第18个常量。最后可以拼接成第一个常量右侧的注释内容:
java/lang/Object."< init> ":()V

这段可以理解为该类的实例构造器的声明,由于Main类没有重写构造方法,所以调用的是父类的构造方法。此处也说明了Main类的直接父类是Object。 该方法默认返回值是V, 也就是void,无返回值。
第二个常量同理可得:
#2 = Fieldref#3.#19// com/rhythm7/Main.m:I #3 = Class#20// com/rhythm7/Main #5 = Utf8m #6 = Utf8I #19 = NameAndType#5:#6// m:I #20 = Utf8com/rhythm7/Main

复制代码此处声明了一个字段m,类型为I, I即是int类型。关于字节码的类型对应如下:
标识字符 含义
B
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,以分号结尾,如Ljava/lang/Object;
对于数组类型,每一位使用一个前置的[字符来描述,如定义一个java.lang.String[][]类型的维数组,将被记录为[[Ljava/lang/String;
方法表集合
在常量池之后的是对类内部的方法描述,在字节码中以表的集合形式表现,暂且不管字节码文件的16进制文件内容如何,我们直接看反编译后的内容。
private int m; descriptor: I flags: ACC_PRIVATE

此处声明了一个私有变量m,类型为int,返回值为int
public 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: StartLengthSlotNameSignature 050thisLcom/rhythm7/Main;

这里是构造方法:Main(),返回值为void, 公开方法。
code内的主要属性为: