2019-03-11 | Java | UNLOCK

探索Java的平台无关性

编译Java文件

一般来说会将Java程序文件的运行过程分为编译期运行期,编译期编译Java的源码,即生成字节码,并存入到对应的*.class文件中。
例如编译一个ByteCodeSample.java的文件:

1
2
3
4
5
6
7
8
9
public class ByteCodeSample {
public static void main(String[] args) {
int i = 1, j = 5;
i++;
++j;
System.out.println(i);
System.out.println(j);
}
}

编译命令javac xxx/ByteCodeSample.java

编译期错误

编译期的作用不仅是编译生成一个.class文件,还有一个作用就是检查代码有没有语法错误,不过如果使用IDE的话,IDE一般都会在代码编写阶段就会检测出这些错误,并提示给程序员。如果没有解决这些错误就强行运行,那么就会报编译期的错误,举例如下:

将上面代码中的 ++j 改成 ++++j,然后运行 javac xxx/ByteCodeSample.java

mark

这也就是所谓的编译期错误!

运行class文件

class文件保存的是Java文件生成的二进制字节码,Java类文件中的属性和方法以及常量信息都会被存储在其中,当然也会添加一个公有的静态属性.class,这个属性记录了类的相关信息、类型信息,是class的一个实例。

执行命令java 包名.ByteCodeSample
执行这个命令后虚拟机会将类信息加载进内存,然后按照指令运行类,就能将程序的运行结果。
在这里不小心踩了一个坑,运行很多次都是:

1
2
E:\workshop\Java\Javafordeeper\src>java ByteCodeSample
错误: 找不到或无法加载主类 ByteCodeSample

然后上网查找,找了很多答案其实都没答道要点上,要想成功编译,有两种方式:

方式一:java -cp [路径] [类名]

这里使用了 -cp 指定了路径参数,特别的如果是绝对路径,那这样在任意路径下都可以执行这个命令。但是需要注意,类的绝对路径或者相对路径不包含包路径,即只需到src目录下即可。类名需要加上包名(如果有),比我的代码

1
E:\workshop\Java\Javafordeeper\src\com\java\deep\bytecode>java -cp E:\workshop\Java\Javafordeeper\src com.java.deep.bytecode.ByteCodeSample

方式二:到src目录下,也就是包名前的目录:

1
2
3
E:\workshop\Java\Javafordeeper\src>java com.java.deep.bytecode.ByteCodeSample
2
6

不过这里也提醒我了,包名不只是简单的文件夹。

探索.class文件

如果直接打开.class文件的话,则会出现乱码或者是一堆二进制数,但是可以使用IDE打开,IDE会自动将这个二进制文件发编译成java文件。也可以使用原生的指令打开,那就是javap。javap是JDK自带的反编译器,用法如下:

1
2
javap -help #查看javap的指令
javap -c #对代码进行反汇编

下面用javap命令,执行成功后生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
E:\workshop\Java\Javafordeeper\src>javap -c com.java.deep.bytecode.ByteCodeSample
Compiled from "ByteCodeSample.java"
public class com.java.deep.bytecode.ByteCodeSample {
public com.java.deep.bytecode.ByteCodeSample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iconst_5
3: istore_2
4: iinc 1, 1
7: iinc 2, 1
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_1
14: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
20: iload_2
21: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
24: return
}

可以看到反编译后的代码中存在很多虚指令

解读class文件

Compiled from "ByteCodeSample.java"表示这个字节码文件从哪个java文件编译而来。

接着是构造了一个类,并且类的全名是:com.java.deep.bytecode.ByteCodeSample

然后又生成了一个无参默认构造函数,并且在这个构造器中需要执行三条指令:

  1. aload_0 表示对this进行操作
  2. invokespecial表示调用父类方法super
  3. return 返回

然后又生成了一个main方法,它是public和static的,这里涉及到了一些栈的操作:

  1. iconst_1表示把 常量1 放到栈顶
  2. istore_1表示将栈顶的值放到局部变量1,局部变量1也就是i
  3. iconst_5表示把 常量5 放到栈顶
  4. istore_2表示将栈顶的值放到局部变量2,局部变量1也就是j
  5. iinc 1, 1 表示将局部变量1加1
  6. iinc 2, 1 表示将局部变量2加1
  7. getstatic 获取PrintStream的静态域对象
  8. iload_1 将本地变量1(==i)的值推送至栈顶
  9. invokevirtual 调用PrintStream静态域对象的println方法输出栈顶元素,即输出i

总结

首先由人工编写出源代码.java文件,然后经过javac的编译就会生成字节码,并把字节码保存在.class文件中。.class文件是实现跨平台的基础,有了.class文件,JVM才能转换成特定平台的执行指令。由于Java实现了不同平台的虚拟机,因此就间接实现了java的跨平台运行。Java语言的跨平台性也就是.class文件的跨平台性。最后通过java命令,让虚拟机把类加载进内存,然后执行字节码中的指令,就能得到相应的结果。

mark

问:

1. Java语言的平台无关性?

Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同平台的平台上运行是不需要重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
因此,JVM运行的对象是.class文件,屏蔽了与具体平台相关的信息,减少原生语言开发的复杂性,使得我们不需要了解虚拟机的运行原理。

2. JVM如何加载 .class文件?

JVM主要由Class Loader、Execution Engin、Native Interface、Runtime Data Area等4部分组成,通过Class Loader符合要求的.class文件加载到内存,并通过Execution Engin去解析class文件中的字节码,然后提交给操作系统去执行。如下图所示是.class的加载过程:
mark

Java虚拟机是内存中的虚拟机,因此JVM先通过Class Loader依据特定的格式,加载class文件到内存。

  • Class Loader:依据特定的格式,加载class文件到内存
  • Execution Engin:对命令进行解析
  • Native Interface:融合不同开发语言的原生库为Java所用
  • Runtime Data Area:JVM内存空间结构模型
  1. 了解native方法

评论加载中