所有文章

Java - 从CLASS到机器码

你真的了解Java代码的执行过程吗?我们都知道,Java代码是被编译成字节码后被JVM所执行,那JVM又是怎么执行的?不是说CPU只能认识机器码吗?那么从CLASS到机器码之间发生了什么?

记得刚开始学Java的时候,老师告诉我们说:“Java是一种解释型语言,我们写的Java代码会被编译成字节码,然后由JVM解释执行”。这样说当然没错啊,只是还不够全面而已。当时在学习Java,同时我也在自学C语言(这么经典的编程语言怎么可以不学),想看看它们之们到底有什么不同之处,后来了解到C语言在被编译时,中间发生了很多事情,最后变成了二进制码,才能被CPU直接执行。可Java呢?老师明明说Java的源码是被编译成了字节码,CPU怎么可能直接认识字节码呢?上网搜了很多资料,都在说Class被JVM加载的过程,千篇一律,这个问题真的困惑我很久,后来有幸看到一篇文章(链接在本文尾部),才渐渐明白了困惑我很久问题。

了解编译过程

我们先简单回顾一下C语言的执行过程: 假设在Linux系统上用GCC编译一个C文件,下面结论只为帮助理解。

编写源文件[a.c] -> 预编译 -> [a.i] -> 编译 -> [a.s] -> 汇编(可选) -> [a.o] -> 链接 -> 机器码 -> CPU

再回顾一下Java的执行过程: 编写源文件[a.java] -> 编译 -> [a.class/a.jar] -> JVM -> ? -> CPU

其中,在编译过程中,也有类似C语言中的链接的过程,比如自定a.java中引用了第三方jar包中的类,那么需要在编译时将这个第三方jar包与a.java打包在一起(类似C中的静态链接),否则需要在运行a.class时,通过指定运行时参数引用到这个第三方jar包(类似C中的动态链接) 关键在class被JVM加载到内存以后,JVM会把字节码翻译成机器码,然后被CPU执行,只不过这一步发生在内存当中,翻译过的机器码也不会被保存在磁盘上(取决于JVM的实现),所以很少有人注意,为会什么JVM的默认实现不把翻译过的机器码保存在磁盘上呢?很简单,虽然是同一个class文件,但放在windowns下翻译出来的机器码跟Linux下翻译出来的机器码并不相同,这样就失去了Java程序的一大特性:可移植性。

JIT优化

说到这里,就得提一提JIT这个东西了,JIT是just in time的缩写, 也就是即时编译编译器,是JVM的一部分,它是的作用是什么呢?上面说过,class字节码是在被CPU执行前翻译成机器码的,那如果我们写了一段循环执行的代码,就这样一句一句翻译再执行必然会很慢,JIT的作用就是把那些需要频繁执行的代码一次性编译成机器码保存在内存当中,这样就避免了大量重复的翻译工作,也就加快了运行速度,而JIT编译代码本身也需要时间,所以那些只需要运行一次的代码则不经过JIT,由解释器解释执行。

那什么样的代码才算是“频繁执行的代码”?这里JIT有自己的判断标准,主要由两个计数器决定,一个是方法被调用的次数,另一个是方法中循环被回弹执行的次数,当次数到达一定界限时,该段代码才会被编译,这个阈值可以通过 -XX:CompileThreshold=N 这个选项指定,默认值为10000 最后再总结一下Java代码被执行的过程: 编写Java源码(示例):

int a = 1
int b = 2
int c = a + b

然后经过javac命令编译后变成了class文件,如果此时将其反汇编,将看到如下:

0 iload_1
1 iload_2
2 iadd
3 istore_3

当这个class被执行时,它在内存中会被一句一句翻成机器码,可能像下面这样:

0000,0000,000000010000
0000,0001,000000000001
0001,0001,000000010000
0001,0001,000000000001

这时才能被CPU直接执行!

参考资料

  1. C语言的编译链接过程详解
  2. 深入浅出 JIT 编译器

编写日期:2017-04-03