我眼中的JVM(一)
关于JVM的资料网上有很多,讲解也十分清晰。但是对于我来说没有一种酣畅淋漓的感觉,总感觉差一些东西让人很难受,都是介绍各种知识点,没有形成一个链路线,所以开始按自己的思维整理下,将学习的内容从一个开始点然后一直走到结尾组成一条完整的链路。
开始分析JVM的学习过程,我的思路是什么呢?
1、JVM是什么?
我只知道全称为(java virtual machine) java虚拟机。附上维基上的解释:A Java virtual machine (JVM) is a virtual machine that enables a computer to run Java programs as well as programs written in other languages and compiled to Java bytecode. 在这里说个题外话,本人英语巨烂,现在估计四级水平都到不了,上面的一段话只认识大约70%的单词,组成的句子也都不太懂。但是还是不建议直接看中文,只有看英文才会有进步。
2、JVM是做什么的?
看了维基的解释,说实话我还是不明白它到底是什么,或者说什么用的?开始从计算机说起,我们知道计算机是由不同的硬件组装起来的,控制硬件运行的是各种硬件的驱动,而操作系统可以调用驱动接口来管理硬件运行,而操作系统会提供一层API接口,程序员可以面向这一层的接口编程,来实现对计算机的控制。
但是现在有很多不同的操作系统,例如window/Linux/Unix等,这些操作系统所提供的API也都是不一样的,那么对于程序员来说就很不友好,总不能针对每种操作系统都写一份代码吧?而JVM的作用就是解决这些问题,JVM负责将Class文件转换对应操作系统所能识别的机器码。只要是符合Class文件应有结构的Class文件都可以在JVM中运行,而并不会关心是什么语言编译成的Class文件。
3、JVM中怎么做的?
按目前的理解,JVM就是将Class文件解析成操作系统识别的机器码。那么这个过程是如何处理的?我先进行主线剖析,首先最先做的工作是加载Class文件,当我们的系统启动服务的时候,类加载器会查找并加载Class文件,然后准备对应资源,进行初始化,之后就可以提供服务了。至于如何将服务转为操作系统识别的机器码在这里就不做过多的解释。其实分析到这里,就知道了这就是一个类的加载过程,那么开始详细研究这个过程。
一、加载
首先,我们需要了解怎么开始加载?JVM中提供了一个工具来查找Class,这个就是类加载器。而JVM中提供了三种类加载器,为什么要三个类加载器呢?或者三个够用吗?可以再加吗?先带着这些疑问来看一下提供的三种类加载器。
1.启动类加载器(Bootstrap Class Loader)
该类加载器由C/C++代码实现的加载器,它负责将 <Java_Runtime_Home>/lib下面的核心类库指定的JAR包加载到内存中。
2.扩展类加载器(Extension Class Loader )
它负责加载<Java_Runtime_Home>\lib\ext目录或系统变量java.ext.dirs所指定的目录中的文件加载到内存中。
3.应用类加载器(Application Class Loader)
它负责将系统类路径ClassPath所指向的目录下的类库加载到内存中。
其实看名字大概知道JVM的用意,它将加载资源分为三个级别,启动类加载器负责加载支撑系统正常启动的资源文件,扩展类加载器负责加载扩展类功能的资源文件,而应用类加载器负责我们的项目运行所需要的项目资源文件。而在这三类加载器中,启动加载器开发者是无法通过代码访问到,扩展类和应用类加载器开发者可以直接使用。
那么是不是有了这三个加载器就足够了吗?答案当然是否定的。如果我要对上述路径之外的文件进行加载呢,或者我对文件执行了加密编译,或者我想加载网络上的某个文件,就没有办法了。所以JVM提供了自定义加载器,开发者需要继承ClassLoader,并覆盖findClass方法来进行实现。
另外JVM在对类加载器的层次关系中加入了一种机制,就是双亲委派机制。具体层次为启动类加载器-->扩展类加载器-->应用类加载器-->自定义加载器。它规定了某个特定的类加载器在接到加载类的请求时,当未发现此类已经被加载了,那么将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才会自己加载。
好吧,了解之后问题又来了,为什么要这种机制? 其实也不难想到,如果你现在使用应用加载器加载一个java.lang.Object类会怎么样?应用加载器会加载出一份Object字节码,那么这个与启动加载器已经加载过的Object字节码比较,是不是内存中就出现了二份Object字节码?那系统应该用哪个?
二、验证
了解类加载器后,我们继续了解类加载的过程。由类加载器找到Class资源文件进行加载,然后做什么?直接分配资源?当然不是,就跟我们平时写代码一样,你接收到参数之后做什么?验证!!是否合法!格式是否正确!JVM中自然也是这样,先执行验证过程,判断Class文件中的字节流是否符合要求,是否合法。而验证过程包括四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
三、准备
当加载的Class文件符合要求时才会进入的准备阶段,这个阶段是正式为类变量分配内存并设置类变量初始值的阶段。这个阶段会出现容易混淆的问题,首先什么是类变量?类变量就是被Static修饰的变量,那么这个阶段为类变量设置的初始值是什么?举个例子:
public static int value = 123;
那么为value设置的初始值时什么? 其实是0。为什么?因为这个时候尚未开始执行任何的Java方法,而为value赋值123的操作在这个时候还未执行。那么是不是所有的变量赋值都是这样?也不是,比如:
public static final value =123;
在这种情况下,会直接为value设置初始值123。这又为什么呢? 先了解下 static、final、static final 三种不同修饰的字段赋值的区别:
- static 在准备阶段被初始化,但是这个阶段只会赋值一个默认的值(0或者null而并非定义变量设置的值)初始化阶段在类构造器中才会赋值为变量定义的值。
- final 修饰的字段在运行时被初始化,可以直接赋值,也可以在实例构造器中赋值,赋值后不可修改。
- static final修饰的字段在javac编译时生成constantValue属性,在类加载的准备阶段直接把constantValue的值赋给该字段。 可以理解为在编译期即把结果放入了常量池中。
当同时被static 和final 修饰后的字段,在编译时就会将值放入到 Class中的constantValue属性中,而在准备阶段会直接将constantValue的值赋给该字段。
四、解析
解析阶段的定义是将常量池中的符号引用替换为直接引用的过程。说实话,我不是太理解。直接看符号引用和直接引用是什么?如果当前的类中存在对其他类的调用或对其他类字段的引用,那么在编译后的Class文件中它们是以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。再更形象的比如说org.until.Person中引用了org.until.Org类,在编译的时候是不知道org.until.Org地址的,那就是用一种符号来代替,类似于CONSTANT_Class_info的常量这种方式,那么在这个阶段就会将这些符号替换为直接引用地址。
五、初始化
这个阶段是类加载过程的最后一步,这个阶段才开始执行类中定义的Java程序代码,将会按照代码中的逻辑去初始化类变量。要注意的是如果当前类存在超类,将先初始化超类;如果类存在类初始方法,将执行此方法。
至此,类的加载过程结束。那么现在是不是就表示这个类进入到使用状态了?当然不是!!为什么?
下篇文章,继续学习!!!