浅析JVM(一)—— 从源文件到JVM类加载的过程

本文主要从整体上介绍JAVA从源文件JVM解释的过程、类加载机制及JVM的运行时数据区

什么是JVM

JVM:指JAVA虚拟机,用来解释并执行编译后的.class文件。

每运行一个JAVA进程,都会启动一个JVM。不同的操作系统有不同的JVM,JVM是实现JAVA“Write onece, run anywhere”的核心。

从源文件到JVM解释的过程

 如图所示,从JAVA源文件到JVM运行的过程,主要是JAVA源码通过编译器编译后生成.class文件,JVM通过类加载机制对.class文件进行解释、执行。

编译过程

 

 如图所示,编译器的编译过程:编译器找到并加载源文件 -> 通过通过词法分析器生成tokens流 -> 通过语法分析器,生成语法树 -> 语义分析器,优化语法树 -> 字节符生成器,生成字节码,即目标文件.class。

1、 词法分析器:将源代码一个字节一个字节的读进来,找到关键词,识别出哪些是合法,哪些是不合法,最终得到一些规范化的token流。类似将各种类型的词、标点找出来的过程。

2、 语法分析器:对tokens进行语法分析,检查关键词组合在一起是否符合JAVA语法规范,最终形成一个符合JAVA语法的抽象语法树。

3、 语义分析器:主要是将一些难懂、复杂的语法转化为更简单的语法。个人理解是将语法分析器生成的语法树再进一步地进行优化。

4、 字节符生成器:将上面获得的语法树转化成字节码,成生目标文件(.class文件)。

JAVA编译器编译后生成的文件不是机器码,而是字节码,无法直接在机器上运行,而是使用JVM将.class文件加载至内存后再进行解释、运行。

类加载机制

1、 什么是类加载机制

类加载机制主要用来分析、执行.class文件。

类的加载就是将.class文件中的数据读入内存,将静态存储结构存放在方法区,之后生成一个对应的Class对象存放在堆中,作为方法区的数据的访问入口。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并对外提供了访问方法区内的数据结构的接口。

 

2、 类加载机制的特点

类加载器不需要等到某个类被使用时再加载它,而是预计类要被使用时就加载它。加载过程中如果.class文件缺失或出错,会在首次使用时报错,如果该类没有被使用,将不会报错。

类加载过程中,全限定路径名相同的类,只会加载一次。这个类的加载过程使用的是双亲委派机制。

3、 双亲委派机制的运行原理

 

JVM遇到一个未加载过的类时,先去找父类是否引用了该类,如果是则再在父类的父类上找其是否被引用,直到找到引用该类的最项层类,在最上层的这个类加载该引用类,而下面的子类不再加载该引用类,这种加载类的方式即为双亲委派机制

注:tomcat加载类的过程是与JVM相反的过程(打破双亲委派机制),即在最初遇到一个未加载过的类时即加载,如果父类有引用相同的类,便不再加载。这样可以减少类在加载时的消耗。

关于双亲委派机制的详细说明可参考:https://www.jianshu.com/p/1e4011617650

4、 类加载机制的过程

 

类加载的过程经过装载(Loading) -> 链接(Linking) -> 初始化(Initializing),每个过程的经过大致如下:

1) 加载

通过类的全限定路径名获取到对应的二进制数据。

将二进制数据中的静态数据结构存放到方法区中。

在堆中生成一个对应类的Class对象,作为访问方法区中的数据入口。

2) 链接

链接主要有三大过程:验证、准备和解析。

验证:确保加载的类的正确性。

准备:为类的静态变量分配内存,将其值初始化为默认值。

解析:把类中的符号引用转化为直接引用。

=> 注:在准备阶段,静态变量并不会被初始化为我们指定的值,而是被暂时赋为默认值

=> 符号引用暂时理解成没有实际含义的、不能代表真正的物理存储的内容,其只是JVM的规范。而直接引用则理解为能够代表真实含义的内容。

3) 初始化:将静态就是赋值为正确(我们指定的)值。

JVM的运行时数据区

当类加载机制获取到.class文件的二进制数据后,将数据存放入内存,而JVM会将该内存分为各个不同的功能模块来存放或运行二进制中的内容。这个内存区域为JVM的运行时数据区。

JVM的运行时数据区大致分为五大模块:方法区、堆、虚拟机栈、本地方法栈和程序计数器。

 

1、 方法区:主要存放类信息(如类的创建时间、作者、元数据等)、静态变量、常量等。

2、 堆:主要存储对象

=>  如果方法区或堆中出现内存不够用的情况,程序将抛出OOM异常(OutOfMemoryError)

3、 虚拟机栈(JVM中最复杂的一块区域)

 

 主要用来执行JAVA程序中的线程。每执行一个线程将创建一个虚拟机栈,每执行一个方法,都会创建一个栈帧。栈帧是虚拟机栈中的元素。

栈帧遵循先进后出的原则。

栈帧中包含:局部变量表、操作数栈、动态链接、返回地址。

1)      局部变量表:存放方法中的参数和局部变量。

2)      操作数栈:执行该方法。

3)      动态链接:将运行时才能确定的方法,在运行时动态地将符号引用转化为直接引用。

4)      返回地址:返回一些栈帧信息来帮助恢复上层方法的执行状态。

4、 本地方法栈:与虚拟机栈差相似,主要用来执行Native方法,如一些C/C++之类的第三方动态库(因为虚拟机栈只执行JAVA方法,那么其他语言提供的第三方动态库中的方法就通过本地方法栈来执行)。

5、 程序计数器:记录非Native方法的线程的执行位置。比如多线程抢占资源时,程序计数器会记录某一线程的执行位置,以便后面如果该线程抢到资源后继续向下执行。

参考

https://blog.csdn.net/fuzhongmin05/article/details/54880257

https://www.cnblogs.com/ityouknow/p/5603287.html

https://www.jianshu.com/p/1e4011617650

原文地址:https://www.cnblogs.com/yinghuanblog/p/12907979.html