目录
1. 背景
2.整体流程
2.1 一看整体流程
2.1 再看运行流程
3. 类的加载过程
3.1 初探类的加载过程
4. 类加载机制
4.1 类加载器
4.2 双亲委派机制
5. 小结:
学习了这么多年的Java,把自己的理解写成JVM系列,以便于后面的温习,以及帮助更多的java开发人员,同时文章内容也会不定期的修改,以便让这个系列更好,更加的易于理解,提高文章的质量。
开篇先梳理下,我们之前写的第一个Hello World!,当我们运行出来后,在控制台打印的那种激动心情,但是他的背后是如何运行的呢?那么问题来了,我们写的java代码到底是如何运行起来的,中间又经历了什么?带着我这些问题,我们继续往下看。
首先假设我们写好了一个java代码,在IDEA或者Eclipse工具上,会有一个“.java“后缀的代码文件,我们写好后,想要部署到线上的机器上去运行,一般都是把代码通过IDEA工具编译生成".class"后缀文件后,再进行打包,打包成“.jar”后缀的jar包,或者 “.war” 后缀的war包; 最后再通过 tomcat部署 ,或者 java 命令进行运行;是不是这样的一个过程;粗略的看确实是这样一个过程,来看下图,来回忆下这个过程:
上面的过程中,java代码通过idea工具编译后,生成“.class” 后缀的,字节码文件(字节码是能够被虚拟机认识的16进制字节,其中还有一些字节码指令等等的内容),那么,我们编译好的文件是如何运行起来的呢?
这个时候我们就要使用一个“Java -jar”的命令,来运行程序,实际上此时就会启动一个JVM进程,这个JVM就会负责运行这些“.class” 字节码文件,相当于负责运行我们写好的系统。
对于上面的过程,IDEA编译打包代码的过程,我们暂时不在这块过多的说明,我们把重点放到运行这块上来,这块才是核心内容,下面就说下打包后,部署运行的内部是如何运行的?
当我们运行了一个"java -jar" 命令后,就是启动了一个JVM进程,就会运行我们编写的字节码文件对吧,我们按照逆推的方式来理解,既然要执行字节码,我们是不是要先找到字节码文件,那么怎么找呢,找到后怎么读取执行,针对这两个问题,我们引出了两个概念 “类加载器”,“字节码执行引擎”,类加载器很好理解,字面意思,就是把类加载到JVM中,以供后续代码运行使用;
字节码执行引擎,就是JVM会基于自己的字节码引擎来执行类加载器,加载到内存中的类。
为了方便我们理解上面的过程,请看下图的过程。
好了,说过了大概的整体JVM运行流程,我们一起来探索下类加载的过程吧。
先想一个问题:JVM在什么情况下会加载一个类呢?
类加载的过程非常的繁琐复杂,我们在工作中,只要把握住核心的工作原理就可以了,
一个类从加载到使用,一般会经历下面的这个过程:
加载->验证->准备->解析->初始化->使用->卸载
所以,首先我们要搞明白上面的问题,就是JVM在什么情况下会去加载一个类呢?翻译过来就是说,啥时候会从".class"字节码文件中加载这个类到JVM内存中,毕竟,内存是有限的,在过去内存是很贵的,另外加载过程很繁琐,也很消耗资源,我们肯定要减少消耗,想通了这,答案就很简单啦,就是在你的代码中用到这个类的时候去加载。
我们明白了类的加载时机,只有在需要的时机去加载,那么加载后又做了什么处理,下面分别介绍下另外三个概念,验证、准备、解析、初始化
验证阶段:简单来说,这个阶段就是根据Java虚拟机规范,来校验你加载进来的".class"文件中的内容,是否符合指定的规范。这个相信很好理解,假如说,你的".class"文件被人篡改了,里面的字节码压根儿不符合规范,那么JVM是没法去执行这个字节码的,所以把".class"加载到内存里之后,必须先验证一下,校验他必须完全符合JVM规范,后续才能交给JVM来运行。
准备阶段:这个也容易理解,我们写好的类里面,都会有些类变量,就比如下面“Contended” 这个类:
public class Contended {
public static final int CACHE_LINE;
}
假设你有这么一个“Contended类,他的"Contended.class"文件内容刚刚被加载到内存之后,会进行验证,确认这个字节码文件的内容是规范的接着就会进行准备工作。这个准备工作,其实就是给这个"Contended"类分配一定的内存空间,然后给他里面的类变量(也就是static修饰的变量)分配内存空间,来一个默认的初始值,比如上面的例子中,就会给“CACHE_LINE”这个变量分配内存空间,给了‘‘0’’ 这个初始值。
解析阶段:这个阶段干的事儿,实际上是把符号引用替换为直接引用的过程,其实这个部分的内容很复杂,涉及到JVM的底层,这里不做过多的介绍了。
初始化阶段:前面说了在准备阶段时,就会把我们的"Contended"类给分配好内存空间,另外他的一个类变量"CACHE_LINE"也会给一个默认的初始值"0",那么接下来,在初始化阶段,就会正式执行我们的类初始化的代码了。
到底什么是类的初始化代码?我们来继续看下面的代码:
public class Contended {
public static final int CACHE_LINE = Integer.getInteger("Intel.CacheLineSize", 64);
}
我们可以看到,对于“CACHE_LINE” 这个类变量,我们是打算通过“Integer.getInteger("Intel.CacheLineSize", 64);” 这段代码获取一个值,来赋值给他,但是他会在准备阶段就给他赋值么,显然是不会的,在准备阶段只会给他分配一个内存空间,并且赋值“0”给他而已。
那么,这个变量的值是在什么时候给他赋值呢,答案就是“初始化“这个阶段。在这个阶段,就会执行类的初始化代码,比如上面的 Integer.getInteger("Intel.CacheLineSize", 64);代码就会在 这里执行,完成一个配置项的读取,然后赋值给这个类变量"CACHE_LINE"。我们搞明白了类的初始化是什么,就得来看看类的初始化的规则了。
什么时候会初始化一个类?
一般来说有以下一些时机:比如"new Contended("来实例化类的对象了,此时就会触发类的加载到初始化的全过程,把这个类准备好,然后再实例化一个对象出来;或者是包含"main()"方法的主类,必须是立马初始化的。
此外,这里还有一个非常重要的规则,就是如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类。
小结:是在实例化对象的时候,类的实例化的过程也就变成了,类被加载到内存后,通过验证、准备解析以及初始化赋值后,再接着进行实例化对象;遇到有父类的时候,Object类是所有类的父类,也就是要先初始化Object类之后才会初始化这个类。
好了,类的加载过程我们先到这吧,此时又有问题了,既然是这个类是用到时候去加载,那么如果我在这个类中,同时用到了多个引用怎么办,就没有个先后顺序么?别担心,这个JVM 早就想到了,他就是类加载机制,双亲委派机制,类加载器都有那些?双亲委派机制又是什么?我们下面接着来看。
通过上面内容相信大家对整个类加载从触发时机到初始化的过程都明白了,我们接着说下类的加载器概念,类加载器就是加载的工具,类的加载过程必须依靠类加载器来进行实现。
既然类加载器这么重要,我们就先看下类的加载器都有那些:
启动类加载器:Bootstrap ClassLoader,他主要是负责加载我们在机器上安装的Java目录下的核心类的。就是在你java安装目录下 "lib" 目录下的核心类库。
相信大家都知道,如果你要在一个机器上运行自己写好的Java系统,无论是windows笔记本,还是linux服务器,是不是都得装一下JDK?那么在你的Java安装目录下,就有一个"lib"目录,大家可以自己去找找看,这里就有Java最核心的一些类库支撑你的Java系统的运行。所以,一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的"lib"目录中的核心类库。
扩展类加载器:Extension ClassLoader,这个类加载器也是在你Java安装目录下,有一个"lib\ext"目录。这里面有一些类,就是需要使用这个类加载器来加载的,支撑你的系统的运行。
应用程序类加载器:Application ClassLoader,这类加载器就负责去加载"ClassPath"环境变量所指定的路径中的类,其实你就可以理解成是我们自己实现的java代码就可以了。
自定义类加载器:这个类加载器就是说,你也可以自己去实现一些类加载器,去根据你自己的需求加载你的类。
我们看完了类加载器,那么类加载要有个加载的规则吧,那就是双亲委派规则:JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。然后基于这种亲子层级结构,就是双亲委派机制:就是先去找父亲去加载,不行的话再由儿子来加载,这样就能避免多层级的加载器结构重复加载某些类。双亲委派规则可以看下图:
我们在上面说了一堆东西,那具体是怎么个操作呢,我们看下一个例子:
假设你的JVM虚拟机要加载“Contended” 类,此时他作为应用程序类,会去找他的上级也就父亲,告诉他,你先在你的背包里面找找,万一在你包里呢,父类作为扩展类,还有上级,他想想还是交给父亲吧,万一他那边有呢,这样就把事情交给了启动类加载器,启动类上面没有了,他是最高级别,他就开始找,找了半天没有找到,他就告诉儿子,扩展类,我这边没有,你在你那边找找吧,扩展类就开始找,找了半天发现也没有找到,就交给了亲儿子应用程序类,应用程序类说,好吧,我就在我背包里面找吧,结果找到了,就可以加载实例化了。
上面的一个寻找的过程就是双亲委派的一个过程,有人问了,为啥要这样设计呢,不这样设计不行么?这个机制规则不能够打破么?这样设计主要是避免加载到内存中的字节码有重复的存在,避免造成不必要的内存消耗。另外一个问题,我们当然可以打破双亲委派规则,方法很简单,就是我们自定义一个类加载器,让他加载某个类的时候不走启动类就可以了,最典型的案例就是tomcat了。当然有的人会说JDBC的驱动好像也是定义了,自定义加载器可以加载各个厂商自定义的类,确实是这样,但是他并没有打破,他加载其他的类还是走的双亲委派的机制,只是引用了自定义加载器的特性而已。
好了,今天就先说到这个地方吧。下面我们一起来梳理下作下小结:
1、Java的整体运行流程是我们编写的JAVA代码,通过IDEA编译成.class字节码文件,然后打包生成.jar 结尾的jar包,通过java -jar 命令运行,启动jvm虚拟机,这样的一个过程;
2、jvm启动后,会先去找带有main 函数的启动入口类进行加载,就会开启类的加载过程,同时类的加载需要遵循双亲委派机制,先通过父类加载,然后在交由子类进行加载;
3、要实例化一个对象,先通过根类加载器,去查询有没有这个类,发现没有,再交给扩展类,最后交给应用程序类加载器,找到我们写的java代码编译生成的字节码类,把字节码类加载到内存中,然后经过校验语法,在内存中开辟出了一个空间,然后基本类型数据给赋值,然后通过解析把这些字节码的符号引用,变成直接引用就是内存的指针地址,然后对类的变量进行初始化赋值操作,最后进行对象的实例化。
这里做个小说明哈,就是JVM启动后的main函数入口类,和超类Object 这俩谁先加载的顺序是不固定的,取决于类加载器的执行顺序和程序的执行流程,这点要注意,别思考着陷入了误区。
我们理解了类的加载过程,以及要遵循的加载规则双亲委派机制,加载到内存中,进行实例话,清楚了,又感觉有些问题,比如这个类,在内存中到底是什么样子的,是怎么进行放置的,类加载使用后,不使用了,会怎么样的操作,保留那些,清空销毁那些?这些规则又是什么?这些问题我们放到后面来讲。