1. 类加载过程图示
从上图的“类加载” 过程,去看看 JVM的类加载机制是什么原理?
2. JVM 在什么情况下会加载一个类
一个类从加载到使用,会经历如下的过程:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
JVM在执行写好的代码时,在代码中用到这个类的时候,就会从 “.class” 字节码文件中加载这个类到 JVM 内存里来。
示例:在一个类(Kafka.class)里有一个“main()” 方法作为主入口。一旦JVM进程启动,就会先把 Kafka.class 类加载到内存里,然后从 “main()” 方法的入口代码开始执行。
如果在main() 方法中使用了 “ReplicaManager” 这个类去实例化对象,那么也需要将 “ReplicaManager.class” 字节码文件中的这个类加载到内存里去。
其加载过程如下图所示:
3. 验证、准备和初始化
1、 验证阶段
验证,就是根据 Java 虚拟机规范,来校验你加载进来的 “.class” 文件中的内容,是否符合指定的规范。只有完全符合 JVM 规范,后续才能交给 JVM 来运行。
2、 准备阶段
一个类中,有一些类变量;比如有 “ReplicaManager” 类,它的 “ReplicaManager.class” 文件内容刚刚被加载到内存之后,会进行验证,确认这个字节码文件的内容是规范的。
接着就会进行准备工作。也就是给这个 “ReplicaManager” 类分配一定的内存空间。然后给它里面的类变量(也就是 static 修饰的变量)分配内存空间,来一个默认的初始值。
比如上面的示例里,就会给 “flushInterval” 这个类变量分配内容空间,给一个 “0” 这个初始值。
整个过程,如下图所示:
3、 解析阶段
这个阶段,是把 符号引用替换为直接引用 的过程。
4、 三个阶段小结
这里最核心的是,“准备阶段” 。准备阶段 是给加载进来的类分配好内存空间,类变量也分配好内存空间,并且给了默认的初始值。
4. 核心阶段: 初始化
1、 类的初始化是什么
在准备阶段之后,就是进行初始化的阶段了。这一阶段会正式执行我们的类初始化的代码。
在上述代码中,对于 “flushInterval” 类变量,这里是通过 Configuration.getInt("replica.flush.interval")这段代码来获取一个值,并且赋值给它。
而在准备阶段是不会执行这个赋值逻辑的,它仅仅是对类变量分配内存,并给初始值 “0” 而已。
该赋值逻辑的代码,是在 “初始化” 阶段执行的。这一阶段,会执行类的初始化代码,而这时上述的赋值逻辑代码就会执行,完成一个配置项的读取,然后赋值给该类变量 “flushInterval” 。
另外,static 静态代码块,也会在这个阶段来执行。
此外,static 静态方法可以给 static 静态变量赋值,也就是说,类初始化的时候,调用static静态方法从磁盘中加载数据副本,并放在静态变量中,如下示例:
2、 什么时候会初始化一个类?(类的初始化的规则)
一般来说,比如使用 "new 类名()" 来实例化类的对象时,就会触发类的加载到初始化的全过程,把这个类准备好,然后再实例化一个对象处来;
或者是包含 “main() ” 方法的主类,必须是立马初始化的。
此外,如果初始化一个类的时候,发现他的父类未初始化,就必须先初始化它的父类。
示例代码如下:
如果要“new ReplicaManager()” 初始化这个类的实例,就会加载这个类,然后初始化这个类;
而在初始化这个类之前,发现 AbstractDataManager作为父类还没加载和初始化,就必须先加载这个父类,并且初始化该父类。
5. 类加载器和双亲委派机制
类加载从触发时机到初始化的过程,依靠类加载器来实现。
Java里有以下几种类加载器:
1)启动类加载器
Bootstrap ClassLoader,在JVM启动时,首先就会依托启动类加载器,负责加载机器上的 Java安装目录下的 “lib” 目录中的核心类库。
2)扩展类加载器
Extension ClassLoader,主要负责从Java安装目录下,加载 “lib\ext” 目录中的类。
3)应用类加载器
Application ClassLoader,负责去加载 “ClassPath” 环境变量所指定的路径中的类。其实就是加载写好的Java代码的类到内存里。
4)自定义类加载器
自定义的类加载器,可以根据自己的需求加载你的类。
5)双亲委派机制
JVM的类加载器有层级结构,启动类加载器在最上层,扩展类加载器在第二层,第三层是应用类加载器,最后一层是自定义类加载器。
基于上面的层级结构,就有一个双亲委派的机制。
**所谓双亲委派机制:**就是在应用程序类加载器需要加载一个类,它首先会委派给父类加载器去加载,最终传导到顶层的类加载器去加载;如果父类加载器在自己负责加载的范围内,没找到这个类,就会下推加载权利给自己的子类加载器。
简而言之,就是先找父类加载器加载,不行的话逐层往下由子类加载器。从而避免多层级的加载器结构重复加载某些类。
6. 思考题
如何对“.class” 文件处理保证不被人拿到以及反编译获取公司源代码?
**答:**首先在编译时,可以采用一些小工具对字节码加密,或者做混淆等处理;也可以购买第三方公司的商业级的字节码文件加密产品;然后在类加载的时候,对加密的类,考虑采用自定义的类加载器来解密文件即可,从而保证你的源代码不被人窃取。