总是感觉java是解释性语言,转载下一篇感觉写的容易理解的文章

转自 http://www.cnblogs.com/rush/p/3155665.html

1.1.1 摘要

我们知道计算机不能直接理解高级语言,它只能理解机器语言,所以我们必须要把高级语言翻译成机器语言,这样计算机才能执行高级语言编写的程序,在接下来的博文中,我们将介绍非托管和托管语音的编译过程。


1.1.2正文


非托管环境的编译过程(C/C++)

纯C/C++的程序通常运行在一个非托管环境中,类是由头文件(.h)和实现文件(.cpp)组成,每个类形成了一个单独的编译单元,当我们编译程序时,几个基本组件会把我们的源代码翻译成二进制代码,接下来我们通过以下图片说明非托管环境的编译过程:

GCC_CompilationProcess

图1 C/C++编译过程

总体来说,C/C++源代码要经过:预处理、编译、汇编和链接,四步才能变成相应平台下的可执行文件。

如果用gcc编译,只需要一个命令就可以生成可执行文件hw:

gcc -o hw.exe  hw.c

接下来我们按照编译顺序看看编译器每一步都做了什么:

cpp hw.c -o hw.i  // 预处理 gcc -E hello.c -o hello.i

cc1 hw.i-o hw.s    // 编译gcc -S hello.i -o hello.s

as hw.s -o hw.o     // 汇编 gcc -c hello.s -o hello.o

ld hw.o -o hw.exe   // 链接gcc hello.o -o hello.exe

第一步预处理,主要处理以下指令:宏定义指令,条件编译指令,头文件包含指令。 预处理所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令,头文件都被展开(递归展开)的文件。

第二步,编译,就是把C/C++代码“翻译”成汇编代码。它不是直接生成二进制代码,而是生成汇编代码(.s),这基本上是所有现代的非结构化语言的共同基础。

第三步,汇编,就是将生成的汇编代码翻译成符合一定格式的目标代码(.o和.obj文件,机器指令),在Linux上一般表现为ELF目标文件。

第四步,链接,将生成的目标文件和系统库文件进行链接,最终生成了可以在特定平台运行的可执行文件。为什么还要链接系统库中的某些目标文件(crt1.o, crti.o等)呢?这些目标文件都是用来初始化或者回收C运行时环境的,比如说堆内存分配上下文环境的初始化等,实际上crt也正是C RunTime的缩写。这也暗示了另外一点:程序并不是从main函数开始执行的,而是从crt中的某个入口开始的,在Linux上此入口是_start。而且默认情况下,ld是将这些系统库文件(本身也是动态库)都是以动态链接方式加入应用程序的,如果要以静态连接的方式进行,需要显示的指定ld命令的参数-static


 

链接器的工作顺序:

   当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义 表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号 表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,就生成一个可执行文件。

   实际链接的时候会更加复杂,目标文件都会把数据,代码分成好几个区,重定向是按区进行,但原理都是一样的。

 

什么是宏?

在C/C++中,宏是预处理指令,它有多种应用技术:包括预定义、创建关键字和条件编 译等等。在一般情况下,这些技术在C++中使用被认为是不好的做法,主要原因是有可能滥用C++提供的语法变化功能,甚至有可能在不知情情况下创建了非标 准的语言,宏不遵循一般的源代码编译规则,由于它通过预处理来处理,而不是编译器。


托管环境的编译过程(C#/Java)

在托管环境中,编译的过程略有不同,我们熟知的托管语言有C#和Java,接下来,我们将以C#和Java为例介绍在托管环境中的编译过程。

当我们在喜爱的IDE中编写代码时,第一个检测我们代码的就是IDE(词法分析),然后,编译成目标文件和链接到动态/静态库或可执行文件进行再次检查(语法分析),最后一次检查是运行时检查。托管环境的共同特点是:编译器不直接编译成机器码,而是中间代码,在.NET中称为MSIL - Microsoft Intermediate Language,Java是字节码(Bytecode)

在那之后,在运行时JIT(Just In Time)编译器将MSIL翻译成机器码,这意味着我们的代码在真正使用的时候才被解析,这允许在CLR(公共语言运行时)预编译和优化我们的代码,实现程序性能的提高,但增加了程序的启动时间,我们也可以使用Ngen(Native Image Generator)预编译我们的程序,从而缩短程序的启动时间,但没有运行时优化的优点。(JeffWong的 补充Java是先通过编译器编译成Bytecode,然后在运行时通过解释器将Bytecode解释成机器码;C#是先通过编译器将C#代码编译成IL, 然后通过CLR将IL编译成机器代码。所以严格来说Java是一种先编译后解释的语言,而C#是一门纯编译语言,且需要编译两次。)

 Dot_Net_Application_Compilation-707676

图2 C#的编译过程

.Net Framework就是在Win32 core上添加了一个抽象层,它提供的一个好处就是支持多语言、JIT优化、自动内存管理和改进安全性;另外一个完整解决方案是WinRT,但这涉及到另外一个主题了,这里不作详细介绍。

MicrosoftBoxologyDiagram

图3 Windows API

JIT编译的优点和缺点

JIT编译带来了许多好处,最大的一个在我看来是性能的优势,它允许CLR(通用语言 运行时扮演Assembler组件)只执行需要的代码,例如:假设我们有一个非常大的WPF应用程序,它不是立即加载整个程序,而是CLR开始执行时,我 们代码的不同部分将通过一个高效的方法翻译成本地指令,因为它能够检查系统JIT和生成优化的代码,而不是按照一个预定义的模式。不幸的是,有一个缺点就 是启动的过程比较慢,这意味着它不适用于加载时间长的包。

JIT的替代方案使用NGen

如果Visual Studio由JIT创建,那么它的启动我们将需要等待几分钟,相反,如果它是使用Ngen(Native Image Generator)编译,它将创建纯二进制可执行文件,如果只考虑速度的问题,那是绝对是正确的选择。


1.1.3总结

在非托管环境中,我们需要知道编译的过程分成编译和连接两个阶段,编译阶段将源程序 (*.c,*.cpp或*.h)转换成为目标代码(*.o或*.obj文件),至于具体过程就是上面说的C/C++编译过程的前三个阶段;链接阶段是把前 面转成成的目标代码(obj文件)与我们程序里面调用的库函数对应的代码链接起来形成对应的可执行文件(exe文件)。

托管环境中,编译过程可以分为:词法分析、语法分析、中间代码生成、代码优化和目标代 码生成等等过程;无论是.NET还是Java,它们都会生成中间代码(MSIL或Bytecode),然后把优化后的中间代码翻译成目标代码,最后在程序 运行时,JIT将IL翻译成机器码。

无论是托管或非托管语言,它们的编译编译过程是把高级语言翻译成计算机能理解的机器码,由于编译过程涉及的知识面很广(编译的原理和硬件知识),而且本人的能力有限,也只能简单的描述一下这些过程,如果大家希望深入了解编译的原理,我推荐大家看一下《编译原理》。

 

 

另增加内容  java程序编译和运行过程,

转自  http://www.360doc.com/content/14/0218/23/9440338_353675002.shtml

 Java整个编译以及运行的过程相当繁琐,本文通过一个简单的程序来简单的说明整个流程。       

          如下图,Java程序从源文件创建到程序运行要经过两大步骤:1、源文件由编译器编译成字节码(ByteCode)  2、字节码由java虚拟机解释运行。因为java程序既要编译同时也要经过JVM的解释运行,所以说Java被称为半解释语言 ( "semi-interpreted" language)。


图1   java程序编译运行过程

 

        下面通过以下这个java程序,来说明java程序从编译到最后运行的整个流程。代码如下:

 

Java代码  :
  1. //MainApp.java  
  2. public class MainApp {  
  3.     public static void main(String[] args) {  
  4.         Animal animal = new Animal("Puppy");  
  5.         animal.printName();  
  6.     }  
  7. }  
  8. //Animal.java  
  9. public class Animal {  
  10.     public String name;  
  11.     public Animal(String name) {  
  12.         this.name = name;  
  13.     }  
  14.     public void printName() {  
  15.         System.out.println("Animal ["+name+"]");  
  16.     }  
  17. }  

 

        第 一步(编译): 创建完源文件之后,程序会先被编译为.class文件。Java编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后 引用,否则直接引用,这个有点象make。如果java编译器在指定目录下找不到该类所其依赖的类的.class文件或者.java源文件的话,编译器话 报“cant find symbol”的错误。

        编译后的字节码文件格式主要分为两部分:常量池方法字节码。常量池记录的是代码出现过的所有token(类名,成员变量名等等)以及符号引用(方法引用,成员变量引用等等);方法字节码放的是类中各个方法的字节码。下面是MainApp.class通过反汇编的结果,我们可以清楚看到.class文件的结构:
                              
图2  MainApp类常量池  

图3  MainApp类方法字节码
          第 二步(运行):java类运行的过程大概可分为两个过程:1、类的加载  2、类的执行。需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有 的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。
        下面是程序运行的详细步骤:
  1. 在 编译好java程序得到MainApp.class文件后,在命令行上敲java AppMain。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将 MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。
  2. 然后JVM找到AppMain的主函数入口,开始执行main函数。
  3. main 函数的第一条命令是Animal  animal = new Animal("Puppy");就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal 类,把Animal类的类型信息放到方法区中。
  4. 加 载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层 实现)的引用。
  5. 当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。
  6. 开始运行printName()函数。

 图4   java程序运行过程
特别说明:java类中所有public和protected的实例方法都采用动态绑定机制,所有私有方法、静态方法、构造器及初始化方 法<clinit>都是采用静态绑定机制。而使用动态绑定机制的时候会用到方法表,静态绑定时并不会用到。本文只是讲述java程序运行的大 概过程,所以并没有细加区分。本文的所述的流程非常粗糙,想深入了解的读者请查阅其他资料。存在谬误的地方,请多指正。
 
posted on 2016-06-12 15:30  池的巧克力  阅读(1448)  评论(0编辑  收藏  举报