字节码有啥用呢?知道了字节码之后你会看到更广阔的天地。作为一个只会敲代码的打工人,你可能只会写java代码,但是知其然而不知其所以然也。大人,时代已经变了,曾经修个电脑就能上班的年代已经一去不复返。如果卷不动别人,就要被人卷。
字节码概述
JVM与字节码
我们都知道java的出现是围绕着一个愿景的:
一个代码写一遍,到处运行,因为那个年代不同的os上的底层的指令集是可能不一样的,这样子可就为难程序员了,同样的逻辑要写好几便。我们it领域解决问题都可以用分层的方式解决,一层不够那就再加一层,TCP就是这么弄出来的。java领域呢,也抽象出来了一层,往下屏蔽os,往上就可以进行统一化了,真正的实现,write one time,run everywhere。
java领域被抽象出来的东西就是JVM,而和jvm本身对接的东西我们称为字节码。
而java代码是更高的一层抽象,这一层屏蔽语言本身。类似于scala,groove,kotlin都可以运行在jvm上,这就是屏蔽了语言的好处。那么我们理一理,java从书写到运行的整个流程。
开发的人根据java语言规范编写 .java代码 -> jdk自带指令javac 进行编译,生成jvm识别的.class文件 -> 运行 java 指令将刚刚写的代码跑起来,之后就是jvm的领域了。
那么从上面的图中,我们就能看出来,理解字节码本身好处颇多。你可以知道你平时写java的背后都有些什么,有了这个基础之后,每行代码是怎么运行的,也是可以脑补出来的。同时思想上的补充也会弥补以前对jvm本身的认知。
以下是罗列出来的各项知识点:
- Class文件结构
- 从字节码角度看java代码
- 基本数据类型与运算符操作
- 流程控制
- 对象初始化
- 方法调用与多态原理
- 异常捕获与处理
- 并发关键字
- 字节码改写框架
- agent寄生插件
既然我们要深究字节码,那自然是需要知道字节码的结构的,在class文件结构设计上,你会学到很多思想
- 如何最小化表达大量信息
- 如何制造出一个稳定的,可支持扩展的文件格式
- 知道如何制造出可以尽可能向下兼容的字节码文件
之后我们就可以和java语言本身做一个对照,看看我们平时的java代码背后都发生了些什么,学的够深了,写起代码来就如有神灵加持。
字节码在一定程度上为开发的人开放了一个口子——在运行态动态修改字节码,完成一个又一个黑科技。skywalking就是这么来的,它相当于是在jvm最上面做了一个代理。背后用的就是agent插件,伴随着jvm运行而运行,因此叫做插件。插件的附着方式可以使用java运行指令携带,也可以使用socket连接,这就是arthas的连接方式。jdk里面的instrument包就是专门干这个事儿的。
因此,如果要玩这么一整套东西,就必须知道字节码指令本身,改写字节码可以借助与框架的力量,毕竟字节码的确复杂。那么这个改写字节码的框架的api就要尽可能熟悉。字节码改写框架又会有很多,挑一个自己熟悉的就可以。
逻辑上,如果要一个闭环,那么就需要知道class是怎么产生的,class文件是怎么在jvm里面运行的。
前者需要知道编译原理,再细节一点就是javac的编译原理,后者就更复杂了,需要知道jvm领域的大量知识进才能把这部分的知识产生一个闭环。
字节码的产生
当我们学习JVM,切入口会从JVM内部构造开始出发,知道JVM的一些行为来指导我们做各种优化。但是当不断深入,字节码的知识就逃不开。当学习完字节码之后,你会好奇字节码是怎么来的。兜兜转转又回到了最本质上的问题——java语言到底是怎么一步一步从低级语言成为当今IT里面的一束红花。
“编译原理”,大家应该都听过,it专业的基本课程之一。我不是it专业的,在不断学习过程中,我一直都想不明白很多事情,包括为什么我的鼠标移动一下,电脑上的鼠标小图标就会移动到我想移动到的位置上,java为什么那么写就能跑,我写成那样,我的机器就能明白我的意思。
编译原理概述
从上图上可以看到,编译原理可以大体上分为两个大部分:
- 前端部分
- 后端部分
前端部分搞定就已经可以制造出类似于python或者js的脚本语言,而后端部分就可以把语义树变成另外一种样子(更低级别的代码形式,例如机器码),然后在此基础上进行不断地优化优化优化,变成执行又快又稳的一种东西。
前端部分的三个概念在这里稍微描述一下,后端部分的步骤暂时就按字面意思理解就可以,问题不大,后面我们详细去说这个东西。
- 词法分析
指的是将你写的代码分割成各种有含义的片段,这种小片段称为token,执行完词法分析之后产生的是一连串的token,我们叫做token流。这是一个首尾相连的东西。例如,你的输入是age >= 45
,那么产出就会是
(Identifier age) <-> (GE >= ) <-> (IntLiteral 45)
()
括号里面的东西我们当成token,<->
可以理解为链表的链,关联前后的token
- 语法分析
经过词法分析,我们得到token流,接下去语法分析就是将平铺开来的token转化为树状形式,树是按照语法规范走的,这棵树我们称为语法树,即AST树。例如你的输入是 2+3*5,输出就是一颗树一样的东西。这种结构,计算机很容易处理。
- 语义分析
语义分析里面的东西就多了,这是需要理解上下文的。比方说全局变量,引用变量进行计算,变量有效域之类的东西。当然,我们后面会详细描述。
Javac的编译过程
java编译过程需要经历4个组件
- 词法分析器 -> 生成token流
- 语法分析器 -> 生成语法树
- 语义分析器 -> 注解语法树
- 字节码生成器 -> 生成字节码
而对应的步骤事实上会很多:
- 词法分析: 把源代码中的字符(各个关键字、变量等)转为标记(Token)集合,单个字符的程序编写的最小单元,而token是编译过程的最小单元。
- 语法分析: 将标记(Token)集合构造为抽象语法树。语法树的每一个节点都代表代码中的一个语法结构(如包、类型、接口、修饰符等等)。
- 填充符号表:符号表是有一组符号地址和符号信息构成的表格。填充符号表的过程的出口是一个待处理列表,包含了每一个抽象语法树(和package-info.java)的顶级节点。
- 插入式注解处理器处理注解: 注解处理器可以增删改抽象语法树的任意元素。因此每当注解处理器对语法树进行修改时,都将重新执行1,2,3步,直到注解处理器不再对语法树进行修改为止。每一次的循环过程都称为一次Round。
- 语义分析:对语法树结构上正确的源程序进行上下文有关的审查。
- 标注检查:包括是否变量声明、变量和赋值类型是否匹配等、常量折叠。
- 数据和控制流分析:对程序上下文逻辑更进一步验证。包括变量使用前是否赋值、方法是否有返回值、异常是否被正确处理等。
- 解语法糖: 把高级语法(如:泛型、可变参数、拆箱装箱等)转为基础语法结构,虚拟机运行时不支持这些高级语法。
- 生成字节码:把语法树、符号表里的信息转为字节码写到磁盘,同时进行少量的代码添加和转换工作。
- 编译与反编译
- 什么是编译
- 前端编译
- 后端编译
- 什么是反编译
- jit优化
- 逃逸分析
- 栈上分配
- 标量替换
- 锁优化
- 编译工具
- 反编译工具
- 类加载机制
- classloader
- 类加载过程
- 双亲委派 如何破坏双亲委派
- 模块化 jboosmodule,osgi,jigsaw
编译与反编译相关指令
这部分我们要熟悉javac与javap指令
字节码的编译指令
回忆一波最开始学Java的时候,肯定书里会教你怎么在黑框框里执行你的"hello word",在此我们再详细地回顾一下HelloWorld是怎么玩的
- 我们写好的代码为xx.java格式
- 使用javac xx.java会在同级目录生成 xx.class文件
- 继续使用
java xx
指令,就可以执行xx下的main函数。
以下是 javac 的编译指令参数
Usage: javac <options> <source files>
where possible options include:
-g Generate all debugging info
-g:none Generate no debugging info
-g:{lines,vars,source} Generate only some debugging info
-nowarn Generate no warnings
-verbose Output messages about what the compiler is doing
-deprecation Output source locations where deprecated APIs are used
-classpath <path> Specify where to find user class files and annotation processors
-cp <path> Specify where to find user class files and annotation processors
-sourcepath <path> Specify where to find input source files
-bootclasspath <path> Override location of bootstrap class files
-extdirs <dirs> Override location of installed extensions
-endorseddirs <dirs> Override location of endorsed standards path
-proc:{none,only} Control whether annotation processing and/or compilation is done.
-processor <class1>[,<class2>,<class3>...] Names of the annotation processors to run; bypasses default discovery process
-processorpath <path> Specify where to find annotation processors
-parameters Generate metadata for reflection on method parameters
-d <directory> Specify where to place generated class files
-s <directory> Specify where to place generated source files
-h <directory> Specify where to place generated native header files
-implicit:{none,class} Specify whether or not to generate class files for implicitly referenced files
-encoding <encoding> Specify character encoding used by source files
-source <release> Provide source compatibility with specified release
-target <release> Generate class files for specific VM version
-profile <profile> Check that API used is available in the specified profile
-version Version information
-help Print a synopsis of standard options
-Akey[=value] Options to pass to annotation processors
-X Print a synopsis of nonstandard options
-J<flag> Pass <flag> directly to the runtime system
-Werror Terminate compilation if warnings occur
@<filename> Read options and filenames from file
翻译过来就是:
-g 生成所有调试信息
-g:none 不生成任何调试信息
-g:{lines,vars,source} 只生成某些调试信息
-nowarn 不生成任何警告
-verbose 输出有关编译器正在执行的操作的消息
-deprecation 输出使用已过时的 API 的源位置
-classpath <路径> 指定查找用户类文件和注释处理程序的位置
-cp <路径> 指定查找用户类文件和注释处理程序的位置
-sourcepath <路径> 指定查找输入源文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
-extdirs <目录> 覆盖所安装扩展的位置
-endorseddirs <目录> 覆盖签名的标准路径的位置
-proc:{none,only} 控制是否执行注释处理和/或编译。
-processor[,,...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
-processorpath <路径> 指定查找注释处理程序的位置
-d <目录> 指定放置生成的类文件的位置
-s <目录> 指定放置生成的源文件的位置
-implicit:{none,class} 指定是否为隐式引用文件生成类文件
-encoding <编码> 指定源文件使用的字符编码
-source <发行版> 提供与指定发行版的源兼容性
-target <发行版> 生成特定 VM 版本的类文件
-version 版本信息
-help 输出标准选项的提要
-A关键字[=值] 传递给注释处理程序的选项
-X 输出非标准选项的提要
-J<标记> 直接将 <标记> 传递给运行时系统
-Werror 出现警告时终止编译
@<文件名> 从文件读取选项和文件名
-g 需要注意,当在后面使用javap反编译的时候,使用javap -l 可以输出方法的变量表table,如果不使用-g指令编译,将得不到方法的变量列表
接下来仔细描述一下javac的一些重要参数的使用方法
-implicit选项
-implicit选项用来指定是否为隐式引用的文件生成字节码文件,默认生成;选项支持:
1、none:不为隐式引用的文件生成字节码文件;
2、class:为隐式引用的文件生成字节码文件,默认选项;
public class Main {
public static void main(String[] args) {
A a = new A();
a.func();
}
}
public class A {
public void func() {
System.out.println("Test Implicit.");
}
}
执行情况如下:
> javac Main.java # 默认生成A的字节码文件
> javac -implicit:none Main.java # 不会生成A的字节码文件
-g 选项
用于生成调试信息,调试信息有-bootclasspathlines、vars和source;
lines:字节码文件中对应源码的行号;字节码调试打断点时,无行号信息,无法打断点。
vars:字节码文件中对应源码的变量信息;字节码调试时,无该信息,无法查看变量信息。
source:字节码文件对应的源文件名,针对类似非public修饰类场景,举例如下:Main.java编译后生成两个字节码文件Main.class && Test.class,Test.class隶属于Main.java,而不是Test.java
不指定-g选项 | 生成lines和source调试信息 |
---|---|
-g | 生成lines、vars、source调试信息 |
-g:none | 不生成任何调试信息 |
-g:{lines,vars,source} | 指定生成哪些调试信息,可以指定多个用逗号隔开; |
-source/-target选项
-source:
用于指定编译源码时使用的JDK版本,例如:javac -source 1.7 TestSource.java 指定使用JDK1.4编译TestSource.java,
但是TestSource.java中使用了lamba表达式,因此编译报错,需要指定JDK版本为1.8;
-target:
用于指定生成的字节码文件要运行在哪个JDK版本,如指定target版本为1.8,则运行字节码文件的JDK版本必须大于等于1.8
编译时同时使用:
运行使用的JDK版本必须大于等于编译使用的JDK版本,即-target指定的版本必须大于等于-source,否则编译会有如下错误:
javac: 源发行版 1.8 需要目标发行版 1.8
public class TestSource {
public static void main(String[] args) {
List<String> stringList = Arrays.stream(new String[]{"hello", "hi", "how are you", "what?", "hi"})
.distinct()
.filter(word -> word.startsWith("h"))
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println(stringList);
}
}
-d/-sourcepath/-classpath选项
-d选项: 指定编译生成字节码文件路径
-classpath选项: 指定依赖类路径,这里的依赖类为字节码文件;用于指导编译器在编译时按照指定路径查找依赖类;可以指定多个classpath,linux用:分隔,windows用;分隔
-sourcepath选项: 指定依赖类源文件路径,并且要求源文件除CLASSPATH依赖外,无其他依赖;如果同时找到了源文件和字节码文件,两者一致,以字节码文件为准;不一致,以源文件为准,并编译源文件
-bootclasspath/-extdirs选项
这两选项是几乎不需要使用的选项,两个选项的作用如下:
-bootclasspath: 用来覆盖引导类文件路径,引导类文件路径为: jdk1.8.0_212\jre\lib\rt.jar
-extdirs: 用来覆盖扩展类文件路径,扩展文件路径为: jdk1.8.0_212\jre\lib\ext\
-Xlint选项
Java编译选项有标准选项和非标准选项之分,标准选项指的是当前版本支持的选项,后续版本也一定支持;非标准选项指的是当前版本支持的选项,后续版本不一定支持。
-Xlint选项用来启用建议的告警,有如下选项:
Java编译选项有标准选项和非标准选项之分,标准选项指的是当前版本支持的选项,后续版本也一定支持;非标准选项指的是当前版本支持的选项,后续版本不一定支持。
非标准选项是以-X开头的选项,但是-X选项则是一个标准选项,用来显示-X选项的帮助信息;有特例:-J选项
1、-Xlint
启用所有编译建议的警告;该选项等同于-Xlint:all,相反禁用所有警告的选项为:-Xlint:none;-Xlint:none并非不显示任何警告,而是会给出存在哪些类型的警告并建议使用-Xlint对应的选项
2、-Xlint:unchecked
启用未经检查的转换警告,JDK1.5泛型引入的,源码中的编译警告即属于该种类型
3、-Xlint:finally
finally语句无法正常结束的警告
4、-Xlint:serial
需要序列化的类,未指定序列化ID的警告
5、-Xlint:fallthrouth
switch case语句中,第一个case语句无break
-encoding指令
指定编译时编码格式,中文windows默认GBK编码,java文件一般使用UTF-8格式,因此常用命令为javac -encoding UTF-8 XXX.java
-verbose
输出编译时的详细信息,包括:classpath、加载的类文件信息。
@<文件名>
用来通经文件指定编译多个java源文件;比如,有如下的几个java源文件,我们希望编译这些源文件,一个一个编译?当然不,
我们可以通过将编译选项和源文件名以行为单位写入文件,然后通过-@标签编译。
javac @compile.cfg
-j <标记>
传递一些信息给 Java Launcher,例如
javac -J -Xms48m Xxx.java
字节码分析工具
javap
JDK 提供了专门用来分析类文件的工具:javap
其中-c -v -l -p -s是最常用的
Usage: javap <options> <classes>
where possible options include:
-help --help -? Print this usage message
-version Version information
-v -verbose Print additional information
-l Print line number and local variable tables
-public Show only public classes and members
-protected Show protected/public classes and members
-package Show package/protected/public classes
and members (default)
-p -private Show all classes and members
-c Disassemble the code
-s Print internal type signatures
-sysinfo Show system info (path, size, date, MD5 hash)
of class being processed
-constants Show final constants
-classpath <path> Specify where to find user class files
-cp <path> Specify where to find user class files
-bootclasspath <path> Override location of bootstrap class files
一般常用的是-v -l -c三个选项。
javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。
javap -l 会输出行号和本地变量表信息。
javap -c 会对当前class字节码进行反编译生成汇编代码。
javap -p 默认情况下,javap 会显示访问权限为 public、protected 和默认(包级 protected)级别的方法,加上 -p 选项以后可以显示 private 方法和字段
javap -v 参数的输出更多详细的信息,比如栈大小、方法参数的个数。
javap -s 可以输出签名的类型描述符。我们可以看下 xx.java 所有的方法签名
AsmTools
我们知道直接修改.class文件是很麻烦的,虽然有一些图形界面的工具,但还是很麻烦。在OpenJDK里有一个AsmTools项目,用来生成正确的或者不正确的java .class文件,主要用来测试和验证。AsmTools引入了两种表示.class文件的语法:
- JASM
用类似java本身的语法来定义类和函数,字节码指令则很像传统的汇编。 - JCOD
整个.class用容器的方式来表示,可以很清楚表示类文件的结构。
查看JASM语法结果
java -jar asmtools.jar jdis Test.class
查看JCOD语法结果
java -jar asmtools.jar jdec Test.class
从JASM/JCOD语法文件生成类文件
因为是等价表达,可以从JASM生成.class文件:
java -jar asmtools.jar jasm Test.jasm
同样可以从JCOD生成.class文件:
java -jar asmtools.jar jcoder Test.jasm
更多使用方法参考:
- https://wiki.openjdk.java.net/display/CodeTools/Chapter+2#Chapter2-Jasm.1
- https://wiki.openjdk.java.net/display/CodeTools/asmtools
idea插件
在插件商场里面找到jclassLib并安装show Bytecode With JclassLib
,找到任意一个.class,然后点开show 的菜单。
你就可以看到这个class文件的数据结构了,基本上你能想到的,这个插件都能帮你捞出来
jar指令
打包.class文件
jar -cvf Test.jar -C classes/ .
这个命令将会把classes下的所有文件(包括.class和文件夹等)打包为Test.jar文件。
上篇博客中,介绍了参数-C的意义:-C 更改为指定的目录并包含其中的文件,如果有任何目录文件, 则对其进行递归处理。它相当于使用 cd 命令转指定目录下。
注意后面的".“的用法,jar可以递归的打包文件夹,”."表示是当前文件夹。如果执行命令“jar -cvf Test.jar .”,表示将当前目录下的所有文件及文件夹打包。所以上面的命令的意思就是“将classes下的所有文件打包为Test.jar”。
生成可以运行的jar包
java -cp Test.jar Main
通过上面的命令就可以执行Test.jar中的Main.class。其中,cp指定了jar文件的位置。
需要指定jar包的应用程序入口点,用-e选项
Class 文件结构
先整体看下class的文件结构
ClassFile {
u4 magic; // 魔数(Magic Number)
u2 minor_version; // 版本号(Minor&Major Version)
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1]; // 常量池(Constant Pool)
u2 access_flags; // 类访问标记(Access Flags)
u2 this_class; // 类索引(This Class)
u2 super_class; // 超类索引(Super Class)
u2 interfaces_count;
u2 interfaces[interfaces_count]; // 接口表索引(Interfaces)
u2 fields_count;
field_info fields[fields_count]; // 字段表(Fields)
u2 methods_count;
method_info methods[methods_count]; // 方法表(Methods)
u2 attributes_count;
attribute_info attributes[attributes_count]; // 属性表(Attributes)
}
Java虚拟机规定用u1、u2、u4三种数据结构来表示1、2、4字节无符号整数。上面的结构是采用类似c结构体的形式表达。
魔数
先造一个最简单的代码:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World");
}
}
生成的class文件用工具打开是这样的(本身是一个二进制,里面都是01,用工具打开才会变成这个样子)
00000000: cafe babe 0000 0034 001d 0a00 0600 0f09 .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507 ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169 umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67 n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75 /String;)V...Sou
00000070: 7263 6546 696c 6501 000a 4865 6c6c 6f2e rceFile...Hello.
00000080: 6a61 7661 0c00 0700 0807 0017 0c00 1800 java............
00000090: 1901 000c 4865 6c6c 6f2c 2057 6f72 6c64 ....Hello, World
000000a0: 0700 1a0c 001b 001c 0100 0548 656c 6c6f ...........Hello
000000b0: 0100 106a 6176 612f 6c61 6e67 2f4f 626a ...java/lang/Obj
000000c0: 6563 7401 0010 6a61 7661 2f6c 616e 672f ect...java/lang/
000000d0: 5379 7374 656d 0100 036f 7574 0100 154c System...out...L
000000e0: 6a61 7661 2f69 6f2f 5072 696e 7453 7472 java/io/PrintStr
000000f0: 6561 6d3b 0100 136a 6176 612f 696f 2f50 eam;...java/io/P
00000100: 7269 6e74 5374 7265 616d 0100 0770 7269 rintStream...pri
00000110: 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61 ntln...(Ljava/la
00000120: 6e67 2f53 7472 696e 673b 2956 0021 0005 ng/String;)V.!..
00000130: 0006 0000 0000 0002 0001 0007 0008 0001 ................
00000140: 0009 0000 001d 0001 0001 0000 0005 2ab7 ..............*.
00000150: 0001 b100 0000 0100 0a00 0000 0600 0100 ................
00000160: 0000 0200 0900 0b00 0c00 0100 0900 0000 ................
00000170: 2500 0200 0100 0000 09b2 0002 1203 b600 %...............
00000180: 04b1 0000 0001 000a 0000 000a 0002 0000 ................
00000190: 0005 0008 0006 0001 000d 0000 0002 000e ................
可以看到,二进制的最开始就是 ca fe ba be
,这四个字节相当于被认为是java代码编译之后的标记。不同种的文件都会有不同的标记,例如png文件,JPEG文件等等。
意味着文件如果不是以ca fe ba be
开头的,将不被jvm承认。
主版本号与副版本号
依旧参考Hello.java反编译出来的二进制:
00000000: cafe babe 0000 0034 001d 0a00 0600 0f09 .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507 ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169 umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67 n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75 /String;)V...Sou
00000070: 7263 6546 696c 6501 000a 4865 6c6c 6f2e rceFile...Hello.
00000080: 6a61 7661 0c00 0700 0807 0017 0c00 1800 java............
00000090: 1901 000c 4865 6c6c 6f2c 2057 6f72 6c64 ....Hello, World
000000a0: 0700 1a0c 001b 001c 0100 0548 656c 6c6f ...........Hello
000000b0: 0100 106a 6176 612f 6c61 6e67 2f4f 626a ...java/lang/Obj
000000c0: 6563 7401 0010 6a61 7661 2f6c 616e 672f ect...java/lang/
000000d0: 5379 7374 656d 0100 036f 7574 0100 154c System...out...L
000000e0: 6a61 7661 2f69 6f2f 5072 696e 7453 7472 java/io/PrintStr
000000f0: 6561 6d3b 0100 136a 6176 612f 696f 2f50 eam;...java/io/P
00000100: 7269 6e74 5374 7265 616d 0100 0770 7269 rintStream...pri
00000110: 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61 ntln...(Ljava/la
00000120: 6e67 2f53 7472 696e 673b 2956 0021 0005 ng/String;)V.!..
00000130: 0006 0000 0000 0002 0001 0007 0008 0001 ................
00000140: 0009 0000 001d 0001 0001 0000 0005 2ab7 ..............*.
00000150: 0001 b100 0000 0100 0a00 0000 0600 0100 ................
00000160: 0000 0200 0900 0b00 0c00 0100 0900 0000 ................
00000170: 2500 0200 0100 0000 09b2 0002 1203 b600 %...............
00000180: 04b1 0000 0001 000a 0000 000a 0002 0000 ................
00000190: 0005 0008 0006 0001 000d 0000 0002 000e ................
紧跟着ca fe ba be的四个字节是,00 00 00 34
00 00
表示副版本号(Minor Version),00 34
(值是52)表示主版本号(Major Version)
Java版本 | Major version |
---|---|
Java1.4 | 48 |
Java5 | 59 |
Java6 | 50 |
Java7 | 51 |
Java8 | 52 |
Java9 | 53 |
JVM运行时向下兼容的,意味着比方1.8的jdk,一看到主版本是高于52的,就直接报出异常来
常量池
常量池是一个最复杂最复杂的数据结构了,还是一样的套路,我们制造一个代码出来
public class Hello1 {
public final boolean bool = true; // 1(0x01)
public final char c = 'A'; // 65(0x41)
public final byte b = 66; // 66(0x42)
public final short s = 67; // 67(0x43)
public final int i = 68; // 68(0x44)
public final long l = Long.MAX_VALUE;
public final double d = Double.MAX_VALUE;
public static void main(String[] args) {
System.out.println("Hello, World");
}
}
编译出来之后的二进制文件是
00000000: cafe babe 0000 0034 004e 0a00 1600 3509 .......4.N....5.
00000010: 0015 0036 0900 1500 3709 0015 0038 0900 ...6....7....8..
00000020: 1500 3909 0015 003a 0700 3b04 7f7f ffff ..9....:..;.....
00000030: 0900 1500 3c07 003d 057f ffff ffff ffff ....<..=........
00000040: ff09 0015 003e 0700 3f06 7fef ffff ffff .....>..?.......
00000050: ffff 0900 1500 4009 0041 0042 0800 430a [email protected].
00000060: 0044 0045 0700 4607 0047 0100 0462 6f6f .D.E..F..G...boo
00000070: 6c01 0001 5a01 000d 436f 6e73 7461 6e74 l...Z...Constant
00000080: 5661 6c75 6503 0000 0001 0100 0163 0100 Value........c..
00000090: 0143 0300 0000 4101 0001 6201 0001 4203 .C....A...b...B.
000000a0: 0000 0042 0100 0173 0100 0153 0300 0000 ...B...s...S....
000000b0: 4301 0001 6901 0001 4903 0000 0044 0100 C...i...I....D..
000000c0: 0166 0100 0146 0100 016c 0100 014a 0100 .f...F...l...J..
000000d0: 0164 0100 0144 0100 063c 696e 6974 3e01 .d...D...<init>.
000000e0: 0003 2829 5601 0004 436f 6465 0100 0f4c ..()V...Code...L
000000f0: 696e 654e 756d 6265 7254 6162 6c65 0100 ineNumberTable..
00000100: 046d 6169 6e01 0016 285b 4c6a 6176 612f .main...([Ljava/
00000110: 6c61 6e67 2f53 7472 696e 673b 2956 0100 lang/String;)V..
00000120: 0a53 6f75 7263 6546 696c 6501 000b 4865 .SourceFile...He
00000130: 6c6c 6f31 2e6a 6176 610c 002d 002e 0c00 llo1.java..-....
00000140: 1700 180c 001b 001c 0c00 1e00 1f0c 0021 ...............!
00000150: 0022 0c00 2400 2501 000f 6a61 7661 2f6c ."..$.%...java/l
00000160: 616e 672f 466c 6f61 740c 0027 0028 0100 ang/Float..'.(..
00000170: 0e6a 6176 612f 6c61 6e67 2f4c 6f6e 670c .java/lang/Long.
00000180: 0029 002a 0100 106a 6176 612f 6c61 6e67 .).*...java/lang
00000190: 2f44 6f75 626c 650c 002b 002c 0700 480c /Double..+.,..H.
000001a0: 0049 004a 0100 0c48 656c 6c6f 2c20 576f .I.J...Hello, Wo
000001b0: 726c 6407 004b 0c00 4c00 4d01 0006 4865 rld..K..L.M...He
000001c0: 6c6c 6f31 0100 106a 6176 612f 6c61 6e67 llo1...java/lang
000001d0: 2f4f 626a 6563 7401 0010 6a61 7661 2f6c /Object...java/l
000001e0: 616e 672f 5379 7374 656d 0100 036f 7574 ang/System...out
000001f0: 0100 154c 6a61 7661 2f69 6f2f 5072 696e ...Ljava/io/Prin
00000200: 7453 7472 6561 6d3b 0100 136a 6176 612f tStream;...java/
00000210: 696f 2f50 7269 6e74 5374 7265 616d 0100 io/PrintStream..
00000220: 0770 7269 6e74 6c6e 0100 1528 4c6a 6176 .println...(Ljav
00000230: 612f 6c61 6e67 2f53 7472 696e 673b 2956 a/lang/String;)V
00000240: 0021 0015 0016 0000 0008 0011 0017 0018 .!..............
00000250: 0001 0019 0000 0002 001a 0011 001b 001c ................
00000260: 0001 0019 0000 0002 001d 0011 001e 001f ................
00000270: 0001 0019 0000 0002 0020 0011 0021 0022 ......... ...!."
00000280: 0001 0019 0000 0002 0023 0011 0024 0025 .........#...$.%
00000290: 0001 0019 0000 0002 0026 0011 0027 0028 .........&...'.(
000002a0: 0001 0019 0000 0002 0008 0011 0029 002a .............).*
000002b0: 0001 0019 0000 0002 000b 0011 002b 002c .............+.,
000002c0: 0001 0019 0000 0002 000f 0002 0001 002d ...............-
000002d0: 002e 0001 002f 0000 006e 0003 0001 0000 ...../...n......
000002e0: 0036 2ab7 0001 2a04 b500 022a 1041 b500 .6*...*....*.A..
000002f0: 032a 1042 b500 042a 1043 b500 052a 1044 .*.B...*.C...*.D
00000300: b500 062a 1208 b500 092a 1400 0bb5 000d ...*.....*......
00000310: 2a14 000f b500 11b1 0000 0001 0030 0000 *............0..
00000320: 0026 0009 0000 0001 0004 0003 0009 0004 .&..............
00000330: 000f 0005 0015 0006 001b 0007 0021 0008 .............!..
00000340: 0027 0009 002e 000a 0009 0031 0032 0001 .'.........1.2..
00000350: 002f 0000 0025 0002 0001 0000 0009 b200 ./...%..........
00000360: 1212 13b6 0014 b100 0000 0100 3000 0000 ............0...
00000370: 0a00 0200 0000 0d00 0800 0e00 0100 3300 ..............3.
00000380: 0000 0200 34 ....4
二进制看上去就复杂很多。
常量池的基本结构是这个样子的
struct {
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
}
意味着紧跟着主版本号的两个字节表达了常量池长度是多少,查看二进制,可以看到,两个字节是00 4e
,意味着我们当前的class文件的constant_pool_count=78,但是真正的常量池条目录数最大是77。
这里有两个含义:
- 为什么是77
- 为什么最大是77
因为常量池是1开始的,0位有特殊含义,表达什么都没有的状态。因此总数是77,那为什么最大是77呢?因为类似long,double事实上是占用两个常量池位置的,因此真正的常量池条目数是<= 77的
而cp_info的结构可以用下面的伪代码表达
cp_info {
u1 tag;
u1 info[];
}
tag指的是常量条目的类型,我在这里列一下:
不同的类型会有不同的tag值,u1 info[] 相当于和这个tag值绑定,有些tag值固定是2个字节,那么在解析class文件的时候就自动往后扣两个字节。接下来,我们分析一下所有的数据类型tag。
数值类型的常量
我们java领域内,所拥有的数值类型有 bool,byte, short, char, int, long, float, long
而在上图中可以看到,bool,byte, short, char类型是没有对应的tag的,那是因为在常量池里面统一使用int操作了。
2.3.1.1 CONSTANT_Integer_info类型
CONSTANT_Integer_info 结构可以使用下面的结构表示
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
因为这里已经确定了是CONSTANT_Integer_info,所以这里的tag是03,后面四个字节表达数值本身。
那么人肉翻译一下代码里面定义的常量:
public final boolean bool = true; // 1(0x01)
public final char c = 'A'; // 65(0x41)
public final byte b = 66; // 66(0x42)
public final short s = 67; // 67(0x43)
public final int i = 68; // 68(0x44)
bool = 03 00 00 00 01
c = 03 00 00 00 41
b = 03 00 00 00 42
s = 03 00 00 00 43
i = 03 00 00 00 44
使用hex Fiend软件圈选出来
2.3.1.2 CONSTANT_Float_info类型
CONSTANT_Float_info类型的数据结构和CONSTANT_Integer_info数据是差不多的
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
意味着,tag值是4,然后后面跟着4个字节表达数据,即 04 7F 7F FF FF
2.3.1.3 CONSTANT_Long_info类型
以下是 CONSTANT_Long_info数据类型
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
可以看到数据本身会被切分为两个四字节存储,分别叫高位值与低位值
2.3.1.4 CONSTANT_Double_info类型
以下是 CONSTANT_Double_info 类型
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
可以看到和CONSTANT_Long_info的存储结构一毛一样,只不过存储的数据本身格式会有很大区别。
字符串类型常量
这里将会涉及到两种类型的数据CONSTANT_String_info与个CONSTANT_Utf8_info
CONSTANT_Utf8_info存储了字符串真正的内容,而CONSTANT_String_info仅仅包含一个指向常量池中CONSTANT_Utf8_info常量类型的索引。
还是一样的套路,我们制造出用于说明的java代码
public class Hello2{
public final String hello = "hello";
public final String x = "\0";
public final String y = "\uD83D\uDE02";// emoji
标签:info,文件,java,字节,...,001,0000,class
From: https://blog.csdn.net/sinat_35426920/article/details/140491785