Java源码 --> Java编译器 --> 字节码文件 --> JVM --> 机器可执行的二进制机器码 --> 操作系统
Java代码的编译,会经历非常复杂的过程,这里我们先不去深入研究。我们只要关心编译之后的class字节码文件。
.class文件
是二进制文件,在文件开头有特定的文件标识(CA FE BA BE)。下面以 Student.class
的字节码信息为例:
官方文档说明:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
// A class file consists of a single ClassFile structure:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
查看字节码文件如果直接使用编辑器去读,可读性极差。我们可以借助一些工具来帮我们查看类中的信息,而且还能看到反编译后的汇编指令。 通过汇编代码,我们可以深入的了解java代码的工作机制。
javap
是 JDK自带的反编译工具,可以直接使用,很方便。(可以在终端通过 javap --help
可以查看帮助信息)
$ javap --help
用法: javap <options> <classes>
其中, 可能的选项包括:
-? -h --help -help 输出此帮助消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
--module <模块>, -m <模块> 指定包含要反汇编的类的模块
--module-path <路径> 指定查找应用程序模块的位置
--system <jdk> 指定查找系统模块的位置
--class-path <路径> 指定查找用户类文件的位置
-classpath <路径> 指定查找用户类文件的位置
-cp <路径> 指定查找用户类文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
GNU 样式的选项可使用 = (而非空白) 来分隔选项名称
及其值。
每个类可由其文件名, URL 或其
全限定类名指定。示例:
path/to/MyClass.class
jar:file:///path/to/MyJar.jar!/mypkg/MyClass.class
java.lang.Object
可以根据自己的需要给javap加入参数使用,例如:
javap -p -v xxxx.class // 显示所有类和成员的附加信息
这个是最直接,最方便的查看工具了,但是可读性没有一些插件好。
jclasslib Bytecode viewer
, ASM Bytecode Viewer
这两款IDEA的插件也是非常方便的。
jclasslib
:比较强大,可以查看类中的所有信息ASMPlugin
:主要可以用来查看汇编指令,格式会比较友好。jclasslib Bytecode viewer 插件的使用
ASM Bytecode Viewer 插件的使用
Java代码 编译成 字节码文件后,可以在不同操作系统的JVM上执行(一次编译,到处执行)
Java虚拟机只关心字节码文件,根本不关心这个字节码文件到底是使用何种编程语言编写和编译的。
所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是用来执行一系列虚拟计算机指令的软件。
无论是 系统虚拟机 还是 程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。
Java虚拟机是执行二进制字节码的虚拟计算机,拥有独立的运行机制,其运行的字节码未必由 Java 语言编译而成。
Java虚拟机是执行二进制字节码的虚拟计算机,运行在操作系统之上,与硬件没有直接的交互。
1. 类加载器(Class Loader)
将字节码文件加载到内存中(即运行时数据区)
2. 运行时数据区(Runtime Data Area)
JVM在运行时所管理的内存,用于存储JVM相关数据
3. 执行引擎(Execution Engine)
字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,
需要通过执行引擎将 java的字节码指令 转换为 底层的系统指令,再交由 CPU 去执行。
4. 本地库接口JNI(Java Native Interface)
用于执行native修饰的方法(调用其他非java语言的本地库接口)
Java 编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。
具体来说:这两种架构之间的区别:
基于栈式架构的特点
基于寄存器架构的特点
典型的应用是 x86 的二进制指令集:比如传统的 PC 以及 Android 的 Davlik 虚拟机
花费更少的指令去完成一项操作,性能优秀,执行更高效。
指令集架构依赖于硬件,可移植性差。
在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主
而基于栈式架构的指令集却是以零地址指令为主
零地址指令:没有地址,只有操作数;一地址指令:1个地址和操作数;二地址指令:2个地址和操作数…
以 2+3 的计算流程为例:
基于栈的计算流程(以 Java 虚拟机为例):
iconst_2 //常量2入栈
istore_1
iconst_3 // 常量3入栈
istore_2
iload_1
iload_2
iadd //常量2/3出栈,执行相加
istore_0 // 结果5入栈
而基于寄存器的计算流程
mov eax,2 //将eax寄存器的值设为1
add eax,3 //使eax寄存器的值加3
小结:
由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的。
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
有如下的几种情况: