JVM-探究(五):读懂Java字节码

Java 文件最终会被编译成为 class 文件,无论多么复杂的 Java 程序最终都会在字节码中进行描述,因此字节码的描述能力应该是(事实上也是)Java 语言描述的超集,因此有很多其他语言也可以运行在 JVM 上,在这些语言中不缺少一些很“火”的语言。譬如:Groovy, JRuby, Jython, Clojure, Scala, Kotlin 等,这里不一一列述。有如此多的语言能够运行在 JVM 上,源于字节码强大的描述能力,本文首先介绍 Class 类的文件结构,然后通过文件结构进一步去解析 Class 类,我们甚至可以通过一些方式不修改源代码而去替换字节码中的部分字节,来实现动态的调整程序表现的目的。

Class 类文件的结构

Class 文件是以字节为单位的无空隙的连续二进制流。只包含两种类型:无符号数和表。

  • 无符号数用来描述数字、索引饮用、数量值或按照UTF-8编码的字符串值。
  • 由多个无符号数或者表组成的复合结构,所以表都习惯性地以_info结尾。

整个 Class 类文件可以看作一张大表

类型 名称 数量 含义
u4 magic 1 魔数,用来标识文件身份
u2 minor_version 1 次版本号
u2 major_version 1 主版本号
u2 constant_pool_count 1 常量个数
cp_info constant_pool constant_pool_count-1 常量信息
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interface_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

本文就是围绕着上面的这个表进行的。

注:高版本的 JDK 能够运行低版本的 Class 文件,但是低版本的 JDK 禁止运行高版本的 Class 文件,即使文件格式没有发生任何改变。等下我们会尝试下通过修改字节码中的版本,让低版本的 JDK 也可以运行高版本的 Class 文件。见实验一

常量池

实验

实验一: 通过修改class中的版本号让低版本的 JDK 也可以运行高版本的 Class 文件。

用 JDK10 编译下面的 Java 文件
javac Hello.java

1
2
3
4
5
6
class Hello {
public static void main(String[] args) {
System.out.println("Hello!");
System.out.println("Max value of int is " + Integer.MAX_VALUE);
}
}

查看字节码(部分)
vim -b Hello.class 使用:%!xxd可以使用十六进制查看文件。

1
2
3
00000000: cafe babe 0000 0036 0021 0a00 0800 1109  .......6.!......
00000010: 0012 0013 0800 140a 0015 0016 0700 1708 ................
00000020: ..... ....

可以看到主版本号和次版本号分别为 0 和 54,54 对应着 JDK10。
然后使用 JDK8 去运行上面的字节码文件。会抛出以下错误:
java Hello

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.UnsupportedClassVersionError: Hello has been compiled by a more recent version of the Java Runtime (class file version 54.0), this version of the Java Runtime only recognizes class file versions up to 52.0
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)

我们尝试将字节码的主版本号修改为 JDK8 让后再次尝试运行该字节码文件。
vim -b Hello.class 使用:%!xxd转换后修改,修改后使用:%!xxd -r转回并保存。
然后重新使用 JDK8 运行该文件,得到以下结果:

1
2
Hello!
Max value of int is 2147483647

我们并没有修改源文件,但是修改了字节码的版本号,就成功使用低版本的 JDK 运行了高版本 JDK 编译成的 class 文件。

未完待续

黄小豆 wechat
关注我的公众号,同步推送博客内容
坚持原创技术分享,您的支持将鼓励我继续创作!
0%