JVM基石:class文件
# JVM基石:class文件
Java是夸平台的语言,JVM是夸语言的平台。JVM能够成为夸语言的平台与字节码是息息相关的。
# 字节码文件的跨平台性
Java虚拟机不和包括Java在内的任何语言绑定,它只与“class”文件这种特定的二进制文件格式所关联。无论是使用哪种语言进行开发,只要能将源文件编译为正确的class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构就是Java虚拟机的基石、桥梁。
虽然JVM的家族庞大,但是所有的JVM全部遵循Java虚拟机规范 (opens new window),也就是说所有的JVM都可以加载符合JVM规范的字节码文件。而我们知道要想让一个Java程序正确地运行在JVM中,Java源码就必须被编译为符合JVM规范的字节码。
一般Java源码的编译过程如下:
- 前端编译器将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。
javac
是一种能够将Java源码编译为字节码的前端编译器。编译过程主要包含了四个步骤:词法解析、语法解析、语义解析以及生成字节码。
# Java的前端编译器
在学习了JVM的解释引擎之后,就知道我们通常说的“编译代码”,其实指的是前端编译器,也就是编译源代码生成字节码的阶段。而解释引擎的JIT编译器我们通常称为后端编译器,我们所指的编译期优化一般是指JIT运行时所做的优化。
HotSpot VM并没有强制要求前端编译器只能用javac
来编译字节码,其实只要编译结果符合JVM规范都可以被JVM所识别。在Java的前端编译器领域,除了javac
之外,还有一种经常被大家用到的前端编译器,那就是内置代Eclipse的ECJ(Eclipse Complier for Java)
编译器,和javac
全量编译方式不同,ECJ
是一种增量式编译器。
在Eclipse中,当开发人员编写完代码后,用“Ctrl + S” 快捷键时,
ECJ
编译器采取的编译方案是把未编译部分的源码逐行进行编译,而非每次都全量编译。因此ECJ
编译器效率会比javac
编译更加迅速和高效,当然编译质量和javac
相比大致是一样的。ECJ
编译器不仅是Eclipse的默认内置前端编译器,在Tomcat中同样也是使用ECJ
编译器来编译JSP文件。由于ECJ
编译器是采用GPLv2的开源协议进行源代码公开,所以,大家可以登录Eclipse官方下载ECJ
编译器的源码进行二次开发。默认情况下,IntelliJ IDEA使用的是
javac
编译器。(可以自己设置为AspectJ编译器,ajc)
# 代码细节-字节码
字节码有时候也是面试可能会问到的,因为很多时候可以通过对字节码的阅读,了解Java代码底层的执行操作,例如自动装箱拆箱、字符串细节、类加载变量初始化等等细节。
BAT面试题
类文件结构有哪几个部分
知道字节码吗?字节码有哪些?Integer x = 5; int y = 5; 比较x == y都经过那些步骤?
# 自动装箱拆箱
Integer x = 5;
int y = 5;
System.out.println(x == y);
字节码:
0 iconst_5
1 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
4 astore_1
5 iconst_5
6 istore_2
7 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
10 aload_1
11 invokevirtual #4 <java/lang/Integer.intValue : ()I>
14 iload_2
15 if_icmpne 22 (+7)
18 iconst_1
19 goto 23 (+4)
22 iconst_0
23 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
26 return
# 字符串细节
public static void main(String[] args) {
String str = new String("hello") + new String("world");
String str1 = "helloworld";
System.out.println(str == str1);
}
字节码,执行原因可阅读StringTable:
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <hello>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <world>
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
34 astore_1
35 ldc #10 <helloworld>
37 astore_2
38 getstatic #11 <java/lang/System.out : Ljava/io/PrintStream;>
41 aload_1
42 aload_2
43 if_acmpne 50 (+7)
46 iconst_1
47 goto 51 (+4)
50 iconst_0
51 invokevirtual #12 <java/io/PrintStream.println : (Z)V>
54 return
# 反解析class文件
Java源代码经过前端编译器后生成的字节码文件是一种二进制的类文件,它的内容是JVM指令(又称字节码指令,例如上面的例子),而不像C、C++经由编译器直接生成机器码。
- 字节码指令(byte code):Java虚拟机的指令由一个字节长度的、代表某种特定操作含义的操作码(opcode)以及跟随其后的零个或多个代表此操作所需的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
- 简单来说,字节码指令 = 操作码 [操作数]
通常有三种方式放解析class文件二进制字节码:
- 人工解析:使用文本编辑器打开,或者工具打开,根据一个个二进制去看,例如,Notepad++安装HEX-Editor插件、Binary Viewer。
- 使用
javap
指令:JDK自带的反解析工具。 - IDEA插件 jclasslib 或 jclasslib bytecode viewer客户端工具。