Java虚拟机里的magic world

536 阅读7分钟

Java的虚拟机包含了很多芝士点,看起来也是一堆能压死人的内容>.<

关于虚拟机的内容是比较枯燥,请做好思想准备~ 其实要讲Java虚拟机,有很多内容,比如JVM内存结构、类加载机制、GC垃圾回收机制等等,可以分好几篇来写。

这篇讲一些基础的吧,主要内容有以下几点:

  • (1)Java VM的语言无关性
  • (2)Java Class文件结构概述
  • (3)解析Class文件每个字节的含义

(计划写下GC垃圾回收机制,和现在SH正在实行的垃圾分类很应景吖-、-)

0. 写在前面

都知道编程起源时,写代码是件相当费劲的事,因为要让机器“读”得懂,人得写机器码,后来发展到了高级编程语言,人写起来轻松一些,那机器就“读”不懂了,就需要中间来个“翻译机” —— Java虚拟机。

1. 语言无关性

不单单是Java语言,其它语言也可以被它“翻译”过去。 代码进来,字节码出去~

我们的口号是:“一次编写,到处运行!”

Write Once, Run Anywhere.

java不仅具有平台无关性,而且具有很强的语言无关性~

看图:

Java虚拟机并不在乎到底Class文件是怎么得来的,只要符合Class文件结构格式,就能在虚拟机中运行了。

那么class文件是什么格式的?

2. 神秘的class文件

既然一切皆对象,一切皆文件

有没有听过虚拟机里的类文件结构?

写个简单的java文件

public class Fruit {
    private int size;
}

用javac来编译下,得到对应的class文件—— Fruit.class

看花眼了?看看第一句是啥?

没看错,"cafebabe"! (Φ皿Φ) 这是什么暗号吗?!

key word

  • 基础单位:8个字节
  • 二进制流

(Class文件是一组以8个字节为基础单位的二进制流。)

2.1 类文件到底有什么信息?

Class文件中只有两种数据类型:无符号数和表。

无符号数好理解,'u'开头的,u1, u2, u4,都是,后面的数字表示占多少个字节。

那表是什么呢? 表的定义是由多个无符号数或其他表作为数据项构成的复合数据类型。 先记个规律,表习惯用'_info'结尾。

整个Class文件是有顺序的,看下面的结构:

整理下,Class文件由10个部分组成:

  • 1、魔数
  • 2、Class文件主次版本号
  • 3、常量池数 + 常量池表
  • 4、访问标记
  • 5、当前类名(用于确定这个类的全限定名)
  • 6、父类名(用于确定这个类的父类的全限定名)
  • 7、继承的接口数 + 接口索引集合 (用于描述这个类实现了哪些接口)
  • 8、包含的所有字段数 + 字段表集合 (用于描述接口或者类中声明的变量)
  • 9、包含的所有方法数 + 方法表集合
  • 10、包含的所有属性数 + 属性表集合

2.1.1 魔数(Magic!)

第一个值叫"magic",位于.class文件的头4个字节,就是前面看到的"cafebabe"~~~

很多文章翻译为“魔数”,其实它就是“暗号”,表明身份来的,和“黄河黄海,我是长江”来对身份一个道理 ~。~ 没有这个暗号,jvm就不认识惹!

只认令牌不管来着是何人! >_<

  • 唯一作用

    就是确定这个文件是否是能够被虚拟机识别的class文件

  • 是固定的"0xCAFEBABE"。

2.1.2 版本号(minor_version + major_version)

魔数后面紧跟着的就是版本号了,包括次版本号和主版本号,各占2个字节。

例子中,是0000和0033,0000表示次版本号为0,0033表示主版本号为51。

2.1.3 大名鼎鼎的常量池!(constant_pool)

constant_pool (cp_info类型,'cp',即constant pool缩写)

谁说不知道常量池的,站出来,保证不打你~

可以理解为 Class 文件之中的资源仓库,它是Class文件中出现的第一个表类型数据类型,也是占用 Class 文件空间最大的数据项目之一。

  • 常量池中的每一项常量都是一个表。
  • 字节码的常量池是从1开始计数的。(第0项常量空出来是表达“不引用任何一个常量池项目”。 )

cp_info伪代码:

cp_info {
    u1 tag; // 每个cp_info的第一个字节表明该常量项的类型
    u1 info[]; // 常量项的具体内容
}

常量池中的每一个常量都是一个表,一共有11种表结构。

(1)常量项类型和tag取值表
Constant Type Value
CONSTANT_Class_info 7
CONSTANT_Fieldref_info 9
CONSTANT_Methodref_info 10
CONSTANT_InterfaceMethodref_info 11
CONSTANT_String_info 8
CONSTANT_Integer_info 3
CONSTANT_Float_info 4
CONSTANT_Long_info 5
CONSTANT_Double_info 6
CONSTANT_NameAndType_info 12
CONSTANT_Utf8_info 1
CONSTANT_MethodHandle_info 15
CONSTANT_MethodType_info 16
CONSTANT_InvokeDynamic_info 18

详细包含的具体内容看下面的表:

常量项1

常量项2

(2) 常量项类型结构

表格看得不太舒服?还是看几个type结构的代码栗子~

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index; // 指向常量池中CONSTANT_Class_info类型的常量
    u2 name_and_type_index; // 指向常量池中CONSTANT_NameAndType_info常量
}

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_InterfaceMethodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

基本类型的结构

CONSTANT_Utf8_info {
    u1 tag;
    u2 length; // 表示字符数组的长度
    u1 bytes[length]; // 是使用UTF-8缩略编码表示的字符串
}

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}
(3) 访问标志(access_flag)

先来看访问标志都有什么,以及每个对应的16进制数是什么?

2.2 再对照class栗子

来拆分看看

// MainActivity.class

cafe babe   magic
0000        minor_version
0034        major_version (52 --- jdk 1.8; 50 --- jdk 1.6)
000f        constant_pool_count 15(从1开始)
            0x000f即十进制的15,代表常量池中有14项常量
0a          第一个常量的tag是0x0a,根据常量项tag表,常量项类型为CONSTANT_Methodref_info。
            由常量项tag表可知, 第一个常量项一共占5个字节(u1 + u2 + u2)
0003 000c   这4个字节是第一个常量项的具体内容
            因为是CONSTANT_Methodref_info类型,所以表示:class_index为#3,name_and_type_index为#12。
07          第二项tag是0x07,类型为CONSTANT_Class_info
000d        第二个常量项的内容
            
            后面的常量项以此类推

手动一个一个比对有点累-.-

通过javap工具来分析下class文件的内容~

javap -verbose Fruit.class

输出如下:

MD5 checksum a5d9286b216489ceb6bf2c8f281ac722
  Compiled from "Fruit.java"
public class com.isa.myapplication.entity.Fruit
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // com/isa/myapplication/entity/Fruit
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               size
   #5 = Utf8               I
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               SourceFile
  #11 = Utf8               Fruit.java
  #12 = NameAndType        #6:#7          // "<init>":()V
  #13 = Utf8               com/isa/myapplication/entity/Fruit
  #14 = Utf8               java/lang/Object

可以清楚地看到,一共有14个项常量项,没错吧~

前面分析到第一个常量项是CONSTANT_Methodref_info类型, 它的内容

class_index为0x03(#3),表示指向第3个常量,而第3个常量是CONSTANT_Class类型,name_index是#14,即指向第14个常量,是个CONSTANT_Utf8类型,解析出来是"java/lang/Object";

name_and_type_index为0x0c(#12),表示指向第12个常量, 继续解析:内容name_index为#6,第6个常量是Utf8 '';descriptor_index为#7,第7个常量是Utf8 '()V'。

所以看到第一条常量项是这样

#1 = Methodref          #3.#12         // java/lang/Object."<init>":()V

Fruit类的父类是Object。

Java 编译器在编译每个类时都会为该类至少生成一个实例初始化方法--即 "()" 方法,此方法与源代码中的每个构造方法相对应。

参考

  1. Oracle的官方文档 The class File Format
  2. 解析 Java 类和对象的初始化过程
  3. JAVA 虚拟机入门(1)----- 类文件结构
  4. 从字节码层面看“HelloWorld”
  5. Java虚拟机之Class类文件结构
  6. 深入理解Java虚拟机(类文件结构)