超级通俗易懂,深入浅出Java的类加载机制,原来是这么回事~

Posted 庆哥Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超级通俗易懂,深入浅出Java的类加载机制,原来是这么回事~相关的知识,希望对你有一定的参考价值。

熟悉的读者都知道,庆哥的文章一向是通俗易懂,直接要害,一看就知道,原来是这么回事,很多人表示,终于懂了,不信,咱们来看看JVM虚拟机中的一个重点知识-类加载


你要先搞清楚这些

类加载这个知识点在Java知识体系中属于哪个位置?,稍微熟悉一点的肯定知道,必须是Java虚拟机啊 ,是的没错,请记住这句话:

学Java必须学习Java虚拟机,JVM不会,你不是一个合格的Java程序员,被淘汰的可能性很大~

有人说了,咋回事,看你一篇文章,还没开始呢?我都不算是Java程序员了?

唉,毕竟我总为你们着想~


Java的类加载属于 JVM虚拟机的知识 一般在学习虚拟机的时候出现,平常基础学习可以先不了解~

啥意思呢?你环境变量还没搞清楚呢?还去弄类加载?不现实啊,不过你还别说,我在教一些人学Java的时候还真发现,好多人对环境变量这块还真是一知半解~

不信,我考你一个问题,俩吧,如下:

  1. 请问javac --version输出的是什么的版本?
  2. 再请问java --version输出的是什么版本?

很多人的基础其实真的差,比如你说下,Java中的引用数据类型包括哪四种?

懵x了吧?

咋又扯远了,不说了,讲回咱们的类加载,那么在学习类加载之前,请你一定记住如下这句话:

类加载核心目的:把存在本地的Java源代码经过javac编译生成的字节码文件(xxx.class 二进制文件)加载到内存中去,让其成为可执行状态,即可用

请把这读个十遍,理解个一二,咱们再继续~

这个时候,你要动脑筋思考了,为啥要加载啊?

然后再请你记住这么一句话:

**为啥要加载**:任何程序想要运行都需要加载进内存中

感觉到了没?看到没,这就是干货啊~

接下来,你还要再思考一个问题,就是这个加载操作谁来完成?谁去执行这个加载动作?

别想了,是JVM,具体就是用ClassLoader,也就是类加载器,也是一个类,不过由JVM去操作,对于我们来说,是无感的~ (加载进内存后还有很多操作)

走神了?可千万别,接下来的内容超级重要,啥嘞?

挺好了,字节码文件对应一个由JVM生成的Class对象,把这句话狠狠的记在心里,然后继续听我给你说:

Java源码和编译生成的字节码文件是我们可以真实看到的,也就是Hello.java和Hello.class,这个时候有一个很重要的知识,就是字节码文件加载进内存后会产生一个与之对应的Class对象,这个Class对象是真实存在的Class类(lang包下,final类型)的一个实例,这个对象主要由JVM去操作~

上面都是重点,没有废话,那好,到了这里,如果你的理解能力一绝的话,那上面的这些看懂了,就OK了,下面的基本上不用咋看~

but我觉得在看的各位,理解能力都是0.5绝,还差点意思,那就老实听着我给你继续唠一唠

要知道大概的步骤

到了这里,我们就直接上干货,首先,你要清楚,类加载的步骤,主要有这么个回事:

  1. 加载(领路人):找到字节码文件,将其送进内存中(对应的Class对象)
  2. 链接(又分为三个小阶段)
    1. 验证(过安检):就是看看你这个字节码正不正经,必须是安全可靠的字节码文件,比如检查魔数等
    2. 准备:为静态字段分配内存并设置默认值
    3. 解析:符号引用(一个代指,不知道具体在哪)转换为实际引用(具体的位置)
  3. 初始化:设置类的正确初始值,JVM开始初始化类(执行client方法),完成这不,类才真实成为可执行的状态~

也就是说,总计三个大步骤,一共是五步,我就习惯上说5步,也就是加载,验证,准备,解析,初始化,这五步操作,要闭着眼睛都能倒背如流~(倒背我没试过,要不你试试也不是不可以)

然后对于类加载的这五步,总结一下就是:

类加载通过三个大阶段,共计5个步骤,将类从外部加载进内存中,使其变得可用,也就是产生了一个Class对象!

以上依然是没有废话,每一个字,每一个词都很重要,务必仔细阅读理解,学到了,那就是你的~

这里有一个需要注意的就是加载,验证,准备和初始化这四个阶段的顺序是确定的,但是解析这一阶段就不一定了,它也有可能在初始化之后才开始,这是为了支持java的运行时绑定,另外以上这几个阶段是按顺序开始,但是可没有说按顺序结束,也就是他们一般情况下都是混杂着进行的

把每个步骤单独拎出来说一说

接下来,我们对每一个步骤再单独拿出来说一说,看一看,这几个步骤都是怎样的?重点清楚,每个步骤,到底干了啥?

加载阶段

这一阶段其实就是类加载器真正作用的阶段,这一阶段主要做的事情就是把字节码文件安全的放到内存中,工作就完成了,然后你还要特别清楚的一件事情就是,在这个阶段:

是将class文件读入内存,并为之创建一个Class对象

注意到重点了吧,产生了一个Class对象,这个一定记住了,把这个阶段干的事情稍微细化下就是:

(1):通过一个类的全限定名来获取定义此类的二进制流(啥玩意?理解成字节码文件就完事了

(2):将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

(3):在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(重点)

然后稍微总结一下就是,这个加载阶段,它是一个过程,这个过程的结果就是将字节码文件(二进制表示)加载进内存中生成对应的一个Class对象,这个Class对象表示的就是该类的一个映射,就是在内存中对应的该类一个存在形式,我们通过操作这个Class对象来实现对该类的一些操作~

OK,注意理解,一定注意理解~

验证阶段

这个阶段,其实人家的名字就说的很清楚了,就是俩字验证,也就是说,在这个阶段里,主要就是确保加载的**类的正确性**,以防加载对虚拟机有危害的类。这样的话对安全的类就有一个评判标准,一般有如下验证步骤:

• 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

• 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

• 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

• 符号引用验证:确保解析动作能正确执行。

然后对于这一阶段,还有一个需要特别注意的地方就是:

验证这一阶段其实是非常重要的,是用来保证虚拟机的安全,但是这一阶段却不是必须的,什么意思呢?也就是说,当你确定你这个类是安全的,比如你经过反复验证,这个类符合虚拟机规范,很安全,就可以考虑采用Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备阶段

到了准备这个阶段,依然是需要着重搞清楚,在这个阶段里面主要是做了哪些事情?

那么在这个准备阶段,它到底干了哪些事情?

正式为类变量静态全局变量,也就是被static修饰的变量)分配内存并设置类变量默认值的阶段,这些变量所使用的内存都将在方法区中进行分配。

所以,看到重点了吗?注意了,就是为类变量赋值,而且是默认值,记着这点那对准备阶段来说就是OK的了~

解析阶段

接下来就来到解析这个阶段,先说好,解析这个阶段是需要费点脑力去理解一个重要概念的,同样,让我们先搞清楚,在这个阶段里面,主要做了什么事情?

其实很简单,在解析这个阶段,主要就是符号引用转换为直接引用重点就是搞清楚符号引用和直接引用是啥?

所以,重点已经出现了,就是你要理解什么是符号引用以及什么是直接引用,重点则是对符号引用的理解,我接下来所说的内容,就是为了让你明白,这个符号引用到底是个啥?

这里的符号引用什么意思呢?其实也就是字面量,也就是我们写的那些东西,比如我们写一个测试类叫做Test.java,那么这个文件中肯定有个类名叫做Test,这个“Test”就是个字面量,同时在虚拟机层面它就是个符号引用,这里将其转变成直接引用就是将其替换为其在内存中的内存地址。

要知道,我们写的代码,加载进内存中都是要有空间将其存储起来的,代码的每一个字母都是要存储的~

然后我们再继续往下理解,首先明确记住,符号引用就是字面量(你能看见的都叫做字面量),比如我们写的一个类文件中引用了另一个类,但是在javac编译成字节码文件的时候,并不知道引用的这个类在哪(还没分配内存地址,不知道具体位置),但是此时需要有个标志代指这个类,这个标志就是符号引用,这个符号引用是什么样的,不是随随便便命名的,有明确定义,在Java虚拟机规范的Class文件格式中。

就是字节码文件是有规范的,该怎样,要怎样,有哪些是有要求的,编译后的字节码文件中就含有相应的符号引用

不知道到了这里,你理解的怎样了?如果差点意思,那就再多读几遍~

其实符号引用强调的是编译成class文件之后,这个时候并不能确定一个类的引用到底指向谁,因此只能使用特定的符号代替,这就叫做符号引用,比如在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

OK,以上就是对符号引用的解读了,那什么又是直接引用呢?简单直白的说,在类加载阶段,经过解析将符号引用解析成直接引用,也就成了指向一个具体目标的内存地址。

也就是说,你就可以理解为就是实际的内存地址

深入严格来说:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用的目标一定在内存中存在。

初始化阶段

接着我们再看最后一个阶段,也就是初始化阶段,那这个阶段式怎么回事呢?

核心就是一个赋值,然后搞清楚:初始化阶段做了什么事情?什么情况下会触发初始化?

首先来看,做了什么事情:

  1. 给类的静态变量赋予实际的值,也你就是你设定的值,不再是默认值
  2. 执行静态代码块(静态代码块只能访问定义在静态代码块之前的静态变量,定义在静态代码块之后的静态变量,可以赋值,但是不能访问)

Java中,并不是一下子把所有的类都给加载完,而是用到谁加载谁,比如你程序中定义一个类,但是此时用不到,如果你用javac将其全部编译生成字节码文件,此时要知道,生成的字节码都是单独存在的,也就是这个暂时用不到的类也有个对应的字节码文件,那刚开始用不到就先不把它加载进内存,一旦用到它再去加载它,那这个时候就得搞清楚,什么时候才算用到它了,也就是什么时候会触发类的初始化,谈到初始化,其实就是在讲类的加载了有且只有五种):

1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的Java场景是:

  1. 关键字new实例化对象的时候,这个最常见也是你常用的了
  2. 读取或者设置一个类的静态字段的时候(注意,final修饰的定值会在编译器就把值设置好并放入到常量池)
  3. 还有就是,当你调用了一个类的静态方法

2、你使用了反射,也就是对类进行反射调用的时候

3、类之间存在继承关系的时候,当你初始化子类,发现其父类没有初始化,则先初始化其父类

4、还是有就是容易被忽视的虚拟机启动的时候,这个时候会初始化main方法所在的类

5、最后一个就是当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。(这个其实个人觉得,你先记住有这个情况就行,不必深究到底目前)

以上内容就是在初始化阶段,你需要了解的内容了~

其实谈到java的类加载,那肯定离不开类加载器的内容,接下来我们再说说这个类加载器相关的内容~

类加载器

类加载依靠的就是类加载器,也就是Classoader,类加载的过程有5步,加载,验证,准备,解析和初始化,实际上,我们能干预操作的也就只有加载,剩余的验证,准备,解析和初始化是JVM控制的,不让你碰~

我们需要重点理解知识点ClassLoader和双亲委派机制

Bootstrap ClassLoader

这个可以被叫做根加载器,启动类加载器,引导类加载器,反正都是它,关于这个加载器,我们需要了解的知识点如下:

  1. 主要负责加载核心类库:JDK\\jre\\lib下或者被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  2. C++实现,JVM的一部分,看不到它的父类,JVM启动会自动运行的一个加载器,需要负责加载其他Java实现的加载器

OK,上述两点清楚了,这个根加载器就事OK的了~

ExtClassLoader

这个叫做扩展类加载器,由于jdk的一些升级,这个被什么平台类加载器替代啥的,这个我们先不管,对于这个扩展类加载器最基本的东西,你得理解,也就是如下这些内容:

  1. 主要加载:JDK\\jre\\lib\\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
  2. Java语言实现,被根加载器加载

以上这些内容足矣~

AppClassLoader

我们写的类,主要就是使用这个加载器来加载了,叫做应用类加载器,是个默认加载器,关于它,你需要知道:

  1. 主要加载:用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  2. Java语言实现,被根加载器加载

以上就是三个我们需要知道的类加载器,接下来依据这些类加载器,我们还需要了解的一个知识点就是双亲委派机制~

双亲委派

什么是双亲委派机制呢?它是这样说的:

默认自己不尝试加载,而是先交给自己的父类加载,父类无法加载,自己在加载,自己也加载不了就报异常,也就是AppClassLoader -> ExtClassLoader -> Bootstrap ClassLoader
这么一个走向~

所以你看,看似双亲委派听起来很高大上,其实概念是简单易懂的~

那这个时候你就得思考了,搞这么一个机制有什么用?不能是平白无故搞这么一个机制吧,存在即有理,关于双亲委派机制的好处,你要熟悉如下两点:

  1. 避免重复加载(如果父类加载器已经加载过这个类的话,子类加载器就不需要再次加载了)
  2. 安全性(java的核心api类库都是被启动类加载器加载的,如果外部突然要加载一个,一个比如java.lang.xxx的话,这个会被传到启动类加载器,启动类加载器发现这个已经加载过啦,所以不管你,直接返回已经加载的,这样就有效的防止核心api被篡改。)

你看,以上就是双亲委派机制你需要了解的知识点了~

接下来,再简单给大家扩展一些知识~

类加载器应用场景

整个类加载过程,我们可以在第一步加载这块做操作来实现一些功能,也就是通过自定义类加载器来解决一些实际问题,比如:

  1. 解决类的依赖冲突
  2. 实现热部署和热加载
  3. jar包的加密保护

解决类依赖冲突:
比如一个项目中的某个业务,本身用到了一个开源库(极简理解为一个类),然后又引用了一个其他的功能模块,这个功能模块本身也是用了这个开源库,只不过这个功能模块用的是这个开源库2.0版本,而业务本身用的是开源库1.0版本,区别就是类中的方法不一样,2.0版本多了方法,由于我们都是用Maven管理这些开源库,Maven的机制会使用引用路径最短的开源库为最终的那个,也就是使用业务本身的1.0版本,这就导致调用2.0版本中的方法找不到,报一个NoSuchMethodException异常~

不要想着统一版本,项目中的东西不是说升级就升级的,解决方案就是自定义类加载器,给这个功能模块自定义加载器去加载这个开源库~

热加载:
热加载的目的就是快速启动,比如我们开发中需要频繁重启应用来调试应用,但是项目的重启是很费时间的,这个时候可以采用热加载快速进行应用的启动。

热加载的方案比较多,比较推荐的是Spring官方的Spring boot devtools,核心理念就是重启不需要把所有的内容全部重新加载,而是只重新加载改动的内容,这个就可以通过自定义类加载器来完成~

热部署:
本质和热加载差不多,只更改变化的内容达到快速部署

加密保护:

这个实际上就是防止别人对我们的字节码文件反编译,我们可以使用自定义类加载机制实现加密保护~

核心就是把字节码打包的时候进行正向加密,然后通过自定义类加载器对加密后的包进行解密再按照字节码正常的加载过程去加载就行了~

至于如何加解密就属于加密算法这块,不深入研究

庆哥推荐学习方法

大家平常的技术学习,我觉得可以采用费曼学习法,不熟悉的可以去了解一下,就比如关于类加载这块的知识,我学了以后我还写文章,写博客教大家这个知识,而且,我为了巩固自己的学习效果,我还把它讲出来,正所谓,你知道了不一定能给别人讲懂,你能别人讲懂了那说明你是真的会了,所以,我还傻乎乎的把它录制出来,哈哈~

虽然累,但是效果好啊毕竟学到了才算自己的

以上是关于超级通俗易懂,深入浅出Java的类加载机制,原来是这么回事~的主要内容,如果未能解决你的问题,请参考以下文章

最通俗易懂的 HashMap 源码分析解读

深入JVM--探索Java虚拟机的类加载机制

《深入理解Java虚拟机系列三》--- 7+2种垃圾收集器(通俗易懂)

《深入理解Java虚拟机系列二》--- 垃圾回收算法(通俗易懂)

谁说深入浅出虚拟机难?现在我让他通俗易懂(JVM)

6类加载器深入解析与重要特性剖析