大白话谈JVM的类加载机制
前言
我们很多小伙伴平时都是做JAVA开发的,那么作为一名合格的工程师,你是否有仔细的思考过JVM的运行原理呢。
如果懂得了JVM的运行原理和内存模型,像是一些JVM调优、垃圾回收机制等等的问题我们才能有一个更清晰的概念。
为了走进JVM,深入了解底层,王子打算写一个JVM的专题,留下自己对JVM探索的足迹,同时也希望能帮到小伙伴们更好的理解JVM。
那我们开始吧。
JAVA代码的运行流程
首先我们就来聊一聊JAVA代码是怎么运行起来的,这部分比较基础相信大家都知道,就当成是个复习吧。
我们编写的代码都是在java文件中编写的,然后会编译成class字节码文件。
当我们使用到哪个类的时候就会通过类加载器把class字节码文件中的类加载到jvm内存中,然后就是在jvm内存中运行我们的代码了。
整体的运行流程就是这样,相信小伙伴们都很清楚这些,但是有关类加载器是如何把类加载到jvm内存中的,小伙伴们有考虑过吗?
今天我们主要就是聊这一部分。
JVM什么时候加载类
其实说到类加载的底层机制,这是一个很复杂的过程,但是对于我们平时的工作来讲,只要懂得它的核心原理就可以了。
一个类的加载过程会经历如下的几个过程:
加载、验证、准备、解析、初始化、使用、卸载
首先我们就先弄明白一个问题,jvm是什么时候去加载类的呢?
其实答案很简单,就是我们什么时候使用到了这个类,它就去class字节码文件中去加载这个类。
而作为程序的入口,具有main方法的类,肯定是最开始的时候就加载到jvm中了。
对于加载类的时间点问题,其实就是这么简单。
类加载器和双亲委派机制
既然我们知道了类加载的时间点,那么jvm是通过什么方式对类进行加载的呢?就是类加载器。
那接下来我们就来聊聊jvm的类加载器。
jvm的类加载器总体上可以分成4层,我们一起看一下。
1.启动类加载器
首先就是jvm启动的第一道关口,启动类加载器Bootstrap ClassLoader,它主要是加载java的核心类。
相信大家都知道,无论是什么环节下运行java程序,都是要安装jvm虚拟机环境的,而在这个环境的目录中是有一个lib文件夹的,这个文件下就是java最核心的类库,支撑着java系统的运行。
所以一旦jvm启动,那么首先就会通过启动类加载器去加载lib文件夹下的核心类库。
2.扩展类加载器
然后我们就到了第二层,扩展类加载器Extension ClassLoader,这个类加载器其实与启动类加载器是类似的。
在我们的jvm虚拟机环境目录下,是有一个lib/ext的文件夹的,这里面的类就是java运行环境的一些扩展类,这些扩展类就是在jvm启动后,通过扩展类加载器进行加载的。
3.应用程序类加载器
加载完核心类库和扩展类,这时候就到了第三层,应用程序类加载器Application ClassLoader,这个类加载器你就可以理解成是加载我们写好的java代码的就可以了。
4.自定义类加载器
前面的三层就是基本的类加载器了,然后第四层是自定义类加载器,根据一些特殊的需求来自己定义类加载器加载我们的类。
整体上类加载器就是这么的4层结构。很多小伙伴可能都听说过双亲委派机制,那么什么是双亲委派机制呢,王子就和大家用最接地气的语言描述一下。
其实很好理解,就是当我们的类加载器要加载一个类的时候,它首先会委派给它的父亲去加载,但是如果它的父亲没找到就会把这个事交给他的孩子自己去完成了。
这就是双亲委派机制。
举个例子,假如我们的应用程序类加载器要加载一个类A,那么首先它会先回家找它老爸扩展类加载器,问问“老爸,你那有这个类A吗?”
然后扩展类加载器接到这个请求之后,同样也懒得处理,再去找它爷爷启动类加载器。
它爷爷找了一圈没找到类A,很生气,就对扩展类加载器说,“我这没有,你自己找去!”
然后扩展类加载器就灰溜溜的自己找了一圈,同样也没找到,这时候就找到应用类加载器了,说:“我这哪有你这个类A,这明明是你自己应该干的活,爱上哪找上哪找去,我不管了”。
这时候应用类加载器就只能自己去处理了,找了一圈发现找到了类A,就把它加载到jvm内存中了。
相信大家看了这个例子应该很容易理解了吧。
所以假设我们自己创建了一个类java.lang.String,它是不会被应用类加载器加载到内存中的,因为父类中可以找到这个类,就直接给加载到内存中了。
聊聊验证、准备、解析、初始化阶段
聊完了加载,我们再来看看验证、准备、解析、初始化这几个阶段jvm都做了什么。
1.验证阶段
这一步其实很容易理解,就是jvm根据java规范,来校验你加载进来的class文件中的内容是否符合规范,如果不符合规范jvm是无法正常运行的。
所以在加载后,首先就是验证阶段。
2.准备阶段
假设我们有一个类A,刚刚加载并通过了验证,那么就会进行准备工作。
这个准备工作其实就是给类A分配一定的内存空间,然后给里面的静态变量(static修饰的变量)也分配内存空间,并赋初始值。
3.解析阶段
这个阶段干的事实际上是把符号引用替换为直接引用,这一过程网上有很多资料,还是比较复杂的,如果感兴趣小伙伴们可以自己查阅一下资料。
实际工作中也很少会接触这部分的内容,所以我们知道有这么个阶段就可以了。
4.初始化阶段
在准备阶段,我们把类A的内存已经分配完了,那么初始化阶段要做些什么事呢?我们先看一下类A的代码
public class A { private static String i=System.getProperty("i"); }
准备阶段我们只是给变量i分配了内存空间,并赋值了初始值,但是后边的System.getProperty(“i”)是不会执行的。
没错,这部分代码就是在初始化阶段执行的,另外静态代码块也会在这一阶段执行。
举个例子,比如我们新建一个对象new A(),此时就会触发从加载到初始化的全过程,把这个类准备好并创建一个实例对象。
此外这里有一个规则,如果类A继承了类B,那么在初始化类A的时候,如果发现类B还没有初始化,会先初始化类B。
扩展
到这里关于JVM的类加载机制其实就已经说完了,王子再给大家扩展一个小知识点。
小伙伴们想过没有,Tomcat也是用java开发的,那么它的类加载机制是什么样的呢,为什么就能支持jsp呢?
其实它就是利用了自定义类加载器这一机制,自己自定义了很多类加载器,整体的结构如下:
Tomcat自定义了这么多的类加载器,用来加载它自己的核心类库,并且Tomcat是打破了双亲委派机制的,感兴趣的小伙伴可以自己去查资料了解一下,王子就不在本篇文章长篇大论来聊Tomcat了。
总结
今天我们聊的内容还是jvm中比较基础的部分,以后的文章我们再慢慢深入,去探索jvm的底层原理,如果对JVM感兴趣的小伙伴可以关注王子的后续文章哦,我们可以一步一个脚印的逐步分解JVM,去了解JVM的垃圾回收机制、性能调优等等实用性问题,让你面对JVM的面试或者生产实践也可以游刃有余。
那我们下次见。
往期文章推荐: