JVM学习系列(1)——Java代码是如何执行的

Java程序的通用执行过程

学习Java编程语言学习时的最初的示例程序一般都是HelloWorld。我们来看一个HelloWorld.java的示例:

public class HelloWorld{ 
    
   public static void main(String args[])
   { 
      System.out.println("Hello World!"); 
   }

} 

在执行代码前,我们需要对其进行编译,执行命令为:

javac HelloWorld.java

就能得到一个名为HelloWorld.class的文件。
接下来,我们运行java命令就可以了执行它。

java HelloWorld

就可以了看到执行结果了:

Hello World!

以上,就是一个通用的Java程序执行过程。想要了解更多细节,就需要学习下JVM的一些知识。下面,我们来看下一个标准的JVM,应该是什么样子的。

JVM标准结构

首先我们来看一个JVM的标准结构。


JVM学习系列(1)——Java代码是如何执行的_第1张图片
JVM标准结构.jpg

学习JVM需要了解

  1. 如何将Java代码编译、装载、执行class文件;
  2. 如何分配和回收内存;
  3. 线程间资源同步和线程间交互的机制。
    本文主要介绍第1点,Java代码编译、装载、执行。这个过程,简单来讲,就是Class文件通过类加载器装入运行时数据区,然后通过执行引擎进行执行。下面我们从Java源码的编译入手,来看Class文件是怎么得到的。

JAVA源码的编译

我们编写的Java代码,都是.java的文件。在执行前,需要执行javac命令对其进行编译。编译主要有三个步骤:

  1. 分析和输入到符号表
  • 分析包括词法分析和语法分析。词法分析是把代码字符串转为token序列;语法分析是根据语法由token序列生成抽象语法树。
  • 输入过程为将符号输入到符号表,通常包括确定类的超类型和接口。根据需要添加默认构造器、将类中出现的符号输入类自身的符号表中。
  1. 注解处理
  • 注解处理主要是处理用户自定义的注解。一个注解处理器(javac工具中包含),以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。关于注解,我们需要一个单独的章节来学习,这里不再赘述。
  1. 语义分析和生成class文件
  • 语义分析主要是基于抽象语法树进行一系列语义分析,包括将语法树中的名字、表达式等元素与变量、方法、类型等联系到一起;检查变量使用前是否已经声明;推导泛型方法的类型参数;检查类型匹配性;消除无用代码。
  • 生成class的步骤:初始化;将抽象语法树生成字节码,进行少量的代码替换(如String相加转变为StringBuilder操作);最后从符号表生成class文件。
  1. Class文件的构成
  • 结构信息
    结构信息包括class文件格式版本号及各部分数量与大小的信息
  • 元数据
    可以认为元数据对应的是Java源码中“声明”与“常量”信息,主要有:类继承的超类,实现的接口的声明信息,域(Field)与方法声明信息和常量池。
  • 方法信息
    可以认为方法信息对应的就是Java源码中“语句”与“表达式”对应的信息,主要有:字节码、异常处理表、求值栈与局部变量区大小、求值栈的类型记录、调试用符号信息。

类文件的加载

类文件的加载有三个步骤:装载、连接、初始化。其中,连接的步骤有验证、准备和解析三个动作。


JVM学习系列(1)——Java代码是如何执行的_第2张图片
类文件加载.jpg
  • 类加载指的是class文件加载到jvm,并形成Class对象的机制,之后应用就可以对Class对象进行实例化并调用;
  • 装载和链接完成后,即将二进制的字节码转换为Class对象,初始化过程不是加载类时必须触发的,但最迟必须在初次主动使用对象前执行,其所做动作为给静态变量赋值,执行()等。

类装载

  • Jvm通过类的全限定名(com.jd.ofc.coi.bean.OwOrder)及类加载器(ClassLoaderA实例)完成类的加载。同样也用这两个元素来标识一个被加载了的类。
  • 类名命名方式:
    –对于接口和非数组类,名称即为类名,此类型的类由所在的ClassLoader负责加载。
    –数组型的类,名称为”[”+(基本类型或L+引用类型名;),数组类型中的元素类型由所在的ClassLoader负责加载,但数组类则由jvm直接创造

链接(Link)

  • 链接过程负责对二进制字节码格式进行校验、初始化装载类中的静态变量及解析类中调用的接口、类。
    – 二进制字节码校验遵循Java Class File Format规范,格式不符抛出VerifyError;校验过程中如果要引用到其他类或接口也会加载;加载失败会抛出NoClassDefFoundError。
    – 完成校验后,JVM初始化类的静态变量,赋值。
    – 最后对类中的所有属性、方法验证,以确保要调用的属性、方法存在,并具备相应的权限。这个阶段失败会造成NoSuchMethodError、NoSuchFieldError等错误信息。

初始化(Initialize)

  • 初始化过程即执行类中的静态初始化代码、构造器代码即静态属性的初始化,以下四种情况的初始化过程会被触发执行:
    – 调用了new
    – 反射调用了类中的方法
    – 子类调用了初始化
    – JVM启动过程中指定的初始化类

类加载器

JVM学习系列(1)——Java代码是如何执行的_第3张图片
类加载器.jpg

Sun的jdk采用C++实现BootstrapClassloader,此类非ClassLoader的子类,代码中无法拿到该对象,其parent为null。
User-Defined ClassLoader是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可加载非Classpath的jar及目录、还可以加载前对class文件做一些操作,如解密。

类加载的异常

  • CLassNotFoundException
    当前ClassLoader中加载类时未找到类文件。对位于System ClassLoader中的类,如果不在Classpath中,就会报这个异常。
  • NoClassDefFoundError
    原因是加载的类中引用到的另外的类不存在。
  • ClassCastException
    类型转换错误,产生原因众多。

类执行

类执行方式主要有三种,字节码解释执行,编译执行,反射执行。
在开始介绍之前,我们先了解下编译执行和解释执行分别是什么。

编译型语言

是一次性编译成机器码,脱离开发环境独立运行,所以运行效率较高,但是由于编译成的是特定平台上机器码,所以可移植性差。编译型语言的典型代表有C、C++、FORTRAN、Pascal等。

解释型语言

是专门的解释器对源程序逐行解释成特定平台的机器码并执行的语言。解释型语言通常不会进行整体性的编译和链接处理,解释语言相当于把编译型语言的编译和解释过程混合到了一起同时完成。于是,每次执行解释型语言的程序都要进行一次编译,因此解释型语言的程序运行效率通畅较低,而且不能脱离解释器独立运行。但解释型语言跨平台容易,只需要提供特定的平台解释器即可。解释型语言可以方便的进行程序的移植,但是以牺牲程序的执行效率为代价的。解释型语言的典型代表有Ruby、Python等。

Java语言

Java语言的特殊性在于用Java编写的程序先要经过编译,但不会生成特定平台的机器码,而是生成一种平台无关的字节码,即.class文件。这种字节码并不是可执行的,必须通过Java编译器来编译执行。因此可以认为Java即是编译型语言也是解释型语言。
Java语言里解释执行字节码文件的是Java虚拟机JVM。JVM是可执行Java字节码文件的虚拟计算机。所有平台上的JVM想编译器提供相同相同的编程接口,而编译器只需要面向虚拟机,生成虚拟机能理解的代码,由虚拟机来解释执行。在一些虚拟机的实现中,还会将虚拟机代码转换成特定系统的机器码执行,从而提高执行效率。

你可能感兴趣的