明白Java代码是如何编译成字节码并在JVM上运行的非常重要,这有助于理解程序运行的时候到底发生了些什么。明白这个不仅能搞清语言特性是如何实现的,并且在做方案讨论的时候能知道相应的副作用及权衡利弊。
本文介绍了Java代码是如何编译成字节码并在JVM上执行的。想了解JVM的内部结构以及字节码运行时用到的各个内存区域,可以看下我前面的
一篇关于JVM内部细节的文章。
本文分为三部分,每一部分都分成几个小节。每个小节都可以单独阅读,不过由于一些概念是逐步建立起来的,如果你依次阅读完所有章节会更简单一些。每一节都会覆盖到Java代码中的不同结构,并详细介绍了它们是如何编译成字节码并执行的。
1. 第一部分, 基础概念
变量
局部变量
JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。
局部变量可以是以下这些类型:
char
long
short
int
float
double
引用
返回地址
除了long和double类型外,每个变量都只占局部变量区中的一个变量槽(slot),而long及double会占用两个连续的变量槽,因为这些类型是64位的。
当一个新的变量创建的时候,操作数栈(operand stack)会用来存储这个新变量的值。然后这个变量会存储到局部变量区中对应的位置上。如果这个变量不是基础类型的话,本地变量槽上存的就只是一个引用。这个引用指向堆的里一个对象。
比如:
int i = 5;
编译后就成了
0: bipush 5
2: istore_0
bipush | 用来将一个字节作为整型数字压入操作数栈中,在这里5就会被压入操作数栈上。 |
istore_0 | 这是istore_<n>这组指令集(译注:严格来说,这个应该叫做操作码,opcode ,指令是指操作码加上对应的操作数,oprand。不过操作码一般作为指令的助记符,这里统称为指令)中的一条,这组指令是将一个整型存储到本地变量中。<n>代表的是局部变量区中的位置,并且只能是0,1,2,3。再高的话只能用另一条指令istore了,这条指令会接受一个操作数,对应的是局部变量区中的位置信息。 |
当这条指令执行的时候,内存布局是这样的:
class文件中的每一个方法都会包含一个局部变量表,如果这段代码在一个方法里面的话,你在类文件的局部变量表中会找到如下的一条记录。
LocalVariableTable:
Start Length Slot Name Signature
0 1 1 i I
字段
Java类里面的字段是作为类对象实例的一部分,存储在堆里面的(类变量对应存储在类对象里面)。关于字段的信息会添加到类文件里的field_info数组里,像下面这样:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_pool[constant_pool_count – 1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
另外,如果变量被初始化了,那么初始化的字节码会加到构造方法里。
下面这段代码编译了之后:
public class SimpleClass {
public int simpleField = 100;
}
如果你用javap进行反编译,这个被添加到了field_info数组里的字段会多出一段描述信息。
public int simpleField;
Signature: I
flags: ACC_PUBLIC
初始化变量的字节码会被加到构造方法里,像下面这样:
public SimpleClass();
Signature: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 100
7: putfield #2 // Field simpleField:I
10: return
<table>
<tr>
<td>aload_0 </td><td>从局部变量数组中加载一个对象引用到操作数栈的栈顶。尽管这段代码看起来没有构造方法,但是在编译器生成的默认的构造方法里,就会包含这段初始化的代码。第一个局部变量正好是this引用,于是aload_0把this引用压到操作数栈中。aload_0是aload_<n>指令集中的一条,这组指令会将引用加载到操作数栈中。n对应的是局部变量数组中的位置,并且也只能是0,1,2,3。还有类似的加载指令,它们加载的并不是对象引用,比如iload_<n>,lload_<n>,fload_<n>,和dload_<n>, 这里i代表int,l代表long,f代表float,d代表double。局部变量的在数组中的位置大于3的,得通过iload,lload,fload,dload,和aload进行加载,这些指令都接受一个操作数,它代表的是要加载的局部变量的在数组中的位置。
</td>
</tr>
<tr><td>invokespecial </td><td>这条指令可以用来调用对象实例的初始化方法,私有方法和父类中的方法。它是方法调用指令集中的一条,其它的还有invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual.这里的invokespecial 指令调用的是父类也就是java.lang.Objectr构造方法。</td>
</tr>
<tr><td>bipush </td><td> 它是用来把一个字节作为整型压到操作数栈中的,在这里100会被加到操作栈里面。</td>
</tr>
<tr><td>putfield </td><td> 它接受一个操作数,这个数引用的是运行时常量池里的一个字段,在这里这个字段是simpleField。赋给这个字段的值,以及包含这个字段的对象引用,在执行这条指令的时候,都 会从操作数栈顶上pop出来。前面的aload_0指令已经把包含这个字段的对象压到操作数栈上了,而后面的bipush又把100压到栈里。最后putfield指令会将这两个值从栈顶弹出。执行完的结果就是这个对象的simpleField这个字段的值更新成了100。</td></tr>
</table>
上述代码执行的时候内存里面是这样的:
这里的putfield指令的操作数引用的是常量池里的第二个位置。JVM会为每种类型维护一个常量池,运行时的数据结构有点类似一个符号表,尽管它包含的信息更多。Java中的字节码操作需要数据,但通常这些数据都太大了,存储在字节码里不适合,它们会被存储在常量池里面,而字节码包含一个常量池里的引用 。当类文件生成的时候,其中的一块就是常量池:
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#17 // SimpleClass.simpleField:I
#3 = Class #13 // SimpleClass
#4 = Class #19 // java/lang/Object
#5 = Utf8 simpleField
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 SimpleClass
#14 = Utf8 SourceFile
#15 = Utf8 SimpleClass.java
#16 = NameAndType #7:#8 // "<init>":()V
#17 = NameAndType #5:#6 // simpleField:I
#18 = Utf8 LSimpleClass;
#19 = Utf8 java/lang/Object
常量字段(类常量)
带有final标记的常量字段在class文件里会被标记成ACC_FINAL.
比如
public class SimpleClass {
public final int simpleField = 100;
}
字段的描述信息会标记成ACC_FINAL:
public static final int simpleField = 100;
Signature: I
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: int 100
对应的初始化代码并不变:
4: aload_0
5: bipush 100
7: putfield #2 // Field simpleField:I
静态变量
带有static修饰符的静态变量则会被标记成ACC_STATIC:
public static int simpleField;
Signature: I
flags: ACC_PUBLIC, ACC_STATIC
不过在实例的构造方法<init>中却再也找 不到对应的初始化代码了。因为static变量会在类的构造方法<cinit> 中进行初始化,并且它用的是putstatic指令而不是putfiled。
static {};
Signature: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #2 // Field simpleField:I
5: return
未完待续。
原创文章转载请注明出处:
http://it.deepinmind.com
英文原文链接
分享到:
相关推荐
Java字节码框架ASM是一个强大的库,它允许程序员在运行时动态生成和修改Java类和接口的字节码。ASM提供了对JVM字节码的底层访问,这使得开发者能够实现诸如AOP(面向切面编程)或者元编程等高级功能。 首先,我们...
BTrace 使用了前面提到的 Java.lang.instrument 包中的 API,允许开发者在运行时安全地添加或修改类的字节码,以便进行动态追踪。BTrace 的设计目标是提供一种简单、安全的方式来监控和诊断 Java 应用程序,而无需...
Java卡应用执行机制主要是将class文件的字节码转换成可以被Java虚拟机识别并执行的虚拟机代码。由于Oracle公司在JCVMSpecification中明确规定了Java虚拟机所支持的所有指令集,因此这是一个标准的转换过程。 二、B...
3. 编译与打包:使用Java Card开发工具,如Oracle的Java Card SDK,将Applet编译成字节码,并打包成CAP文件。 4. 个人化:将CAP文件加载到智能卡,完成卡片的个性化过程。 5. 测试验证:进行功能测试和安全性测试,...
在性能上,Java通过即时编译(JIT)技术将字节码转化为机器码,提升了运行效率。并且,Java内存管理的垃圾回收机制自动处理不再使用的对象,减少了内存泄漏的问题,让开发者可以更专注于业务逻辑。 Java在计算机...
另外,Servlet 编译后的 Java 字节码只有在被请求时才执行,同时服务器会缓存运行的 Servlet,所以尽管当首次调用 Servlet 时会有几秒钟的加载时间,但对后续的客户端请求响应会非常快。 本文对 JSP、ASP 和 PHP 三...
类装载器负责从源(如.class或.jar文件,或其它来源如内存、网络)获取字节码并将其转化为可执行的形式。 类装载器的主要功能包括: 1. 提供对类的请求服务:当JVM需要一个类时,类装载器负责找到并返回这个类。 2....
- **跨平台性**:通过将Java源代码编译为中间代码(字节码),JVM可以在多种不同的平台上解释执行这些字节码,实现了“一次编写,到处运行”的理念。 - **安全性**:JVM提供了沙盒执行环境,可以防止恶意代码对...
4. **Luajit-decomp**:专门针对LuaJIT字节码进行反编译的工具,通过先将LuaJIT字节码转换为汇编语言,再转换为Lua源代码的方式进行反编译。 #### 五、结论 综上所述,Lua脚本的加密与解密是一个复杂而多变的过程...
JVM将字节码转换为特定平台的机器指令执行,实现了Java的“一次编写,到处运行”特性。 5)与Java语言的关联性:虽然Class文件主要由Java源代码编译而来,但理论上其他编程语言也可以生成符合Class文件格式的二进制...
Java编译器将源代码转换为字节码,然后java解释器生成机器代码,该机器代码由运行java程序的机器直接执行。它可靠,分布式,便携。它可用于开发独立应用程序或基于Web的应用程序。 PHP被称为超文本预处理器,它是一...
此外,Java的虚拟机(JVM)使得它可以在不同的操作系统上运行相同的字节码,这也是其跨平台特性的体现。 C/C++语言因其高效性和对硬件的控制能力,被认为是开发系统底层软件、嵌入式系统和游戏开发的首选。C语言在...
- Java 代码需要先通过 Java 编译器编译成字节码(.class 文件),然后在 Java 虚拟机(JVM)上运行,这使得 Java 程序能跨平台运行,但需要客户端安装相应的 JVM。 - JavaScript 代码则是解释执行的,无需预先...
- **Android Runtime**:Dalvik虚拟机是Android运行时的核心,它执行.dex格式的字节码文件,这些文件经过Java编译器编译并通过dx工具转换。Dalvik虚拟机基于寄存器,优化了内存使用,并且每个应用都有自己的进程...
如果线程执行的是Java方法,计数器记录字节码的下一条指令;如果是Native方法,计数器值为空。 2. **Java虚拟机栈**:同样为线程私有,用于存储栈帧,每个栈帧包含局部变量表、操作数栈、动态链接和方法出口等信息...
- **jadx**: 是一款强大的Java字节码反编译器,能够将DEX格式的文件(如Android APK中的主类文件)转换成易于阅读的Java源代码。通过这种方式,开发者和安全研究人员可以更轻松地理解应用程序的行为。 - **frida**: ...
- **Dalvik Virtual Machine (DVM)**:这是一个专门为Android设计的虚拟机,用于执行应用程序中的.dex格式的字节码文件。 - **Core Libraries**:这些核心库包含了一系列的标准Java库,以及一些专为Android设计的...
3-2 从字节码角度剖析线程不安全操作.mp4 3-3 原子性操作.mp4 3-4 深入理解synchronized.mp4 3-5 volatile关键字及其使用场景.mp4 3-6 单例与线程安全.mp4 3-7 如何避免线程安全性问题.mp4 4-1 锁的分类.mp4 ...
3-2 从字节码角度剖析线程不安全操作.mp4 3-3 原子性操作.mp4 3-4 深入理解synchronized.mp4 3-5 volatile关键字及其使用场景.mp4 3-6 单例与线程安全.mp4 3-7 如何避免线程安全性问题.mp4 4-1 锁的分类.mp4 ...
3-2 从字节码角度剖析线程不安全操作.mp4 3-3 原子性操作.mp4 3-4 深入理解synchronized.mp4 3-5 volatile关键字及其使用场景.mp4 3-6 单例与线程安全.mp4 3-7 如何避免线程安全性问题.mp4 4-1 锁的分类.mp4 ...