`
deepinmind
  • 浏览: 451436 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
1dc14e59-7bdf-33ab-841a-02d087aed982
Java函数式编程
浏览量:41622
社区版块
存档分类
最新评论

Java字节码运行浅析

JVM 
阅读更多
明白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

    英文原文链接
    6
    0
    分享到:
    评论

    相关推荐

      java字节码框架ASM操作字节码的方法浅析

      Java字节码框架ASM是一个强大的库,它允许程序员在运行时动态生成和修改Java类和接口的字节码。ASM提供了对JVM字节码的底层访问,这使得开发者能够实现诸如AOP(面向切面编程)或者元编程等高级功能。 首先,我们...

      20201210技术分享--java动态追踪技术浅析.pptx

      BTrace 使用了前面提到的 Java.lang.instrument 包中的 API,允许开发者在运行时安全地添加或修改类的字节码,以便进行动态追踪。BTrace 的设计目标是提供一种简单、安全的方式来监控和诊断 Java 应用程序,而无需...

      浅析Java卡应用执行机制及虚拟机能效优化.pdf

      Java卡应用执行机制主要是将class文件的字节码转换成可以被Java虚拟机识别并执行的虚拟机代码。由于Oracle公司在JCVMSpecification中明确规定了Java虚拟机所支持的所有指令集,因此这是一个标准的转换过程。 二、B...

      浅析Java智能卡的应用与开发.zip

      3. 编译与打包:使用Java Card开发工具,如Oracle的Java Card SDK,将Applet编译成字节码,并打包成CAP文件。 4. 个人化:将CAP文件加载到智能卡,完成卡片的个性化过程。 5. 测试验证:进行功能测试和安全性测试,...

      浅析计算机软件JAVA编程的特点及应用.zip

      在性能上,Java通过即时编译(JIT)技术将字节码转化为机器码,提升了运行效率。并且,Java内存管理的垃圾回收机制自动处理不再使用的对象,减少了内存泄漏的问题,让开发者可以更专注于业务逻辑。 Java在计算机...

      浅析JSP、ASP与PHP技术.pdf

      另外,Servlet 编译后的 Java 字节码只有在被请求时才执行,同时服务器会缓存运行的 Servlet,所以尽管当首次调用 Servlet 时会有几秒钟的加载时间,但对后续的客户端请求响应会非常快。 本文对 JSP、ASP 和 PHP 三...

      [浅析J2EE应用服务器的JAVA类装载器]python回朔异常的模块.docx

      类装载器负责从源(如.class或.jar文件,或其它来源如内存、网络)获取字节码并将其转化为可执行的形式。 类装载器的主要功能包括: 1. 提供对类的请求服务:当JVM需要一个类时,类装载器负责找到并返回这个类。 2....

      浅析JVM

      - **跨平台性**:通过将Java源代码编译为中间代码(字节码),JVM可以在多种不同的平台上解释执行这些字节码,实现了“一次编写,到处运行”的理念。 - **安全性**:JVM提供了沙盒执行环境,可以防止恶意代码对...

      浅析android手游lua脚本的加密与解密(番外篇之反编译的对抗)

      4. **Luajit-decomp**:专门针对LuaJIT字节码进行反编译的工具,通过先将LuaJIT字节码转换为汇编语言,再转换为Lua源代码的方式进行反编译。 #### 五、结论 综上所述,Lua脚本的加密与解密是一个复杂而多变的过程...

      浅析java class 文件

      JVM将字节码转换为特定平台的机器指令执行,实现了Java的“一次编写,到处运行”特性。 5)与Java语言的关联性:虽然Class文件主要由Java源代码编译而来,但理论上其他编程语言也可以生成符合Class文件格式的二进制...

      java与php的区别浅析

      Java编译器将源代码转换为字节码,然后java解释器生成机器代码,该机器代码由运行java程序的机器直接执行。它可靠,分布式,便携。它可用于开发独立应用程序或基于Web的应用程序。 PHP被称为超文本预处理器,它是一...

      浅析Java、C/C++、JavaScript、PHP、Python分别用来开发什么?

      此外,Java的虚拟机(JVM)使得它可以在不同的操作系统上运行相同的字节码,这也是其跨平台特性的体现。 C/C++语言因其高效性和对硬件的控制能力,被认为是开发系统底层软件、嵌入式系统和游戏开发的首选。C语言在...

      JavaScript 和 Java 的区别浅析

      - Java 代码需要先通过 Java 编译器编译成字节码(.class 文件),然后在 Java 虚拟机(JVM)上运行,这使得 Java 程序能跨平台运行,但需要客户端安装相应的 JVM。 - JavaScript 代码则是解释执行的,无需预先...

      基于Linux的Android OS平台应用浅析.pdf

      - **Android Runtime**:Dalvik虚拟机是Android运行时的核心,它执行.dex格式的字节码文件,这些文件经过Java编译器编译并通过dx工具转换。Dalvik虚拟机基于寄存器,优化了内存使用,并且每个应用都有自己的进程...

      浅析Java内存模型与垃圾回收

      如果线程执行的是Java方法,计数器记录字节码的下一条指令;如果是Native方法,计数器值为空。 2. **Java虚拟机栈**:同样为线程私有,用于存储栈帧,每个栈帧包含局部变量表、操作数栈、动态链接和方法出口等信息...

      App检测的静态逆向分析思路浅析.pdf

      - **jadx**: 是一款强大的Java字节码反编译器,能够将DEX格式的文件(如Android APK中的主类文件)转换成易于阅读的Java源代码。通过这种方式,开发者和安全研究人员可以更轻松地理解应用程序的行为。 - **frida**: ...

      Android嵌入式系统架构及内核浅析

      - **Dalvik Virtual Machine (DVM)**:这是一个专门为Android设计的虚拟机,用于执行应用程序中的.dex格式的字节码文件。 - **Core Libraries**:这些核心库包含了一系列的标准Java库,以及一些专为Android设计的...

      Java互联网架构多线程并发编程原理及实战 视频教程 下载.zip

      3-2 从字节码角度剖析线程不安全操作.mp4 3-3 原子性操作.mp4 3-4 深入理解synchronized.mp4 3-5 volatile关键字及其使用场景.mp4 3-6 单例与线程安全.mp4 3-7 如何避免线程安全性问题.mp4 4-1 锁的分类.mp4 ...

      Java互联网架构多线程并发编程原理及实战 视频教程 下载4.zip

      3-2 从字节码角度剖析线程不安全操作.mp4 3-3 原子性操作.mp4 3-4 深入理解synchronized.mp4 3-5 volatile关键字及其使用场景.mp4 3-6 单例与线程安全.mp4 3-7 如何避免线程安全性问题.mp4 4-1 锁的分类.mp4 ...

      Java互联网架构多线程并发编程原理及实战 视频教程 下载2.zip

      3-2 从字节码角度剖析线程不安全操作.mp4 3-3 原子性操作.mp4 3-4 深入理解synchronized.mp4 3-5 volatile关键字及其使用场景.mp4 3-6 单例与线程安全.mp4 3-7 如何避免线程安全性问题.mp4 4-1 锁的分类.mp4 ...

    Global site tag (gtag.js) - Google Analytics