精华内容
下载资源
问答
  • 本篇文章通过我和三妹的对话来谈一谈“Java程序在编译和运行时发生什么”。 没见过这么有趣的标题吧?“语不惊人死不休”,没错,本篇文章的标题就是这么酷炫,接受不了的同学就别点进来看了,所谓好奇心害死猫;...

    大家好,我是沉默王二。本篇文章通过我和三妹的对话来谈一谈“Java程序在编译和运行时发生了什么”。

    没见过这么有趣的标题吧?“语不惊人死不休”,没错,本篇文章的标题就是这么酷炫,接受不了的同学就别点进来看了,所谓好奇心害死猫;能够接受的同学我只能说你赚到了,你不仅能在阅读的过程中感受到思维的乐趣,还真的能学习到知识。下面就由我来介绍一下故事的背景吧。

    我有一个漂亮如花的妹妹(见上图),她叫什么呢?我想聪明的读者朋友们都能猜得出:沉默王三,没错,年方三六。父母正考虑让她向我学习,做一名正儿八经的 Java 程序员。我期初是想反抗的,因为程序员这行业容易掉头发。但家命难为啊,与其反抗,不如做点更积极的事情,写点有趣的文章,教妹妹如何更快地掌握 Java 这门编程语言。毕竟程序员还算得上高薪(都是拿命换的啊)。

    (铺垫结束,正文开始)

    “二哥,看了上一篇 Hello World 的程序后,我很好奇,它是怎么在屏幕上打印出‘Hello World’呢?”

    “三妹啊,‘Hello World’这段代码之所以能够正常工作,可以分为两大步骤:编译和运行,下面二哥会详细来说道说道。”

    01、编译时发生了什么

    现在,我们有一个名叫 Cmower.java 的文件,它的内容是这样的:

    package main.java.com.cmower.java_demo.javatpoint.five;
    
    public class Cmower {
        public static void main(String[] args) {
            System.out.println("大家好,我们沉默王二的妹妹沉默王三");
        }
    }
    

    然后我们点击 IDEA 工具栏中的锤子按钮(Build Project)。此时可以在 src 的同级目录 target 的包路径下找到一个 Cmower.class 的文件(如果找不到的话,在目录上右键选择「Reload from Disk」)。

    可以双击打开它。

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package com.cmower.java_demo.javatpoint.five;
    
    public class Cmower {
        public Cmower() {
        }
    
        public static void main(String[] args) {
            System.out.println("大家好,我们沉默王二的妹妹沉默王三");
        }
    }
    

    IntelliJ IDEA 默认会用 Fernflower 反编译工具将字节码文件反编译为我们可以看得懂的 Java 源代码。

    实际上,字节码文件 Cmower.class 的内容是下面这样的:

    // class version 52.0 (52)
    // access flags 0x21
    public class com/cmower/java_demo/javatpoint/five/Cmower {
    
      // compiled from: Cmower.java
    
      // access flags 0x1
      public <init>()V
       L0
        LINENUMBER 3 L0
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
        RETURN
       L1
        LOCALVARIABLE this Lcom/cmower/java_demo/javatpoint/five/Cmower; L0 L1 0
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x9
      public static main([Ljava/lang/String;)V
       L0
        LINENUMBER 5 L0
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        LDC "\u5927\u5bb6\u597d\uff0c\u6211\u4eec\u6c89\u9ed8\u738b\u4e8c\u7684\u59b9\u59b9\u6c89\u9ed8\u738b\u4e09"
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
       L1
        LINENUMBER 6 L1
        RETURN
       L2
        LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
        MAXSTACK = 2
        MAXLOCALS = 1
    }
    

    可以通过「View」中的「Show Bytecode」查看。

    “三妹啊,懂了没?也就是说,在编译阶段,Java 编译器会将 Java 源代码文件编译为字节码文件。二哥再给你画幅图。”

    02、运行时发生了什么

    要想运行 Java 文件,可以点击 IDEA 工具栏中的运行按钮。此时会在底部视图中输出文字“大家好,我们沉默王二的妹妹沉默王三”。

    那运行时都发生了什么呢?来画幅图吧!

    字节码加载器:JVM 的一个子系统,用来载入字节码文件。

    字节码验证:检查字节码中是否存在可能违法对象访问权限的非法代码。

    指令执行:读取字节码流,执行指令。

    “懂了没?三妹,字节码在运行前仍然做了不少工作,以防被篡改,保证安全性。”

    “二哥,我懂了,你讲得可真棒!”

    03、鸣谢

    本篇文章为《教妹学Java》专栏的第五篇文章,是不是有趣得很?我相信你能感受的到,这可是全网独一份,我看到已经有人在模仿了。现在定价只需 9.9 元,9.9 元你连一杯奶茶都买不到,但却能买下二哥精心制作的专栏,据说 CSDN 已经考虑涨价了,毕竟已经卖出一百多份了。

    我知道,购买专栏的同学都是冲着二哥的名声来的,毕竟二哥是 CSDN 的明星博主,哈哈。为表谢意,我再附送上个人微信(qing_gee),你有什么问题都可以来咨询。

    上一篇回顾:教妹学Java(四):Hello World

    PS:本篇文章中的示例代码已经同步到 GitHub,地址为 itwanger.JavaPoint,欢迎大家 star 和 issue。

    原创不易,喜欢就点个赞,因为你一个小小的举动,就会让这个世界多一份美好。

    展开全文
  • 什么是即时编译

    千次阅读 2020-04-09 14:08:50
    什么是即时编译

    即时编译(Just In Time,JIT)

    在了解什么是即时编译之前,我们需要先弄明白一个问题:Java 虚拟机是怎么运行 Java 字节码的?

    下面我将以标准 JDK 中的 HotSpot 虚拟机为例,从虚拟机以及底层硬件两个角度,简单给你讲一讲 Java 虚拟机具体是怎么运行 Java 字节码的。

    从虚拟机视角来看,执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。

    在运行过程中,每当调用进入一个 Java 方法,Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且 Java 虚拟机不要求栈帧在内存空间里连续分布。

    对于方法区、栈帧、局部变量表等概念不懂的读者可以看我的另一片文章《java 运行时数据区》

    当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。

    从硬件视角来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码

    在 HotSpot 里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。

    即时编译后的代码也会被保存到方法区中,当这个方法再次被调用时,就省去了解释的过程,直接执行即时编译缓存的机器码,以达成提升性能的目的。

    到这里要明白三个概念:

    编译执行: 通过编译器,将高级语言代码编译成对应平台的机器码文件,交于对应平台执行。机器码就是直接能被计算机理解的代码。

    解释执行: 通过解释器,在代码执行时逐条翻译成机器码,不做保存。

    即时编译(JIT): 将热点代码编译成与本地平台相关的机器码,并保存到内存。

    即时编译

    Java 程序最初都是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“ 热点代码 ”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,这个过程就叫即时编译,运行时完成这个任务的后端编译器被称为即时编译器

    既然编译后的机器码效率高,为什么不把全部代码提前编译

    至于为什么不采用提前编译(Ahead Of Time,AOT)直接编译的方法,在峰值性能差不多的这个前提下,线下编译和即时编译就是两种选项,各有优缺点。JVM这样做,主要也是看重字节码的可移植性,而牺牲了启动性能(程序需要 JIT 预热才能达到最高性能)。

    另外呢,现代工程语言实现都是抄来抄去的。JVM也引入了AOT编译,在线下将Java代码编译成可链接库。

    如果你只想了解什么是即时编译看到这里就可以了,接下来的内容是一些扩展。

    即时编译器

    HotSpot 内置了三个即时编译器,其中有两个编译器存在已久、分别被称为“客户端编译器”(Client Compiler)和“服务端编译器”(Server Compiler),或者简称为 C1 编译器和 C2 编译器,C2 相较于 C1 编译出的机器码优化程度更高,但是耗时也更长。第三个是 JDK 10 才出现的、长期目标是替代 C2 的 Graal 编译器。Graal 是一个实验性质的即时编译器,可以通过参数 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用,并且替换 C2。

    在 Java 7 以前,我们需要根据程序的特性选择对应的即时编译器。对于执行时间较短的,或者对启动性能有要求的程序,我们采用编译效率较快的 C1,对应参数 -client。

    对于执行时间较长的,或者对峰值性能有要求的程序,我们采用生成代码执行效率较快的 C2,对应参数 -server。

    用 C1 获取更高的编译速度,用 C2 获取更好的编译质量。

    由于即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化程度越高的代码,所花费的时间也就越长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行阶段的速度也有所影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot 虚拟机在编译子系统中加入了分层编译的功能。

    分层编译可以简单理解为:对于热点代码使用 C1 编译器,对于热点代码中的热点代码使用 C2 编译获取更高地性能。

    分层编译将 Java 虚拟机的执行状态分为了五个层次,详见本文附录

    Java 8 默认开启了分层编译。不管是开启还是关闭分层编译,原本用来选择即时编译器的参数 -client 和 -server 都是无效的。当关闭分层编译的情况下,Java 虚拟机将直接采用 C2。

    如果你希望只是用 C1,那么你可以在打开分层编译的情况下使用参数 -XX:TieredStopAtLevel=1。在这种情况下,Java 虚拟机会在解释执行之后直接由 1 层的 C1 进行编译。

    混合模式

    目前主流的 Java 虚拟机,譬如 HotSpot、OpenJ9 等,内部都同时包含解释器与编译器,解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候解释器可以首先发挥作用省去编译的时间立即运行。当程序启动后,随着时间的偏移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。

    你也可以使用参数 -Xint 强制虚拟机运行于“解释模式”(Interpreted Mode),这时候编译器完全不介入工作,全部代码都使用解释方式执行。另外,也可以使用参数 -Xcomp 强制虚拟机运行于“编译模式”(compiled mode),这时候将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。可以通过 -version 命令的输出结果显示这三种模式,内容如下,请注意加粗部分:

    $ java -version
    java version “1.8.0_212”
    Java™ SE Runtime Environment (build 1.8.0_212-b10)
    Java HotSpot™ 64-Bit Server VM (build 25.212-b10, mixed mode)

    $ java -Xint -version
    java version “1.8.0_212”
    Java™ SE Runtime Environment (build 1.8.0_212-b10)
    Java HotSpot™ 64-Bit Server VM (build 25.212-b10, interpreted mode)

    $ java -Xcomp -version
    java version “1.8.0_212”
    Java™ SE Runtime Environment (build 1.8.0_212-b10)
    Java HotSpot™ 64-Bit Server VM (build 25.212-b10, compiled mode)

    同时,解释器还可以作为编译器激进优化时后备的“逃生门”(如果情况允许,HotSpot 虚拟机中也会采用不进行激进优化的客户端编译器 C1 充当“逃生门”的角色),让编译器根据概率选择一些不能保证所有情况都正确,但大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类以后,类型继承结构出现变化、出现“罕见陷阱”(Uncommon Trap)时可以通过逆优化(Deoptimization)退回到解释状态继续执行,因此在整个 Java 虚拟机执行架构里,解释器与编译器经常是相辅相成地配合工作,其交互关系如下图:

    在这里插入图片描述

    ps:讲真,以上最后一小段我也还没理解是什么意思。。

    编译对象与触发条件

    我们提到了在运行过程中会被即时编译器编译的目标是"热点代码",这里所指的热点代码主要由两类,包括:

    • 被多次调用的方法
    • 被多次执行的循环体

    对于这两种情况,编译的目标对象都是整个方法体,而不会是单独的循环体。第一种由于是依靠方法调用触发的编译,那编译器理所当然地会以整个方法作为编译对象,这种编译也是虚拟机中标准的即时编译方式。而对于后一种情况,尽管编译动作是由循环体所触发的,热点只是方法的一部分,但编译器依然必须以整个方法作为编译对象,只是执行入口(从方法的第几条字节码指令开始执行)会稍有不同,编译时会传入执行入口点字节码序号(Byte Code Index,BCI)。这种编译方式因为编译发生在方法执行的过程中,因此别很形象的称为“栈上替换”(On Stack Replacement,OSR),即方法的栈帧还在栈上,方法就被替换了。

    OSR 实际上是一种技术,它指的是在程序执行过程中,动态地替换掉 Java 方法栈桢,从而使得程序能够在非方法入口处进行解释执行和编译后的代码之间的切换。事实上,逆优化(也称去优化,deoptimization)采用的技术也可以称之为 OSR。

    那么多少次才算“多次”呢Java 虚拟机又是如何统计次数的?解决了这两个问题,也就解答了即时编译被触发的条件。

    要知道某段代码是不是热点代码,是不是要触发即时编译,这个行为称为“热点探测”(Hot Spot Code Detection),其实进行热点探测并不一定要知道方法具体被调用了多少次,主流的判定方式有两种,分别为:

    • 基于采样的热点探测(Sample Based Hot Spot Code Detection)。这种方法会周期性的检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那么这个方法就是”热点方法“。基于采样的热点探测的好处是实现简单高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热点,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
    • 基于计数器的热点探测(Counter Based Hot Spot Code Detection)。这种方法会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。这种统计方法实现起来要麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系。但是它的统计结果相对来说更加精确严谨。

    HotSpot 虚拟机中使用了基于计数器的热点探测,为了实现热点计数,HotSpot 为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter,“回边”的意思就是指在循环边界往回跳转)。当虚拟机运行参数确定的情况下,这两个计数器都有一个明确的阈值,计数器阈值一旦溢出,就会触发即时编译。

    方法调用计数器:这个计数器就是用于统计方法被调用的次数,他的默认阈值在 C1 模式下是 1500 次,在 C2 模式下是 10000 次,这个阈值可以通过虚拟机参数 -XX:CompileThreshold 来人为设定。

    当启用分层编译时,Java 虚拟机将不再采用由参数 -XX:CompileThreshold 指定的阈值(该参数失效),而是使用另一套阈值系统。在这套系统中,阈值的大小是动态调整的。

    所谓的动态调整其实并不复杂:在比较阈值时,Java 虚拟机会将阈值与某个系数 s 相乘。该系数与当前待编译的方法数目成正相关,与编译线程的数目成负相关。

    系数的计算方法为:
    s = queue_size_X / (TierXLoadFeedback * compiler_count_X) + 1
     
    其中 X 是执行层次,可取 3 或者 4;
    queue_size_X 是执行层次为 X 的待编译方法的数目;
    TierXLoadFeedback 是预设好的参数,其中 Tier3LoadFeedback 为 5,Tier4LoadFeedback 为 3;
    compiler_count_X 是层次 X 的编译线程数目。
    

    在 64 位 Java 虚拟机中,默认情况下编译线程的总数目是根据处理器数量来调整的(对应参数 -XX:+CICompilerCountPerCPU,默认为 true;当通过参数 -XX:+CICompilerCount=N 强制设定总编译线程数目时,CICompilerCountPerCPU 将被设置为 false)。

    Java 虚拟机会将这些编译线程按照 1:2 的比例分配给 C1 和 C2(至少各为 1 个)。举个例子,对于一个四核机器来说,总的编译线程数目为 3,其中包含一个 C1 编译线程和两个 C2 编译线程。

    对于四核及以上的机器,总的编译线程的数目为:
    n = log2(N) * log2(log2(N)) * 3 / 2
    其中 N 为 CPU 核心数目。
    

    当启用分层编译时,即时编译具体的触发条件如下。

    当方法调用次数大于由参数 -XX:TierXInvocationThreshold 指定的阈值乘以系数,或者当方法调用次数大于由参数 -XX:TierXMINInvocationThreshold 指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数 -XX:TierXCompileThreshold 指定的阈值乘以系数时,便会触发 X 层即时编译。
     
    触发条件为:
    i > TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s  && i + b > TierXCompileThreshold * s)
    
    

    其中 i 为调用次数,b 为循环回边次数。

    如果没有做任何设置,执行引擎默认不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被即时编译器编译完成。当编译工作完成后,这个方法的调用入口地址就会被系统自动改成新值,下一次调用该方法就会使用已编译的版本了,整个即时编译的交互过程如下:

    在这里插入图片描述

    在默认设置下,方法调用计数器统计的并不是方法被调用的解决次数,而是一个相对的执行频率,即一段时间之内方法的被调用次数。当超过一定的时间限制,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那该方法的调用计数器就会被减少一半,这个过程被称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time),进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,另外还可以使用 -XX:CounterHalfLifeTime 参数设置半衰周期的时间,单位是秒。

    现在我们再来看另一个计数器——回边计数器,它的作用是统计一个方法中循环体代码执行的次数(准确的说,应当是回边的次数而不是循环次数,因为并非所有的循环都是回边,如空循环实际上就可以视为自己跳转到自己的过程,因此并不算控制流向后跳转,也不会被回边计数器统计),在字节码中遇到控制流向后跳转的指令就称为“回边”,很显然建立回边计数器统计的目的就是为了触发栈上的替换编译。

    关于回边计数器的阈值,虽然 HotSpot 虚拟机也提供了一个类似于方法调用计数器阈值 -XX:CompileThreshold 的参数 -XX:BackEdgeThreshold 供用户设置,但是当前的 HotSpot 虚拟机实际上并未使用此参数,我们必须设置另外一个参数 -XX:OnStackReplacePercentage 来间接调整回边计数器的阈值,其计算公式有如下两种:

    1. 虚拟机运行在 C1 模式下,回边计数器阈值计算公式为:方法调用计数器阈值(-XX:CompileThreshold)乘以 OSR 比率(-XX:OnStackReplacePercentage)除以 100,其中 -XX:OnStackReplacePercentage 默认值为933,如果都是默认值,那 C1 模式的回边计数器的阈值是 13995;
    2. 虚拟机运行在 C2 模式下,回边计数器阈值的计算公式为:方法调用计数器阈值(-XX:CompileThreshold)乘以 (OSR 比率(-XX:OnStackReplacePercentage)减去解释器监控比率(-XX:InterpreterProfilePercentage)的差值)除以 100。其中 -XX:OnStackReplacePercentage 默认值为 140,-XX:InterpreterProfilePercentage 默认值为 33,如果都取默认值,那 C2 回边计数器的阈值为 10700.

    当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本,如果有的话,它将优先执行已编译的代码,否则就把回边计数器的值加一,然后判断方法调用计数器与回边计数器值之和是否超过回边计数器的阈值。当超过阈值时,将会提交一个栈上替换编译请求,并且把回边计数器的值稍微降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果,整个过程如下:

    在这里插入图片描述

    与计数器方法不同,回边计数器没有计数器热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。当计数器溢出的时候,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程。

    附录

    分层编译将 Java 虚拟机的执行状态分为了五个层次

    1. 解释执行;
    2. 执行不带 profiling 的 C1 代码;
    3. 执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
    4. 执行带所有 profiling 的 C1 代码;
    5. 执行 C2 代码。

    通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。然而,对于 C1 代码的三种状态,按执行效率从高至低则是 1 层 > 2 层 > 3 层。

    其中 1 层的性能比 2 层的稍微高一些,而 2 层的性能又比 3 层高出 30%。这是因为 profiling 越多,其额外的性能开销越大。

    这里解释一下,profiling 是指在程序执行过程中,收集能够反映程序执行状态的数据。这里所收集的数据我们称之为程序的 profile。

    在 5 个层次的执行状态中,1 层和 4 层为终止状态。当一个方法被终止状态编译过后,如果编译后的代码并没有失效,那么 Java 虚拟机是不会再次发出该方法的编译请求的。

    在这里插入图片描述

    这里我列举了 4 个不同的编译路径。通常情况下,热点方法会被 3 层的 C1 编译,然后再被 4 层的 C2 编译。

    如果方法的字节码数目比较少(如 getter/setter),而且 3 层的 profiling 没有可收集的数据。

    那么,Java 虚拟机断定该方法对于 C1 代码和 C2 代码的执行效率相同。在这种情况下,Java 虚拟机会在 3 层编译之后,直接选择用 1 层的 C1 编译。由于这是一个终止状态,因此 Java 虚拟机不会继续用 4 层的 C2 编译。

    在 C1 忙碌的情况下,Java 虚拟机在解释执行过程中对程序进行 profiling,而后直接由 4 层的 C2 编译。在 C2 忙碌的情况下,方法会被 2 层的 C1 编译,然后再被 3 层的 C1 编译,以减少方法在 3 层的执行时间。

    选择用 1 层的 C1 编译。由于这是一个终止状态,因此 Java 虚拟机不会继续用 4 层的 C2 编译。

    在 C1 忙碌的情况下,Java 虚拟机在解释执行过程中对程序进行 profiling,而后直接由 4 层的 C2 编译。在 C2 忙碌的情况下,方法会被 2 层的 C1 编译,然后再被 3 层的 C1 编译,以减少方法在 3 层的执行时间。

    参考 《深入拆解Java虚拟机》、《深入理解Java虚拟机》

    展开全文
  • 比如说你把 for 写成了 fot 的话就会产生编译错误。...运行错误一般是程序通过编译没有发生错误,但是程序执行的时候遇到了某些阻碍,比如说你要读取一个文本,但是该文本不存在,这就属于运行错

    比如说你把 for 写成了 fot 的话就会产生编译错误。编译错误一般是语法错误

    而运行错误是指,在运行期间(此时已经没有编译错误了)产生的错误,比如说,计算 5 / 0 什么的。


    编译错误一般是指语法错误,比如说没加分号,少写了括号,这都属于编译错误。
    运行错误一般是程序通过编译没有发生错误,但是程序执行的时候遇到了某些阻碍,比如说你要读取一个文本,但是该文本不存在,这就属于运行错误,因为你的语法本身没有错误,所以编译正常通过,但是系统找不到你的源文件,所以会出现运行错误。


    编译错误应该还包括很明显的逻辑错误。
    比如说,你在某个方法的最后一行写上:
    if(false) return "";
    编译器会告诉你,函数需要一个返回值。因为……很明显


    编译错误:在编译的时候就出现的错误,不能运行,也就是javac命令出现的错误. 是违背了Java的语法规则.如果你用的是Eclipse的话,就是编码时出现的红色部分
    运行时错误:可以通过编译,但是在运行的时候出现异常,比如空指针异常,主要是一些逻辑错误


    展开全文
  • 文章目录前言编译方式一步到位分步执行编译流程预处理编译汇编链接总结源代码 前言 一直好奇程序的编译过程到底做了哪些工作,后来学会在Ubuntu上使用gcc编译程序,知道了生成可执行文件需要分为预编译编译、汇编...

    前言

    一直好奇程序的编译过程到底做了哪些工作,后来学会在Ubuntu上使用gcc编译程序,知道了生成可执行文件需要分为预编译、编译、汇编和链接4个步骤,逐渐了解了其中的细节,但是过一段时间之后总是记不太清楚了,所以总结一下增强记忆,同时方便日后查找使用。

    编译方式

    一步到位

    使用gcc命令可以一步将main.c源文件编译生成最终的可执行文件main_direct

    gcc main.c –o main_direct
    

    分步执行

    gcc的编译流程通常认为包含以下四个步骤,实际上就是将上面的命令分成4步执行,这也是gcc命令实际的操作流程,生成的可执行文件main与上面单条命令生成的可执行文件main_direct是一模一样的

    • 预处理,生成预编译文件(.i文件):gcc –E main.c –o main.i
    • 编译,生成汇编代码(.s文件):gcc –S main.i –o main.s
    • 汇编,生成目标文件(.o文件):gcc –c main.s –o main.o
    • 链接,生成可执行文件(executable文件):gcc main.o –o main

    编译流程

    这里的编译是指将源文件(.c)生成可执行文件(executable)的这个完整的过程,而不是上面提到的四个步骤中的第二步,为了弄清楚编译过程究竟都做了哪些工作,接下来我们可以分步骤来看一下gcc编译.c文件的过程,了解了每一步的内容,也就明白了整个编译流程,先给出源文件 mian.c 的源代码。

    #include <stdio.h>
    #define A 100
    
    // calc sum
    int sum(int a, int b)
    {
        return a + b;
    }
    
    int main()
    {
        int b = 1;
        int c = sum(A, b);
        printf("sum = %d\n", c);
    
        return 0;
    }
    

    预处理

    预处理又叫预编译,是完整编译过程的第一个阶段,在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容,对于C语言来说预处理的可执行程序叫做 cpp,全称为C Pre-Processor(C预处理器),是一个与 C 编译器独立的小程序,预编译器并不理解 C 语言语法,它仅是在程序源文件被编译之前,实现文本替换的功能。简单来说,预处理就是将源代码中的预处理指令根据语义预先处理,并且进行一下清理、标记工作,然后将这些代码输出到一个 .i 文件中等待进一步操作。

    一般地,C/C++ 程序的源代码中包含以 # 开头的各种编译指令,被称为预处理指令,其不属于 C/C++ 语言的语法,但在一定意义上可以说预处理扩展了 C/C++。根据ANSI C 定义,预处理指令主要包括:文件包含、宏定义、条件编译和特殊控制等4大类。

    预处理阶段主要做以下几个方面的工作:

    1. 文件包含:#includeC 程序设计中最常用的预处理指令,格式有尖括号 #include <xxx.h> 和双引号 #include "xxx.h" 之分,分别表示从系统目录下查找和优先在当前目录查找,例如常用的 #include <stdio.h> 指令,就表示使用 stdio.h 文件中的全部内容,替换该行指令。

    2. 添加行号和文件名标识: 比如在文件main.i中就有类似 # 2 "main.c" 2 的内容,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。

    3. 宏定义展开及处理: 预处理阶段会将使用 #define A 100 定义的常量符号进行等价替换,文中所有的宏定义符号A都会被替换成100,还会将一些内置的宏展开,比如用于显示文件全路径的__FILE__,另外还可以使用 #undef 删除已经存在的宏,比如 #undef A 就是删除之前定义的宏符号A

    4. 条件编译处理:#ifdef#ifndef#else#elif#endif等,这些条件编译指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理,将那些不必要的代码过滤掉,防止文件重复包含等。

    5. 清理注释内容: // xxx/*xxx*/ 所产生的的注释内容在预处理阶段都会被删除,因为这些注释对于编写程序的人来说是用来记录和梳理逻辑代码的,但是对编译程序来说几乎没有任何用处,所以会被删除,观察 main.i 文件也会发现之前的注释都被删掉了。

    6. 特殊控制处理: 保留编译器需要使用 #pragma 编译器指令,另外还有用于输出指定的错误信息,通常来调试程序的 #error 指令。

    查看main.i文件

    编译

    编译过程是整个程序构建的核心部分,也是最复杂的部分之一,其工作就是把预处理完生成的 .i 文件进行一系列的词法分析、语法分析、语义分析以及优化后产生相应的汇编代码文件,也就是 .s 文件,这个过程调用的处理程序一般是 cc 或者 ccl。汇编语言是非常有用的,因为它给不同高级语言的不同编译器提供了可选择的通用的输出语言,比如 CFortran 编译产生的输出文件都是汇编语言。

    1. 词法分析: 主要是使用基于有线状态机的Scanner分析出token,可以通过一个叫做 lex 的可执行程序来完成词法扫描,按照描述好的词法规则将预处理后的源代码分割成一个个记号,同时完成将标识符存放到符号表中,将数字、字符串常量存放到文字表等工作,以备后面的步骤使用。

    2. 语法分析: 对有词法分析产生的token采用上下文无关文法进行分析,从而产生语法树,此过程可以通过一个叫做 yacc 的可执行程序完成,它可以根据用户给定的语法规则对输入的记号序列进行解析,从而构建一棵语法树,如果在解析过程中出现了表达式不合法,比如括号不匹配,表达式中缺少操作符、操作数等情况,编译器就会报出语法分析阶段的错误。

    3. 语义分析: 此过程由语义分析器完成,编译器 cc 所能分析的语义都是静态语义,是指在编译期间可以确定的语义,通常包括声明和类型的匹配,类型的转换等。比如将一个浮点型的表达式赋值给一个整型的表达式时,语义分析程序会发现这个类型不匹配,编译器将会报错。而动态语义一般指在运行期出现的语义相关问题,比如将0作为除数是一个运行期语义错误。语义分析过程会将所有表达式标注类型,对于需要隐式转换的语法添加转换节点,同时对符号表里的符号类型做相应的更新。

    4. 代码优化: 此过程会通过源代码优化器会在源代码级别进行优化,针对于编译期间就可以确定的表达式(例如:100+1)给出确定的值,以达到优化的目的,此外还包括根据机器硬件执行指令的特点对指令进行一些调整使目标代码比较短,执行效率更高等操作。

    查看main.s文件

    汇编

    汇编过程是整个程序构建中的第三步,是将编译产生的汇编代码文件转变成可执行的机器指令。相对来说比较简单,每个汇编语句都有相对应的机器指令,只需根据汇编代码语法和机器指令的对照表翻译过来就可以了,最终生成目标文件,也就是 .o 文件,完成此工作的可执行程序通常是 as。目标文件中所存放的也就是与源程序等效的目标的机器语言代码,通常至少包含代码段和数据段两个段,并且还要包含未解决符号表,导出符号表和地址重定向表等3个表。汇编过程会将extern声明的变量置入未解决符号表,将static声明的全局变量不置入未解决符号表,也不置入导出符号表,无法被其他目标文件使用,然后将普通变量及函数置入导出符号表,供其他目标文件使用。

    1. 代码段: 包含主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。

    2. 数据段: 主要存放程序中要用到的各种全局变量或静态的数据,一般数据段都是可读,可写,可执行的。

    3. 未解决符号表: 列出了在本目标文件里有引用但不存在定义的符号及其出现的地址。

    4. 导出符号表: 列出了本目标文件里具有定义,并且可以提供给其他目标文件使用的符号及其在出现的地址。

    5. 地址重定向表: 列出了本目标文件里所有对自身地址的引用记录。

    查看main.o文件

    链接

    链接过程是程序构建过程的最后一步,通常调用可执行程序 ld 来完成,可以简单的理解为将目标文件和库文件打包组装成可执行文件的过程,其主要内容就是把各个模块之间相互引用的部分都处理好,将一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得各个模块之间能够正确的衔接,成为一个能够被操作系统装入执行的统一整体。

    虽然汇编之后得到的文件已经是机器指令文件,但是依然无法立即执行,其中可能还有尚未解决的问题,比如源代码 main.c 中的 printf 这个函数名就无法解析,需要链接过程与对应的库文件对接,完成的重定位,将函数符号对应的地址替换成正确的地址。前面提到的库文件其实就是一组目标文件的包,它们是一些最常用的代码编译成目标文件后打成的包。比如 printf的头文件是 stdio.h,而它的实现代码是放在动态库 libc.so.6 中的,链接的时候就要引用到这个库文件。

    从原理上讲,连接的的工作就是把一些指令对其他符号地址的引用加以修正,主要包括了地址和空间分配、符号决议和重定位等步骤,根据开发人员指定的链接库函数的方式不同,链接过程可分为静态链接和动态链接两种,链接静态的库,需要拷贝到一起,链接动态的库需要登记一下库的信息。

    1. 静态链接: 函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时,代码将被装入到该进程的虚拟地址空间中,静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码,最终生成的可执行文件较大。

    2. 动态链接: 函数的代码被放到动态链接库或共享对象的某个目标文件中。链接处理时只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在这样该程序在被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间,根据可执行程序中记录的信息找到相应的函数代码。这种连接方法能节约一定的内存,也可以减小生成的可执行文件体积。

    查看main可执行文件

    总结

    • gcc编译器的工作过程:源文件 --> 预处理 --> 编译 --> 汇编 --> 链接 --> 可执行文件
    • gcc编译过程文件变化:main.c --> main.i --> mian.s --> main.o --> main
    • 通过上面分阶段的解释编译过程,我们也明白了gcc其实只是一个后台程序的包装,它会根据阶段要求来调用 cppccasld 等命令

    源代码

    整个编译过程产生的中间文件及最终结果可以通过传送门—— gcc编译项目 来获得,其中还有gccg++分别调用的对比,查看生成的文件可以发现,同样的源代码使用gccg++生成的文件是不一样的,总的来说使用g++编译生成的可执行文件要大一些。
    gcc_compile

    展开全文
  •  编译android4.4的时候,突然编译停止在target SharedLib: libwebviewchromium (out/target/product/generic/obj/SHARED_LIBRARIES/libwebviewchromium_intermediates/LINKED/libwebviewchromium.so) 最后出现编译...
  • 对于稍微大一点的 .NET 解决方案来说,编译时间通常都会长一些。如果项目结构和差量编译优化的好,可能编译完也就 5~30 秒,但如果没有优化好,那么出现 1~3 分钟都是可能的。 如果能够在编译出错的第一时间停止编译...
  • 什么是编译?何时需要预编译

    千次阅读 2020-02-13 16:34:47
    编译又称预处理,是整个编译过程最先做的工作,即程序执行前的一些预处理工作。主要处理#开头的指令。如拷贝#include包含的文件代码、替换#define定义的宏、条件编译#if等。 何时需要预编译: 总是使用不经常改动...
  • 如果出现编译都没法编译,不显示错误讯息,也不显示编译讯息;build也不发生成。这时候可以看一下自己的文件是不是放的太深了,可以尝试放到桌面试下。 转载于:https://blog.51cto.com/11372477/2398658...
  • 什么是编译

    千次阅读 2009-12-08 21:19:00
    文章(一)处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等 就是为编译做的预备工作的阶段 主要处理#开始的预编译指令 预编译指令指示了在程序正式编译前就由编译器进行的操作...
  • 在JSP第一次获得请求时,不管请求来自于客户端浏览器还是服务器上... 在编译时候如果发现jsp文件有任何语法错误,转换过程将中断,并向客户端发出出错信息;而如果编译成功了,则所转换产生的servlet代码被编译,然后
  • ),例如metaspace,代码即时编译缓存,直接内存,mmap内存 gc 担保失败,请参考:-XX:-HandlePromotionFailure 一般的,我们现在不会去太关心到底是哪种 GC,而是主要关心哪些 GC STW 的时间长,导致所有线程停止...
  • D:\Program Files\spring-framework\spring-beans>gradle cleanidea eclipse ...至于那三行会有什么影响暂时不清楚,起码能够通过编译,可以导入到eclipse中并查看源码。 图片可能有点看不清,能明白意思就成。
  • ads编译时所发生的错误

    千次阅读 2009-12-29 23:59:00
    第一个错误摘自:http://hi.baidu.com/cumtsun/blog/item/2b2aa40efc4d88e336d122df.html现象表述:选中所有文件执行全部编译时候,有2个警告提示:warning:C2207W:inventing extern int gliethttp_ini();...
  • 程序中的局部变量是编译时候分配地址的还是运行时分配的呢? [问题点数:40分] https://bbs.csdn.net/topics/350012472 borefo 结帖率 90% 程序中的局部变量是编译时候分配地址的还是运行时分配的呢? 按照我...
  • 2-JS预编译什么时候发生编译到底什么时候发生? 误以为预编译仅仅发生在script内代码块执行前 这倒并没有错 预编译确确实实在script代码内执行前发生了 但是它大部分会发生在函数执行前 3-实例分析
  • 这几天用VS2015编译boost 1.62很正常,但是在用VS2013编译boost1.62的时候,却报错了: \boost/type_traits/common_types.h(42) : fatal error C1001: 编译发生内部错误 根据网上找到了这篇文章《vs2013编译boost...
  • 版权声明:本文为博主原创文章,转载请注明源地址...这几天用VS2015编译boost 1.62很正常,但是在用VS2013编译boost1.62的时候,却报错了: \boost/type_traits/common_types.h(42) : fatal error C1001: 编译发生...
  • 编译原理】编译原理简单介绍

    万次阅读 多人点赞 2017-05-07 13:27:20
    什么编译程序 翻译程序 编译程序 翻译和编译的区别 编译的过程 词法分析 语法分析 语义分析和中间代码的产生 优化 目标代码生成 编译程序的结构 编译程序总框 表格与表格的管理 出错处理 遍 编译的前端与后端 编译...
  • 什么编译原理被称为龙书?

    千次阅读 多人点赞 2020-07-17 08:32:21
    什么这本书叫做 龙书(Dragon book)? 这本书很有意思,它的书名是 《Compilers: Principles, Techniques, and Tools》,也就是编译器的原则、技术和工具。但它却画出了一个恐龙和骑士,恐龙身上写的是 Complexity...
  • IDEA13 编译时候出现问题:代码不提示错误,编译时出错解决办法今天遇到了奇葩事儿,有人讲java代码写到了配置文件存放的文件夹,最终导致IDEA开发工具中代码不提示错误,编译时提示找不到类和方法,找了很久才发现...
  • warning C4603: “SQ”: 未定义宏或在预编译头使用后定义发生改变 一类的异常。 解决方法:调整以下代码前面的顺序 #define SQ(y) ((y)*(y)) //定义带参数的宏 #include "stdafx.h" #include&lt;...
  • 我遇到的是第三种情况:Maven 错误找不到符号问题,通常有三种原因: 之前发布项目,一直不知道为什么我发布的项目里缺少core,core-utils的jar包,在这篇文章里找到了答案,聚合项目每次maven clean之后,要将依赖的...
  • Opencascad 7.0编译问题

    千次阅读 2016-09-01 14:37:06
    opencascade编译问题
  • 1.由于博创杯开发板的不可更改性,所以导致的结果是什么呢?那就是无法动态连接,还有一个需要注意的是,板子上面上面用的是4.4.1的gcc,一旦你Ubuntu使用的开发板高于它,都有可能导致运行程序失败,这就是程序失败...
  • 编译概述与引论

    千次阅读 2016-07-20 20:02:08
    什么编译程序通常,我们所说的翻译程序是指这样的一个程序,它能够把某一种语言程序(称为源语言程序)转换成另一种语言程序(称为目标语言程序),而后者与前者在逻辑上是等价的。关于编译原理博文更多讨论与交流...
  • 什么要学编译原理

    千次阅读 2018-06-23 11:03:11
    大学课程为什么要开设编译原理呢?这门课程关注的是编译器方面的产生原理和技术问题,似乎和计算机的基础领域不沾边,可是编译原理却一直作为大学本科的必修课程,同时也成为了研究生入学考试的必考内容。编译原理及...
  • 然后在一个模块里调用这些API, 编译出错: ERROR:· func_name [module_name.ko] undefined! 但是在其它编译进内核的文件里调用确没有问题。原来是忘记在新添加的代码文件里包含了一个头文件: #include . ...
  • 一般这种情况下是你的exe需求的dll文件或者其它的依赖你没有放到Release的文件夹下导致的
  • 深入浅出iOS编译

    万次阅读 多人点赞 2018-12-23 20:59:26
    两年前曾经写过一篇关于编译的文章《iOS编译过程的原理和应用》,这篇文章介绍了iOS编译相关基础知识和简单应用,但也很有多问题都没有解释清楚: Clang和LLVM究竟是什么 源文件到机器码的细节 Linker做了哪些工作 ...
  • 正常默认编译下,调用成功,没有发生任何错误,调用成功。 但O2优化条件下,却发生了Segmentation fault错误。 这一般表明代码存在内存错误访问的情况。 二、检测代码发生Segmentation fault的位置: 两种方式 第...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 483,754
精华内容 193,501
关键字:

编译是什么时候发生的