Java编译和运行

整理一下Java编译和运行的大致过程。先上一张Java平台逻辑结构图:


1. 概述


如图, 从上至下分别是编译型语言,解释型语言,Java的执行模型。Java程序从源文件创建到程序运行要经过两大步骤:1、源文件由编译器编译成字节码(ByteCode) 2、字节码由java虚拟机解释运行。关于Javay属于编译型语言还是解释性语言,可以参照Java 是编译型语言还是解释型语言


2. 编译

Java代码编译是由Java源码编译器来完成,流程图如下所示:

2.1 编译机制

Java 源码编译由以下三个过程组成:

  • 分析和输入到符号表
  • 注解处理
  • 语义分析和生成class文件

最后生成的class文件由以下部分组成:

  • 结构信息。包括class文件格式版本号及各部分的数量与大小的信息
  • 元数据。对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
  • 方法信息。对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息

2.2 字节码

Class文件是8位字节流,按字节对齐。之所以称为字节码,是因为每条指令都只占据一个字节,所有的操作码和操作数都是按字节对齐的。
Java虚拟机规范中规定,Class文件格式采用一种类似C语言结构体的伪结构来存储,它只有两种数据类型

  • 无符号数(基本数据类型)
    主要用于描述数字、索引引用、数量值、或UTF-8编码构成的字符串;
    u1 – 1个字节
    u2 – 2个字节
    u4 – 4个字节
    u8 – 8个字节
  • 表(符合数据类型)
    用于描述有层次关系的符合结构的数据;习惯性以“_info”结尾

更详细介绍可以参照;
Java字节码小结
Java代码到字节码
Java字节码.class文件案例分析


3. 运行

Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:

3.1 什么是JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
JVM规范参照:Java Virtual Machine Specification
中文版:Java虚拟机规范(JavaSE7)

3.2 JRE/JDK/JVM的关系

  • JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
  • JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
  • JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。

3.3 JVM架构

3.3.1 JVM体系结构

JVM = 类加载器 classloader + 执行引擎 execution engine + 运行时数据区域 runtime data area

  • 类装载器(ClassLoader)(用来装载.class文件)
  • 执行引擎(执行字节码,或者执行本地方法)
  • 运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)
    JVM体系结构:

3.3.2 classloader

classloader作用是装载.class文件。
classloader 有两种装载class的方式(时机):

  1. 隐式:运行过程中,碰到new方式生成对象时,隐式调用classLoader到JVM
  2. 显式:通过class.forname()动态加载
    类的层次关系和加载顺序可以由下图来描述:

1)Bootstrap ClassLoader
负责加载 $JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
2)Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
3)App ClassLoader
负责记载classpath中指定的jar包及目录中class
4)Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

3.3.3 执行引擎

执行字节码,或者执行本地方法。类被加载到虚拟机内存中开始,到卸载出内存为主,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个阶段称为连接(Linking)。

3.3.4 运行时数据区

JVM运行时数据区:

  1. PC程序计数器:一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器, NAMELY存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。Java 的多线程机制离不开程序计数器,每个线程都有一个自己的PC,以便完成不同线程上下文环境的切换。

  2. java虚拟机栈:与PC 一样,java虚拟机栈也是线程私有的。每一个JVM线程都有自己的java虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  3. 堆(Heap):它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。

    1)堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
    2)Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
    3)TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
    4)所有新创建的Object 都将会存储在新生代Yong Generation中。如果Young Generation的数据在一次或多次GC后存活下来,那么将被转移到OldGeneration。新的Object总是创建在Eden Space。

  4. 方法区域(Method Area)
    1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。
    2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

  5. 运行时常量池(Runtime Constant Pool)
    存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。

  6. 本地方法堆栈(Native Method Stacks)
    JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

3.4 JVM垃圾回收

这里对垃圾回收作简单介绍,后面再详细展开。
GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
1)对新生代的对象的收集称为minor GC;
2)对旧生代的对象的收集称为Full GC;
3)程序中主动调用System.gc()强制执行的GC为Full GC。

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
3)弱引用:在GC时一定会被GC回收
4)虚引用:由于虚引用只是用来得知对象是否被GC


4. 实例

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

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

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

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()函数。


参照资料:
JVM的工作原理,层次结构以及GC工作原理
JVM介绍
Java代码编译和执行过程
JVM学习笔记(二)——Java代码编译和执行的整个过程
JVM Internals