字节码加载与执行
字节码
字节码是高级语言和 JVM 通信的桥梁,高级语言只做一件事:把代码编译成字节码。因此,完全可以编写一套自己的编程语言,定义他的语法规则,然后将实现一个编译器,将代码转为字节码即可。
来看看 Java 中的字节码长什么样子,源代码如下:
1 |
|
编译之后部分字节码如下:
图中的字节码,正在一行行地执行指令,这些指令作用可以参考 Java 字节码指令表。
不知你是否察觉,我们可以直接编写字节码,而不再需要高级语言编译成字节码,这当然是可行的,因为我们熟知的「动态代理」就是这么做的。
字节码加载
编译器把高级语言编译成字节码文件,那么 JVM 又是怎么加载和执行字节码的呢?
不知你注意到没,在使用 IDE 运行程序时,好像只要点击绿色箭头代码就可以运行了。IDE 好像有魔力一般,但实际上那个绿色箭头只是在后台「偷摸」地拼接一个命令行,启动一个 JVM ,仅此而已。
你可以通过控制台查看到 IDE 帮你拼接的命令行,命令行中有一个参数 **”-classpath”**,这个 classpath 就指明了 JVM 加载字节码路径。想更多了解 Java 启动命令相关知识,可以参考我之前写的博客。
使用 classpath 指明了加载路径,于是 JVM 便使用类加载器 (ClassLoader) 加载字节码。
类加载器只有一个作用,负责加载字节码文件,仅此而已。他也支持动态加载代码,动态生成代码等,用一段代码简单演示动态加载代码过程。
1 |
|
上诉代码在项目中没有引入任何依赖和 Jar 包,只是在项目根目录中放了两个不同版本的 FastJson jar 包。用户输入 FastJson 版本,类加载器去加载对应的 jar 包,获取成功打印 jar 包的版本信息,这就是个简单的动态加载过程。
上诉代码可以让 ClassLoader 加载指定版本的 jar 包,其实热部署的原理也类似于此。通过监听器监听对应的文件或文件夹,倘若发生改变,则调用自定义的 ClassLoader 进行重新加载,达到热部署的目的。
当然类加载器的场景还存在 Mock ,AOP 中,使用过 Mockito 或者 AOP 就会知道,他们都是通过字节码增强的方式生成目标对象的子类,然后交给 JVM 执行。
双亲委派加载模型
由于 ClassLoader 只负责加载字节码,因此完全有可能,编写一个恶意的字节码文件,让 ClassLoader 去加载执行它,破环我们的程序。为了程序的安全性,于是就有了「双亲委派加载模型」。
在 Java 8 及之前的双亲委派加载模型如图:
通过图中的关系,可以发现 JVM 的启动不止有一个类加载器,而是有三个。这三个类加载器各司其职,分工明确,且三个类加载器都是继承关系。
- Application ClassLoader (应用类加载器)是负责加载 classpath 里面的包,即我们编写好的代码,该加载器继承 Extension ClassLoader。
- Extension ClassLoader (扩展类加载器)负责加载与运行程序相关的 jar 包,像使用的 ArrayList,Object 等等,该类加载器继承 Bootstrap ClassLoader。
- ** Bootstrap ClassLoader(启动类加载器) **负责加载 JVM 启动时至关重要的包。
双亲委派加载模型简单描述就是:子加载器加载类之前,需要先去询问父加载器,如果父加载器不为空且找到该类,则直接返回,否则子加载器才去加载。
在这个过程中你发现了吗?越核心的类都是由父加载器去加载的,这样在一定程度上保存了程序的安全。倘若你伪造一个 java.lang.Object 类,想让类加载器去加载该 Object 类,这是做不到的。当 Application ClassLoader 去加载伪造的 Object 时,就会先去询问父加载器是否加载该类,显然该 Object 类已经被 Extension ClassLoader 加载,只不过加载的 Object 并非你伪造的 Object,因此你伪造的 Object 并不会被加载。
神奇的类
自定义一个 ClassLoader 并且实现了 loadClass 方法,如果加载的类名字为 BadClass 则使用自定义的类加载器去加载,否则由父加载器加载。然后将加载好的 BadClass 字节码文件,实例化一个 BadClass 对象。详细代码如下:
1 |
|
当你运行这段代码时,你会发现竟然报错了,在我的控制台中错误信息如下:
什么?两个相同的类包名也完全一样,竟然不能互转,,真是令人大跌眼镜。
那么为什么会这样呢?这是因为这两个 BadClass,不是由同一个类加载器加载的。BadClass 在 JVM 中是由 Application ClassLoader 加载的,上面的代码使用 MyClassLoader 覆盖 loadClass 逻辑,主动去加载 BadClass 并用加载的字节码实例化一个 BadClass 对象,但由于声明的对象和实例化的对象并非相同的类加载器加载的,就会导致类型转换异常。
同样的在 Java 的 instanceof 方法,类加载器也会被检查,若不是同一个类加载器加载的类,则 instanceof 判断为 false,代码如下:
1 |
|
JIT Compiler
有了类加载器加载字节码,还需要将字节码「翻译」成对应平台的指令。这个「翻译」过程由两种方式:编译执行和解释执行。
解释执行就是每执行一行就把该行翻译成机器指令,类似于「同声传译」。自然他会比较慢,因为每次执行都需要一个转换的过程,但是它对于跨平台是很方便的,因为,不用操心平台是否能看懂字节码,只需要带个同声传译的翻译即可。
编译执行就是把要翻译的内容,提前翻译好,然后直接给到平台。这样的好处是执行快,平台不用等你翻译,它直接拿翻译好的文件阅读即可。缺点也很明显,就是不灵活,在 Windows 平台运行,要提前翻译成 Windows 的指令,在 Liunx 平台运行,又要翻译成 Liunx 的指令,且翻译后的文件都比较臃肿。
那么 HotSpot 是用的那种方式呢?
答案是混合模式,它既不想丢失解释执行的便利性,也不想失去编译执行的速度,所以采取了折中的方案。他把一些常用的方法采用编译执行编译好,以提升执行的代码执行速度,其他的则采用解释执行。
JIT Compiler 全称是 Just In Time Compiler(即时编译器),HotSpot 就是使用该编译器动态地发现 JVM 运行时的热点,然后针对这些热点编译成相应的 native code ,提高运行效率。
小结
至此,字节码的加载与执行过程已经结束了,他的神秘面纱也被揭开。
高级语言通过编译器编译成字节码,JVM 使用类加载器去加载字节码。字节码存在安全性问题,因此 JVM 采用双亲委派机制去加载字节码。又因为每次执行都需要将字节码编译成机器指令,效率并不高,因此采用了即时编译 JIT 技术,这样既保证了效率又保证了跨平台性。
纵观整个过程发现,编程语言到可以执行的机器指令,经历了不止一次编译过程。把从高级语言到字节码的过程称为编译前端,从字节码到机器指令的过程称为编译后端。两个端各司其职,互不干扰,都是通过字节码这个「中介」交流。