TOC
一、 我们写的 Java 代码到底是如何运行起来的?
你采用“java”命令,实际上此时就会启动一个 JVM 进程。
这个 JVM 就会来负责运行这些“.class”字节码文件,也就相当于是负责运行我们写好 的系统。
接着下一步,JVM 要运行这些“.class”字节码文件中的代码,那是不是首先得把这些“.class”文件中包含的各种类给加载进来?
这些“.class”文件不就是我们写好的一个一个的类,此时会采用 类加载器把编译好的那些“.class”字节码文件给加载到 JVM 中,然后供后续代码运行来使用。
最后一步,JVM 就会基于自己的字节码执行引擎,来执行加载到内存里的我们写好的那些类了,比如你的代码中有一个“main()”方法,那么 JVM 就会从这个“main()”方法开始执行里面的代码。他需要哪个类的时候,就会使用类加载器来加载对应的类,反正对应的类就在“.class 文件中。
二、类加载机制
那么今天,我们就来仔细看看中的“类加载”这个过程,看看JVM的类加载机制到底是怎么样的?
一个类从加载到使用,一般会经历下面的这个过程: 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
也就是说,啥时候会从“.class”字节码文件中加载这个类到 JVM 内存里来。就是在你的代码中用到这个类的时候。
验证、准备、初始化
(1)验证阶段. 简单来说,这一步就是根据 Java 虚拟机规范,来校验你加载进来的“.class”文件中 的内容,是否符合指定的规范。
(2)准备阶段. 其实就是给这个类分配一定的内存空间 然后给他里面的类变量(也就是 static 修饰的变量)分配内存空间,来一个默认的初始值,比如static int 类型的给个0
(3)解析阶段 实际上是把符号引用替换为直接引用的过程,其实这个部分的内容很复杂,涉及到 JVM 的底层
4)三个阶段的小结。其实这三个阶段里,最核心的大家务必关注的,就是“准备阶段”,因为这个阶段是给加载进来的类分配好了内存空间,类变量也分配好了内存空间,并且给了默认的初始值,这个概念,大家心里一定要有。
核心阶段:初始化
public class ReplicaMananger {
public static int flushInterval =
Configuration.getInt("replica.flush.interval");
}
准备阶段会执行这个赋值逻辑吗? NO!在准备阶段,仅仅是给“flushInterval”类变量开辟一个内存空间,然后给个初始值“0”罢了。
那么这段赋值的代码什么时候执行呢?答案是在“初始化”阶段来执行。 在这个阶段,就会执行类的初始化代码,比如上面的 Configuration.getInt("replica.flush.interval")
代码就会在这里执行,完成一个配置项的读取,然后赋值给这个类变量“flushInterval”
另外比如 static 静态代码块,也会在这个阶段来执行。
那么搞明白了类的初始化是什么,就得来看看类的初始化的规则了。
什么时候会初始化一个类?
一般来说有以下一些时机:比如“new ReplicaManager()”来实例化类的对象了,此时就会触发类的加载到初始化的全过程,把这个类准备好,然后再实例化一个对象出来;
或者是包含“main()”方法的主类,必须是立马初始化的。 此外,这里还有一个非常重要的 规则,就是如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类
简单概括一下:首先你的代码中包含“main()”方法的主类一定会在JVM进程启动之后被加载到内存,开始执行你的“main()”方法中的代码
接着遇到你使用了别的类,比如“ReplicaManager”,此时就会从对应的“.class”字节码文件加载对应的类到内存里来。
3、到底什么是JVM的内存区域划分?
JVM在运行我们写好的代码时,他是必须使用多块内存空间的,不同的内存空间用来放不同的数据,然后配合我们写的代码流程,才能让我们的系统运行起来。
4、存放类的方法区
这个方法区是在JDK 1.8以前的版本里,代表JVM中的一块区域。
主要是放从“.class”文件里加载进来的类,还会有一些类似常量池的东西放在这个区域里。
但是在JDK 1.8以后,这块区域的名字改了,叫做“Metaspace”,可以认为是“元数据空间”这样的意思。当然这里主要还是存放我们自己写的各种类相关的信息。
5、类加载器和双亲委派机制
现在相信大家都搞明白了整个类加载从触发时机到初始化的过程了,接着给大家说一下类加载器的概念
因为实现上述过程,那必须是依靠类加载器来实现的
那么Java里有哪些类加载器呢?简单来说有下面几种:
(1)启动类加载器
Bootstrap ClassLoader,他主要是负责加载我们在机器上安装的Java目录下的核心类的,所以一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库。
(2)扩展类加载器
Extension ClassLoader,这个类加载器其实也是类似的,就是你的Java安装目录下,有一个“lib\ext”目录
(3)应用程序类加载器
Application ClassLoader,这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类
(4)自定义类加载器
除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类。
(5)双亲委派机制
JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。
什么意思呢?
就是假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载
但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。
比如你的JVM现在需要加载“ReplicaManager”类,此时应用程序类加载器会问问自己的爸爸,也就是扩展类加载器,你能加载到这个类吗?
然后扩展类加载器直接问自己的爸爸,启动类加载器,你能加载到这个类吗?
启动类加载器心想,我在Java安装目录下,没找到这个类啊,自己找去!
然后,就下推加载权利给扩展类加载器这个儿子,结果扩展类加载器找了半天,也没找到自己负责的目录中有这个类。
这时他很生气,说:明明就是你应用程序加载器自己负责的,你自己找去。
然后应用程序类加载器在自己负责的范围内,比如就是你写好的那个系统打包成的jar包吧,一下子发现,就在这里!然后就自己把这个类加载到内存里去了。
这就是所谓的双亲委派模型:先找父亲去加载,不行的话再由儿子来加载。
这样的话,可以避免多层级的加载器结构重复加载某些类。