面试官对于JVM类加载机制的猛烈炮火,你能顶住吗?

本文经受权转自公众号狸猫技术窝的专栏:《从零开始带你成为JVM实战高手》java

做者:救火队队长linux

目录:面试

  1. 前文回顾windows

  2. JVM在什么状况下会加载一个类?服务器

  3. 从实用角度出发,来看看验证、准备和初始化的过程微信

  4. 核心阶段:初始化工具

  5. 类加载器和双亲委派机制加密

  6. 昨日思考题的解答设计

  7. 今日思考题3d

一、前文回顾

我们今天先来回顾一下昨天讲到的JVM总体的一个运行原理。

咱们首先从“.java”代码文件,编译成“.class”字节码文件,而后类加载器把“.class”字节码文件中的类给加载到JVM中,接着是JVM来执行咱们写好的那些类中的代码,总体是这么个顺序。

再看看下图,感觉一下这个过程:

那么今天,咱们就来仔细看看上图中的“类加载”这个过程,看看JVM的类加载机制究竟是怎么样的?

搞清楚这个过程了,那么之后在面试时,对面试官常问的JVM类加载机制,就能把一些核心概念说清楚了。


二、JVM在什么状况下会加载一个类?

其实类加载过程很是的琐碎复杂,可是对于咱们平时从工做中实用的角度来讲,主要是把握他的核心工做原理就能够。

一个类从加载到使用,通常会经历下面的这个过程:

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

因此首先要搞明白的第一个问题,就是JVM在执行咱们写好的代码的过程当中,通常在什么状况下会去加载一个类呢?

也就是说,啥时候会从“.class”字节码文件中加载这个类到JVM内存里来。

其实答案很是简单,就是在你的代码中用到这个类的时候

举个简单的例子,好比下面你有一个类(Kafka.class),里面有一个“main()”方法做为主入口。

那么一旦你的JVM进程启动以后,它必定会先把你的这个类(Kafka.cass)加载到内存里,而后从“main()”方法的入口代码开始执行。

咱们仍是坚持一步一图,你们先看看下图,感觉一下:

接着假设上面的代码中,出现了以下的这么一行代码:

这时可能你们就想了,你的代码中明显须要使用“ReplicaManager”这个类去实例化一个对象,此时必须得把“ReplicaManager.class”字节码文件中的这个类加载到内存里来啊!是否是?

因此这个时候就会触发JVM经过类加载器,从“ReplicaManager.class”字节码文件中加载对应的类到内存里来使用,这样代码才能跑起来。

咱们来看下面的图:

上面就是给你们举的一个例子,相信很是的通俗易懂。

简单归纳一下:首先你的代码中包含“main()”方法的主类必定会在JVM进程启动以后被加载到内存,开始执行你的“main()”方法中的代码

接着遇到你使用了别的类,好比“ReplicaManager”,此时就会从对应的“.class”字节码文件加载对应的类到内存里来。


三、从实用角度出发,来看看验证、准备和初始化的过程

其实上面的类加载时机的问题,对于不少有经验的同窗来讲不是什么问题。可是对于不少初学者来讲,是一个很是重要的须要捋清的概念。

接下来就来简单带着你们,从实用的角度出发,过一下另外三个概念:

验证、准备、初始化

其实对于这三个概念,没太大的必要去深究里面的细节,这里的细节不少很繁琐,对于大部分同窗而言,只要脑子里有下面的几个概念就能够了:

(1)验证阶段

简单来讲,这一步就是根据Java虚拟机规范,来校验你加载进来的“.class”文件中的内容,是否符合指定的规范。

这个相信很好理解,假如说,你的“.class”文件被人篡改了,里面的字节码压根儿不符合规范,那么JVM是无法去执行这个字节码的!

因此把“.class”加载到内存里以后,必须先验证一下,校验他必须彻底符合JVM规范,后续才能交给JVM来运行。

下面用一张图,展现了这个过程:

(2)准备阶段

这个阶段其实也很好理解,我们都知道,咱们写好的那些类,其实都有一些类变量,好比下面的这个“ReplicaManager”类:

假设你有这么一个“ReplicaManager”类,他的“ReplicaManager.class”文件内容刚刚被加载到内存以后,会进行验证,确认这个字节码文件的内容是规范的。

接着,就会进行准备工做,这个准备工做,其实就是给这个“ReplicaManager”类分配必定的内存空间。

而后给他里面的类变量(也就是static修饰的变量)分配内存空间,来一个默认的初始值。

好比上面的示例里,就会给“flushInterval”这个类变量分配内容空间,给一个“0”这个初始值。

整个过程,以下图所示:

(3)解析阶段

这个阶段干的事儿,其实是把符号引用替换为直接引用的过程,其实这个部分的内容很复杂,涉及到JVM的底层

可是注意,同窗们,就我本意而言,但愿第一周的文章,绝对是浅显易懂的,按部就班,要保证每一个同窗都能绝对看懂。

因此针对这个阶段,如今不打算作过深的解读,由于从实用角度而言,对不少同窗在工做中实践JVM技术其实也用不到,因此这里你们就暂时知道有这么一个阶段就能够了。

一样,我仍是给你们画图展现一下:

(4)三个阶段的小结

其实这三个阶段里,最核心的你们务必关注的,就是“准备阶段”

由于这个阶段是给加载进来的类分配好了内存空间,类变量也分配好了内存空间,而且给了默认的初始值,这个概念,你们内心必定要有。


四、核心阶段:初始化

以前说过,在准备阶段时,就会把咱们的“ReplicaManager”类给分配好内存空间

另外他的一个类变量“flushInterval”也会给一个默认的初始值“0”,那么接下来,在初始化阶段,就会正式执行咱们的类初始化的代码了。

那么什么是类初始化的代码呢?咱们来看看下面这段代码:

你们能够看到,对于“flushInterval”这个类变量,咱们是打算经过Configuration.getInt("replica.flush.interval")这段代码来获取一个值,而且赋值给他的

可是在准备阶段会执行这个赋值逻辑吗?

NO!在准备阶段,仅仅是给“flushInterval”类变量开辟一个内存空间,而后给个初始值“0”罢了。

那么这段赋值的代码何时执行呢?答案是在“初始化”阶段来执行。

在这个阶段,就会执行类的初始化代码,好比上面的 Configuration.getInt("replica.flush.interval") 代码就会在这里执行,完成一个配置项的读取,而后赋值给这个类变量“flushInterval”。

另外好比下图的static静态代码块,也会在这个阶段来执行。

相似下面的代码语义,能够理解为类初始化的时候,调用“loadReplicaFromDish()”方法从磁盘中加载数据副本,而且放在静态变量“replicas”中:

那么搞明白了类的初始化是什么,就得来看看类的初始化的规则了。

何时会初始化一个类?

通常来讲有如下一些时机:好比“new ReplicaManager()”来实例化类的对象了,此时就会触发类的加载到初始化的全过程,把这个类准备好,而后再实例化一个对象出来;

或者是包含“main()”方法的主类,必须是立马初始化的。

此外,这里还有一个很是重要的规则,就是若是初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类

好比下面的代码:

若是你要“new ReplicaManager()”初始化这个类的实例,那么会加载这个类,而后初始化这个类

可是初始化这个类以前,发现AbstractDataManager做为父类还没加载和初始化,那么必须先加载这个父类,而且初始化这个父类。

这个规则,你们必须得牢记,再来一张图,借助图片来进行理解:

五、类加载器和双亲委派机制

如今相信你们都搞明白了整个类加载从触发时机到初始化的过程了,接着给你们说一下类加载器的概念。由于实现上述过程,那必须是依靠类加载器来实现的。

那么Java里有哪些类加载器呢?简单来讲有下面几种:

(1)启动类加载器

Bootstrap ClassLoader,他主要是负责加载咱们在机器上安装的Java目录下的核心类的

相信你们都知道,若是你要在一个机器上运行本身写好的Java系统,不管是windows笔记本,仍是linux服务器,是否是都得装一下JDK?

那么在你的Java安装目录下,就有一个“lib”目录,你们能够本身去找找看,这里就有Java最核心的一些类库,支撑你的Java系统的运行。

因此一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库。

(2)扩展类加载器

Extension ClassLoader,这个类加载器其实也是相似的,就是你的Java安装目录下,有一个“lib\ext”目录

这里面有一些类,就是须要使用这个类加载器来加载的,支撑你的系统的运行。

那么你的JVM一旦启动,是否是也得从Java安装目录下,加载这个“lib\ext”目录中的类?

(3)应用程序类加载器

Application ClassLoader,这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类

其实你大体就理解为去加载你写好的Java代码吧,这个类加载器就负责加载你写好的那些类到内存里。

(4)自定义类加载器

除了上面那几种以外,还能够自定义类加载器,去根据你本身的需求加载你的类。

(5)双亲委派机制

JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。

你们看下图:

而后,基于这个亲子层级结构,就有一个双亲委派的机制

什么意思呢?

就是假设你的应用程序类加载器须要加载一个类,他首先会委派给本身的父类加载器去加载,最终传导到顶层的类加载器去加载

可是若是父类加载器在本身负责加载的范围内,没找到这个类,那么就会下推加载权利给本身的子类加载器。

听完了上面一大堆绕口令,是否是很迷茫?别着急,我们用一个例子来讲明一下。

好比你的JVM如今须要加载“ReplicaManager”类,此时应用程序类加载器会问问本身的爸爸,也就是扩展类加载器,你能加载到这个类吗?

而后扩展类加载器直接问本身的爸爸,启动类加载器,你能加载到这个类吗?

启动类加载器心想,我在Java安装目录下,没找到这个类啊,本身找去!

而后,就下推加载权利给扩展类加载器这个儿子,结果扩展类加载器找了半天,也没找到本身负责的目录中有这个类。

这时他很生气,说:明明就是你应用程序加载器本身负责的,你本身找去。

而后应用程序类加载器在本身负责的范围内,好比就是你写好的那个系统打包成的jar包吧,一会儿发现,就在这里!而后就本身把这个类加载到内存里去了。

这就是所谓的双亲委派模型:先找父亲去加载,不行的话再由儿子来加载。

这样的话,能够避免多层级的加载器结构重复加载某些类。

最后,给你们来一张图,感觉一下类加载器的双亲委派模型。

六、昨日思考题的解答

好!今天的文章看完了,相信你们就能大体推测出昨日的思考题的答案了。

我昨天的问题是:如何对“.class”文件处理保证不被人拿到之后反编译获取公司源代码?

其实认真看完今天的文章,就很简单了。首先你编译时,就能够采用一些小工具对字节码加密,或者作混淆等处理

如今有不少第三方公司,都是专门作商业级的字节码文件加密的,因此能够付费购买他们的产品。

而后在类加载的时候,对加密的类,考虑采用自定义的类加载器来解密文件便可,这样就能够保证你的源代码不被人窃取。

七、今日思考题

今天再给你们留一个思考题,相信每一个作Java的同窗,都知道如今通常用Java开发的Web系统,除非是基于Java写中间件,通常都是采用Tomcat之类的Web容器来部署的。

那么你们想一想,Tomcat自己就是用Java写的,他本身就是一个JVM。

咱们写好的那些系统程序,说白了,就是一堆编译好的.class文件放入一个war包,而后在Tomcat中来运行的。

那么,Tomcat的类加载机制应该怎么设计,才能把咱们动态部署进去的war包中的类,加载到Tomcat自身运行的JVM中,而后去执行那些咱们写好的代码呢?

你们先思考,明天文末会给你们进行梳理并给出答案。

End

推荐一个专栏:

《从零开始带你成为JVM实战高手》

做者是我多年好友,之前团队的左膀右臂

一块儿经历过各类大型复杂系统上线的血雨腥风

现任阿里资深技术专家,对JVM有丰富的生产实践经验

专栏目录参见文末,能够扫下方海报进行试读

经过上面海报购买,再返你24元

领取方式:加微信号:Giotto1245,暗号:返现