1。在前言中,我们先来了解一下JVM的整体运行原理。我们先从“.java”代码文件编译出“.class”字节码文件,然后类加载器将“.class”字节码文件中的类加载到JVM中,然后JVM执行我们写的代码。那些好的类中的代码整体上就是这个顺序。我们看下图,感受下这个过程:那么今天,我们就来仔细看看上图中“类加载”的过程,看看JVM的类加载机制是怎么样的?搞清楚这个过程,那么在面试的时候,就可以弄清楚面试官经常问到的JVM类加载机制的一些核心概念。2、JVM在什么情况下会加载一个类?其实类加载的过程是非常琐碎和复杂的,但是对于我们来说从实际工作的角度来说,主要是掌握它的核心工作原理。一个类从加载到使用,一般会经历以下过程:加载->验证->准备->解析->初始化->使用->卸载,所以首先要了解的问题是JVM在执行我们的过程中编写好的代码,在什么情况下会加载一个类?也就是说,这个类什么时候会从“.class”字节码文件中加载到JVM内存中。其实答案很简单。就是在你的代码中使用这个类时给出一个简单的例子。例如,下面有一个类(Kafka.class),它有一个“main()”方法作为主要入口。然后一旦你的JVM进程启动,它会首先将你的类(Kafka.cass)加载到内存中,然后从“main()”方法的入口代码开始执行。我们仍然坚持一步一个脚印。我们先看下图感受一下:那么假设上面的代码中出现了下面这行代码:这时候你可能会认为你的代码中显然需要使用“ReplicaManager”。要用这个类实例化一个对象,此时必须将“ReplicaManager.class”字节码文件中的类加载到内存中!是还是不是?所以这时候就会触发JVM通过类加载器从“ReplicaManager.class”字节码文件中加载对应的类到内存中使用,这样代码才能运行。我们看下图:上面是给大家举个例子,相信还是很容易理解的。简单总结一下:首先,你代码中包含“main()”方法的主类在JVM进程启动后会被加载到内存中,并开始执行你的“main()”方法中的代码。其他的类,比如“ReplicaManager”,此时会从对应的“.class”字节码文件中加载对应的类到内存中。3、从实际的角度来看一下验证、准备和初始化的过程。其实上面的类加载时机问题对于很多有经验的同学来说都不是问题。但是对于很多初学者来说,这是一个非常重要的概念,需要搞清楚。接下来,我将从实践的角度简单介绍一下另外三个概念:验证、准备和初始化。其实这三个概念的细节没有太多深究的必要。这里有很多细节。这很麻烦。对于大多数同学来说,只要心中有以下概念即可:(1)验证阶段简单来说,这一步就是根据Java虚拟机规范对你加载的“.class”进行验证。》文件中的内容是否符合规定的规范,这个相信很容易理解,如果你的.class文件被篡改了,里面的字节码根本不符合规范,那么JVM就无法执行这段字节码!所以在将“.class”加载到内存后,首先要验证它必须完全符合JVM规范,然后才能交给JVM运行。下图就是这个过程:(2)准备阶段其实很好理解,我们都知道我们写的类其实都有一些类变量,比如下面的“ReplicaManager”类。:假设你有这样一个“ReplicaManager”类,在它的内容之后“ReplicaManager.class”文件刚刚加载到内存中,就会进行校验,确认这个字节码文件的内容是规范的。然后,就会进行准备工作。这个准备工作其实就是分配一定数量的我内存空间到“ReplicaManager”类。然后给里面的类变量(也就是static修饰的变量)分配内存空间,得出一个默认的初始值。例如,在上面的例子中,内容空间将分配给类变量“flushInterval”,并赋予初始值“0”。整个过程如下图所示:(3)分析阶段这个阶段所做的其实就是将符号引用替换为直接引用的过程。其实这部分内容非常复杂,涉及到JVM底层。但是各位同学注意啦,就我个人而言,希望第一周的文章绝对通俗易懂,循序渐进,保证每个同学都能绝对看懂。所以对于这个阶段,我现在不打算做深入的解读,因为从实用的角度来说,其实很多同学在工作中没有必要去实践JVM技术,所以在座的大家暂时知道有这样的舞台。同样的,我画一张图给大家展示一下:(4)三个阶段的总结其实在这三个阶段中,最核心的是你必须要注意的“准备阶段”,因为这个阶段是分配给类加载了内存空间,类变量也分配了内存空间,并赋予了默认的初始值,这个概念,大家一定要牢记。4.核心阶段:在初始化之前提到过,在准备阶段,我们会为我们的“ReplicaManager”类分配内存空间。另外,他的一个类变量“flushInterval”也会给一个默认的初始值“0”,那么在初始化阶段,我们类初始化的代码就会正式执行。那么类初始化的代码是什么呢?我们来看看下面的代码:可以看到,对于类变量“flushInterval”,我们打算通过代码Configuration.getInt(“replica.flush.interval”)获取一个值并赋值给它但是会这个分配逻辑在准备阶段执行?不!在准备阶段,只要为“flushInterval”类变量开辟一块内存空间,然后赋初值“0”即可。那么这个赋值代码是什么时候执行的呢?答案是在“初始化”阶段进行。这个阶段会执行类的初始化代码。比如上面的Configuration.getInt("replica.flush.interval")代码会在这里执行完成读取一个配置项,然后赋值给类变量“flushInterval”。另外,比如下图中的static静态代码块也会在这个阶段执行。类似下面的代码语义,可以理解为在类初始化时,调用“loadReplicaFromDish()”方法从磁盘中加载数据副本,并放入静态变量“replicas”中:那么就理解了什么类的初始化是,你得看一下类初始化的规则。什么时候初始化一个类?一般来说,有以下几种机会:比如“newReplicaManager()”实例化类的对象。这时候就会触发类从加载到初始化的整个过程,类就绪,然后实例化一个对象。;或包含必须立即初始化的“main()”方法的主类。另外,这里还有一个很重要的规则,就是如果在初始化一个类的时候,发现它的父类还没有初始化,那么必须先初始化它的父类,比如下面的代码:如果你想"newReplicaManager()"初始化这个类的一个实例,然后会加载这个类,然后初始化这个类,但是在初始化这个类之前,发现作为父类的AbstractDataManager还没有被加载和初始化,那么这个父类必须先被加载,这个父类必须被初始化。这个规则,大家一定要牢记,这里再放一张图,借助图片来理解:五、类加载器和双亲委派机制现在相信大家都明白了类加载从触发时机到初始化的全过程,然后我给大家讲一下类加载器的概念。因为要实现上述过程,就必须依赖类加载器来实现。那么Java中有哪些类加载器呢?简单的说有以下几种:(1)BootstrapClassLoader,主要负责加载我们在机器上安装的Java目录下的核心类。相信大家都知道,如果要自己在一台机器上运行,写的好的Java系统,不管是windows笔记本还是linux服务器,都要安装JDK吗?然后,在你的Java安装目录下,有一个“lib”目录。你可以自己找。下面是一些核心Java类库,以支持您的Java系统的运行。所以一旦你的JVM启动,它首先会依赖启动类加载器来加载你Java安装目录下“lib”目录下的核心类库。(2)扩展类加载器ExtensionClassLoader,这个类加载器其实是类似的,就是在你的Java安装目录下,有一个“lib\ext”目录,里面有一些类,需要使用这个类加载器来加载,以支持您的系统运行。那么一旦你的JVM启动了,你是否必须从Java安装目录加载“lib\ext”目录中的类?(3)应用类加载器ApplicationClassLoader,这类加载器负责加载“ClassPath”环境变量指定路径中的类。其实你可以大致理解为加载你写的Java代码。这个类加载编译器负责将你写的类加载到内存中。(4)自定义类加载器除了以上几种,你还可以根据自己的需要自定义类加载器来加载你的类。(5)双亲委派机制JVM的类加载器具有父子层次结构,也就是说启动类加载器是最上层,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。我们看下图:那么,基于这种父子层级,有父子委托机制是什么意思呢?它假设你的应用类加载器需要加载一个类,它会先委托给它的父类加载器加载,最后转给顶层类加载器加载,但是如果父类加载器在自己负责加载如果没有找到该类,它会将加载权限下推给自己的子类加载器。听了上面一大堆绕口令,你是不是一头雾水?别担心,让我们用一个例子来说明。例如,您的JVM现在需要加载“ReplicaManager”类。这时候应用类加载器就会问它的父亲,也就是扩展类加载器,能不能加载这个类?然后扩展类加载器直接问你爸,启动类加载器,你能加载这个类吗?启动类加载器心想,在Java安装目录下没有找到这个类,自己找吧!然后,将加载权限下推给扩展类加载器的子类。结果扩展类加载器找了半天,也没能在自己负责的目录下找到这个类。这时候他就很生气的说:明明是你的applicationloader负责的,你自己找吧。那么应用类加载器就在你自己负责的范围内了,比如说是你写的系统打包的jar包,你一下子就找到了,就在这里!然后自己把这个类加载到内存中。这就是所谓的双亲委托模型:先找父亲加载,不行再让儿子加载。这样,多级加载器结构可以避免重复加载某些类。最后放一张图给大家感受下类加载器的双亲委派模型。
