Java学习之编译、反编译以及字节码入门

本文主要围绕Java语言的编译、反编译及字节码展开。介绍了编译的概念、方式、层次,Java的前端编译、JIT编译、AOT编译等方式及优缺点,还提及编译工具。阐述了反编译的场景和工具,以及Java字节码的指令集等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注:本文所述的编译与反编译知识点绝大部分基于Java语言。

编译

什么是编译

利用编译程序从源语言编写的源程序产生目标程序的过程。编译就是把高级语言变成计算机可识别的2进制语言。

编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。

编译方式

编译方式,即编译分类,一种分类是静态编译和动态编译。

静态编译:Static Compilation,事前编译(ahead-of-time compilation,AOT)。在编译时把所有的模块都编译进目标程序(如exe)里去,当启动这个目标程序时所有模块都加载进来。

静态编译就是编译器在编译可执行文件时,将可执行文件需要调用的对应动态链接库(.so或.lib)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行时不依赖于动态链接库。其优缺点与动态编译的可执行文件正好互补。

动态编译:Dynamic Compilation,在运行时进行编译。在编译时,所需的模块没有都编译进去,一般情况下可把那些模块都编译成dll,这样启动程序(初始化)时这些模块不会被加载,而是在运行时,用到哪个模块就调用哪个模块。动态编译的可执行文件需要附带一个动态链接库。在执行时,需要调用其对应动态链接库中的命令。

优点:

  • 缩小执行文件本身体积;
  • 加快编译速度,节省系统资源。

缺点:

  • 是哪怕是很简单的程序,只用到链接库中的一两条命令,也需要附带一个相对庞大的链接库;
  • 如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

编译层次

在JVM中,字节码(Bytecode)的编译层次是指JVM如何将字节码优化为机器代码的不同阶段。这一过程由JIT(Just-In-Time)编译器负责,主要分为两种编译器:C1(Client Compiler)和C2(Server Compiler)。此外,Profiling(性能监控)是整个编译优化流程的重要组成部分。

Java的编译层次是分阶段优化字节码的一种策略。为了在性能和启动时间之间取得平衡,HotSpot JVM定义以下编译层次:

  • 字节码解释执行:初始阶段,字节码由解释器逐条解释执行,适合短生命周期代码。性能较低,但启动速度快;
  • C1编译:主要用于客户端应用程序(如GUI),关注启动时间和占用内存。编译过程中,C1会对代码进行轻量级优化。可选择是否进行Profiling,帮助JVM收集运行时性能数据;
  • C2编译:主要用于服务器端应用程序(如Web服务),关注吞吐量和长期性能。执行更复杂的优化,包括循环展开、方法内联等。需要依赖Profiling数据,以便在热点代码上进行深度优化;
  • 分层编译:Tiered Compilation,HotSpot JVM从JDK7开始的默认模式,结合C1和C2的优势,初始阶段由解释器和C1提供快速启动,后续阶段由C1收集Profiling数据,提供给C2用于深度优化。
层次执行方式特点
解释执行逐条解释字节码启动快,性能低
C1编译轻量编译,适合短生命周期代码可选择带Profiling,优化简单,提升启动性能
C2编译高级优化,适合长期运行代码依赖Profiling数据,优化复杂,提升长期性能
Tiered CompilationC1和C2协同工作启动快,长期性能高

Profiling是指JVM在代码运行时,收集有关代码执行行为的数据,以指导后续优化。C1和C2编译器可以带或不带Profiling数据工作:

C1编译

  • 带Profiling:JVM启用-XX:+TieredCompilation时,C1会同时进行编译和Profiling。收集方法调用频率、分支跳转路径、循环执行次数等数据,供C2编译优化使用。
  • 不带Profiling:C1直接生成简单的本地代码,不做复杂优化,适合对启动时间敏感的场景。

C2编译

  • 带Profiling:基于C1收集的Profiling数据,C2进行高层次优化,生成性能极高的机器代码。Profiling数据指导C2如何优化,例如方法内联、循环展开。
  • 不带Profiling:C2编译器也可以单独工作,但由于缺乏运行时数据,优化效果不如带Profiling的方式。

参数设置:

  • -XX:+TieredCompilation:默认,启用分层编译;
  • -XX:-TieredCompilation:禁用分层编译,仅使用C2;
  • -Xint:完全禁用JIT,仅解释执行

Java编译

Java程序代码需要编译后才能在虚拟机中运行,编译涉及到:编译原理、语言规范、虚拟机规范、本地机器码优化等。

Java编译时间
指虚拟机的JIT编译器编译热点代码的时间。Java源代码编译出的class文件中存储的是字节码,虚拟机通过解释方式执行字节码文件,比起C、C++的本地二进制代码速度慢很多,JVM内置两个运行时编译器,如果一个Java方法,被调用次数达到一定程度,就会被判定为热代码,交给JIT编译器编译为本地代码,提高运行速度。C、C++是静态编译,而Java大多是动态编译。

Java有三种编译方式:前端编译、JIT编译(即时编译,just-in-time compile)、AOT编译(静态提前编译)

JIT编译狭义来说是当某段代码即将第一次被执行时进行编译,因而叫即时编译。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的JIT编译所指的区别。

自适应动态编译(adaptive dynamic compilation)也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序以某种形式先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。

前端编译

把Java源码文件编译成Class文件的过程;也即把满足Java语言规范的程序转化为满足JVM规范所要求格式的文件。

优点:

  1. 这阶段的优化是指程序编码方面的;
  2. 许多Java语法新特性(泛型、内部类等语法糖),是靠前端编译器实现的,而不是依赖虚拟机;
  3. 编译后的Class文件可以直接给JVM解释器解释执行,省去编译时间,加快启动速度;

缺点:

  1. 对代码运行效率几乎没有任何优化措施;
  2. 解释执行效率较低;
  3. 前端编译器:Oracle javac、Eclipse JDT中的增量式编译器(ECJ)等;

JIT编译

后端编译,JVM内置,在运行时把Class字节码文件编译成本地机器码的过程。

JIT优化技术:

  1. 内联:可避免方法跳跃;
  2. 垃圾代码:死代码,当某些对象存在于字节码中且不被使用时,编译器可以决定从机器代码中删除它们;
  3. 循环优化:编译器可以组织并优化循环执行顺序或对尾递归优化成for循环等,以此来优化CPU所执行的代码;
  4. 用实现方法替换接口方法:当给定接口的一个方法有且仅由一个对象实现时,编译器可以决定直接使用实现的方法,以避免在运行时绑定真正实现的方法所引起的开销。

优点:

  1. 通过在运行时收集监控信息,把热点代码(Hot Spot Code)编译成与本地平台相关的机器码,并进行各种层次的优化;
  2. 可以大大提高执行效率;

缺点:

  1. 收集监控信息影响程序运行;
  2. 编译过程占用程序运行时间(如使得启动速度变慢);
  3. 编译机器码占用内存;
  4. JIT编译器:HotSpot虚拟机的C1、C2编译器等;

JIT编译速度及编译结果的优劣,衡量JVM性能的重要指标;所以对程序运行性能优化集中到这个阶段;可在这个阶段进行JVM调优。

AOT编译

程序运行前,直接把Java源码文件编译成本地机器码的过程;
优点:

  1. 编译不占用运行时间,可以做一些较耗时的优化,并可加快程序启动;
  2. 把编译的本地机器码保存磁盘,不占用内存,并可多次使用;

缺点:

  1. 因为Java语言的动态性(如反射)带来额外的复杂性,影响静态编译代码的质量;
  2. 一般静态编译不如JIT编译的质量,这种方式用得比较少;
  3. AOT编译器:JAOTC、GCJ、Excelsior JET、ART(Android Runtime)等;

组合

目前Java体系中主要还是采用前端编译+JIT编译的方式,如HotSpot虚拟机,其运作过程大体如下:

  1. 首先通过前端编译把符合Java语言规范的程序代码转化为满足JVM规范所要求Class格式;
  2. 然后程序启动时Class格式文件发挥作用,解释执行,省去编译时间,加快启动速度;
  3. 针对Class解释执行效率低的问题,在运行中收集性能监控信息,得知热点代码;
  4. JIT逐渐发挥作用,把越来越多的热点代码编译优化成本地代码,提高执行效率;

编译工具

  1. javac
  2. JitWatch

查看class文件

javac编译.java文件到.class文件:javac Hello.java

javap查看class文件:javap -c Hello.class

反编译

反编译,即计算机软件反向工程(Reverse Engineering),计算机软件还原工程,是指通过对他人软件的目标程序(可执行程序)进行逆向分析、研究工作,以推导出他人的软件产品所使用的思路、原理、结构、算法、处理过程、运行方法等设计要素,某些特定情况下可能推导出源代码。反编译作为自己开发软件时的参考,或者直接用于自己的软件产品中。

class文件打破C或者C++等语言所遵循的传统,使用这些传统语言写的程序通常首先被编译,然后被连接成单独的、专门支持特定硬件平台和操作系统的二进制文件。通常情况下,一个平台上的二进制可执行文件不能在其他平台上工作。

Java class文件是可运行在任何支持JVM的硬件平台和操作系统上的二进制文件。

Java最突出的跨平台优势使得它不能被编译成本地代码,而要以中间代码的形式运行在VM环境中,这使得Java的反编译要比别的高级语言容易实现,且反编译的代码经过优化后几乎可与源代码相媲美。

Java反编译,就是通过*.class文件得到其.java文件。

什么时候需要反编译

1、只有一个类的class文件(或者是jar、war包),但是看不懂class文件或者说看起来效率非常低下,此时可考虑反编译。
2、想知道Java一些语法糖实现细节,可借助反编译。

反编译工具

又名反编译器,解码器,将目标程序码反转成源代码。两者之间的转换不是一一对应的:两段完全不同的Java程序也可能生成完全相同的字节码,有时需要一些试探才能更加接近源码。

工具:

  1. javap:JDK自带。
  2. Jad
  3. JODE
  4. Java Decompiler
  5. Fernflower decompiler:IDEA使用的反编译器。

JAD官网,一个命令行工具,没有图形界面,上述的这些工具大多是在JAD内核的基础之上加一个图形界面(比如jd-GUI)。JAD是使用MS Visual C++开发的,运行速度非常快,可处理很复杂的Java编译文件。JAD提供很多参数用于灵活应付多种加密手段,使得反编译得到代码更加优化和易读:

  • -d:用于指定输出文件的目录;
  • -s:输出文件扩展名(默认为: .jad),通常都会把输出文件扩展名直接指定为.java,以方便修改的重新编译;
  • -8:将Unicode字符转换为ANSI字符串,如果输出字符串是中文的话一定要加上这个参数才能正确显示。

命令:Jad –d c:\\java –s .java -8 test.class
将当前目录下的test.class反编译为test.java并保存在c:\\java目录里,其中的提示输出为中文,而不是Unicode代码。

JODE,官网,即Java Optimize and Decompile Environment,纯Java开发,众多Java反编译软件的核心引擎,能应付一些常见的加密手段,如混淆技术等。很古老,很久没有更新,源码拖管于SourceForge。

字节码

字节码文件,即.class文件。Java一次编译到处运行,得益于JVM对字节码文件的解析。不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合JVM规范,JVM就能执行该文件。

public class Demo {
	private static final Integer NUM = 1;
	
	public static void main(String[] args) {
	}
}

习惯使用IDE,此时如果想要看字节码文件,不能使用IDE查看class文件,IDE会自动将class文件反编译成和源文件非常类似的文件:

public class Demo {
	private static final Integer NUM = 1;
	
	public Demo() {
	}
	
	public static void main(String[] args) {
	}
}

上面即为使用IDEA自带的Fernflower decompiler反编译之后得到的文件,增加一个默认的构造方法。
想要查看字节码文件需要使用文本编辑器,比如sublime text。
看到的是这样的效果:

cafe babe 0000 0034 001f 0a00 0500 160a
0017 0018 0900 0400 1907 001a 0700 1b01
0003 4e55 4d01 0013 4c6a 6176 612f 6c61

一堆16进制的字节。
在这里插入图片描述
Java字节码的总览图,一共含有10部分。

指令集

在JVM的字节码指令集中,invokevirtual、invokestatic、invokeinterface、invokespecial和invokedynamic是用于调用方法的不同指令。代表不同类型的方法调用方式,主要用于实现多态、静态方法调用、接口方法调用等特性:

  • invokevirtual:实例方法,依赖虚方法表(vtable)来实现方法的动态分派;
  • invokestatic:静态方法,不依赖于对象,静态绑定,即编译期确定;
  • invokeinterface:接口方法,在运行时会通过接口方法表(itable)查找具体方法,比invokevirtual更灵活,支持接口的多实现
  • invokespecial:调用一些需要特殊处理的实例方法,如构造方法<init>,私有方法,父类方法super.method()
  • invokedynamic:动态调用方法,在运行时由引导方法(Bootstrap Method)确定具体的调用逻辑。主要用于实现动态语言特性(如Lambda表达式和动态代理),在运行时动态解析并绑定目标方法。
指令用途绑定类型是否支持多态
invokevirtual调用实例方法动态绑定支持(运行时确定方法)
invokestatic调用静态方法静态绑定不支持
invokeinterface调用接口方法动态绑定支持(运行时查找itable)
invokespecial调用私有、父类方法或构造器静态绑定不支持
invokedynamic动态调用动态绑定支持(灵活性最高)

invokevirtual和invokeinterface

反汇编

比反编译更底层的过程。

推荐阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

johnny233

晚饭能不能加鸡腿就靠你了

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值