Java编译程序和运行过程详解

java整个编译以及运行的过程相当繁琐,我就举一个简单的例子说明:
编译原理简单过程:词法分析 --> 语法分析 --> 语义分析和中间代码生成 --> 优化 --> 目标代码生成
Java程序从源文件创建到程序运行要经过两大步骤:
1、Java文件会由编译器编译成class文件(字节码文件),会经过编译原理简单过程的前三步;
2、字节码由java虚拟机解释运行,解释执行即为目标代码生成并执行。因为java程序既要编译的同时也要经过JVM的解释运行,所以说Java被称为半解释语言!
( "semi-interpreted" language)
 
public class Main {
 
    public static void main(String[] args) {
        Animal animal = new Animal("Tom");
        animal.printName();
    }
 
}
 
class Animal{
    private String name;
 
    public Animal(String name) {
        super();
        this.name = name;
    }
     
    public void printName(){
        System.out.println("Animal = " + this.name);
    }
}
 
第一步(编译):创建完源文件之后,程序先要被JVM中的java编译器进行编译为.class文件。java编译一个类时,若这个类所依赖的类还没有被编译,编译器会自动的先编译这个所依赖的类,然后引用;若java编译器在指定的目录下找不到该类所依赖的类的 .class文件或者 .java源文件,就会报"Can't found sysbol"的异常错误。
 
编译后的字节码文件格式主要分为两部分:常量池和方法字节码。
  常量池记录的是代码出现过的字面量(文本字符串、八种基本类型的值、被声明为final的常量等)以及符号引用(类和方法的全限定名、字段的名称和描述符、方法的名称和描述符);
  方法字节码中放的是各个方法的字节码(依赖操作数栈和局部变量表,由JVM解释执行)
 
第二步(运行):java类运行的过程大概分为两个步骤:
(1)类的加载
  加载 --> 验证 --> 准备 --> 解析 --> 初始化(其中验证、准备、解析统称为类的连接);(参考《深入了解Java虚拟机》)
  加载:通过一个类的全限定名来获取定义此类的二进制字节流(Class文件);将这个二进制字节流所代表的静态存储结果转化为方法区的运行时数据结构;在内存中生成一个java.lang.Class对象,注意:存放在方法区;
 
  验证:验证目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;使用纯粹的Java代码无法做到诸如访问数组边界意外的数据、将一个对象转型为它未实现的类型、跳转到不存在的代码之类的事情,如果这样做了,编译器将拒绝编译;
 
  准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。首先这时候进行内存分配的仅包括类变量(static修饰的变量),而不是实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;
 
public static int value = 123; 
 
  变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,在类初始化的时候才会将value的值赋为123;
 
  解析:解析阶段是虚拟机将class常量池内的符号引用替换为直接引用的过程;
     符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可;
       直接引用:是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。有了直接引用,那引用的目标必定已经在内存中存在。
  初始化:类初始化阶段是类加载过程的最后一步;在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源:初始化阶段是执行类构造器<clinit>( )方法的过程。
  <clinit>( )方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static { }块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。 
 
(2)类的执行
  需要说明的一点的是:JVM主要在程序第一次运行时主动使用类的时候,才会立即去加载,加载完毕就会生成一个java.lang.Class对象,并且存放在方法区。换言之,JVM并不是在运行时就会把所有使用到的类都加载到内存中,而是用到,不得不加载的时候,才加载进来,而且只加载一次,初始化类构造器<clinit>()方法也只执行一次,所以static{} 块,类变量赋值语句也就只执行一次,只生成一个java.lang.Class对象!
  由Java虚拟机的执行引擎来解释执行Java字节码,过程:输入字节码文件,字节码解析,输出执行完的结果!(不再赘述,请自行参考《深入了解Java虚拟机》)
 
重点理解:根据上面的程序和概念解释,详解该程序运行的详细步骤
(1)在类路径下找到编译好的 java 程序中得到 Test.class 字节码文件后,在命令行上敲 java Test,系统就会启动一个 JVM 进程,JVM进程从classpath路径下找到一个名为Test.class的二进制文件,将Test.class文件中的 类信息加载到运行时数据区的方法区(JDK 8 方法区存在 堆区) 中,这一过程叫做类的加载。(只有类信息在方法区中,才能创建对象,使用类中的成员变量);
 
(2)JVM 找到main方法的主函数入口, 持有一个指向当前类(Test)常量池的指针,而常量池中的第一项发现是一个对Animal对象的符号引用,并且main方法中第一条指令是Animal animal = new Animal("Tom"),就是让JVM创建一个Animal对象,但是方法区中还没有Animal类的类信息,于是JVM就要马上的加载Animal类,将Animal类信息放入到方法区中,于是JVM 以一个直接指向方法区 Animal类的指针(直接引用)替换了常量池中第一项的符号引用;
 
(3)加载完Animal类的信息以后,JVM虚拟机就会在堆内存中为一个Animal类实例分配内存,然后调用其构造函数初始化Animal实例,这个实例持有指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。(animal指向了Animal对象的引用会自动的放在栈中,字符串常量"Tom"会自动的放在方法区的运行时常量池中,对象会自动的放入堆区);
 
(4)当使用 animal.pringName()的时候,JVM根据栈中animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息方法表,获得pringName()函数的字节码地址,然后Java虚拟机执行引擎依赖局部变量表,操作数栈进行字节码解释执行,返回结果!
 
大家可能对Java执行引擎,结合局部变量表和操作数栈执行字节码的理解不是很透彻,下来我简单介绍一下字节码的执行过程:
 
public int calc(){
     int a = 100;
     int b = 200;
     int c = 300;
     return (a + b) * c;     
}
 
字节码指令展示:
  public int calc();
  Code:
  Stack=2, Locals=4, Args_size=1 //操作栈深度为2和4个Slot局部变量表
  0:bipush 100   //将100压入操作数栈
  2:istore_1   //将栈顶100数值存放到局变量Slot,index=1中
  3:sipush 200  //将200压入操作数栈
  6:istore_2      //将栈顶200数值存放到局部变量Slot,index=2中
  7:sipush 300  //将300压入操作数栈
  10:istore_3     //将栈顶200数值存放到局部变量Slot,index=3中
  11:iload_1       //将index=1的局部变量表数值压入操作数栈(100)
  12:iload_2      //将index=2的局部变量表数值压入操作数栈(200)
  13:iadd          //取栈顶两个数值相加,结果压入操作数栈(300)
  14:iload_3      //将index=3的局部变量表数值压入操作数栈(300)
  15:imul          //取栈顶两个数值相乘,结果压入操作数栈(90000)
  16:ireturn      //取栈顶数值返回调用者结果
 
局部变量表 index = 0存储当前对象本身 this 
 
参考资料:

posted @ 2018-11-22 11:57  菜鸟的奋斗之路  阅读(998)  评论(0编辑  收藏  举报